|
1 | | -""" |
2 | | -Draft comment for pypsa-validator GitHub PRs. |
| 1 | +"""Text components to generate validator report.""" |
3 | 2 |
|
4 | | -Script can be called via command line or imported as a module. |
5 | | -""" |
6 | | - |
7 | | -import argparse |
8 | 3 | import os |
9 | 4 | import re |
10 | 5 | from dataclasses import dataclass |
11 | 6 | from pathlib import Path |
12 | 7 |
|
13 | 8 | import numpy as np |
14 | 9 | import pandas as pd |
| 10 | + |
15 | 11 | from metrics import min_max_normalized_mae, normalized_root_mean_square_error |
16 | | -from numpy.typing import ArrayLike |
17 | 12 | from utils import get_env_var |
18 | 13 |
|
19 | 14 |
|
20 | | -def create_numeric_mask(arr: ArrayLike) -> np.ndarray: |
21 | | - """ |
22 | | - Create a mask where True indicates numeric and finite values. |
23 | | -
|
24 | | - Parameters |
25 | | - ---------- |
26 | | - arr : array-like |
27 | | - Input array |
28 | | -
|
29 | | - Returns |
30 | | - ------- |
31 | | - np.ndarray: Numeric mask where True indicates numeric and finite sort_values |
32 | | -
|
33 | | - """ |
34 | | - arr = np.array(arr) |
35 | | - return np.vectorize(lambda x: isinstance(x, (int, float)) and np.isfinite(x))(arr) |
36 | | - |
37 | | - |
38 | 15 | @dataclass |
39 | 16 | class CommentData: |
40 | 17 | """Class to store data for comment generation.""" |
@@ -74,12 +51,15 @@ def errors(self, branch_type: str) -> list: |
74 | 51 | logs = list( |
75 | 52 | Path(f"{self.dir_artifacts}/logs/{branch_type}/.snakemake/log/").glob("*") |
76 | 53 | ) |
77 | | - if len(logs) != 1: |
| 54 | + if len(logs) > 1: |
78 | 55 | msg = ( |
79 | | - "Expected exactly one log file in snakemake/log directory " |
80 | | - "({branch_type} branch)." |
| 56 | + "Expected exactly one log fiie in snakemake/log directory " |
| 57 | + f"({branch_type} branch). Found {len(logs)}." |
81 | 58 | ) |
82 | 59 | raise ValueError(msg) |
| 60 | + elif len(logs) == 0: |
| 61 | + inpt_erros = ['no_logs_found'] |
| 62 | + return inpt_erros |
83 | 63 |
|
84 | 64 | with logs[0].open() as file: |
85 | 65 | log = file.read() |
@@ -150,7 +130,7 @@ def create_details_block(summary: str, content: str) -> str: |
150 | 130 | return "" |
151 | 131 |
|
152 | 132 |
|
153 | | -class RunSuccessfull(CommentData): |
| 133 | +class RunSuccessfullComponent(CommentData): |
154 | 134 | """Class to generate successfull run component.""" |
155 | 135 |
|
156 | 136 | def __init__(self): |
@@ -315,20 +295,22 @@ def _format_csvs_dir_files(self, df: pd.DataFrame) -> pd.DataFrame: |
315 | 295 | # Set header |
316 | 296 | df.columns = df.iloc[header_row_index] |
317 | 297 | # Fill nan values in header |
318 | | - df.columns = [ |
319 | | - f"col{i+1}" if pd.isna(col) or col == "" or col is None else col |
320 | | - for i, col in enumerate(df.columns) |
321 | | - ] |
| 298 | + df.columns = pd.Index( |
| 299 | + [ |
| 300 | + f"col{i+1}" if pd.isna(col) or col == "" or col is None else col |
| 301 | + for i, col in enumerate(df.columns) |
| 302 | + ] |
| 303 | + ) |
322 | 304 | # Remove all rows before header |
323 | 305 | df = df.iloc[header_row_index + 1 :] |
324 | 306 |
|
325 | 307 | # Make non-numeric values the index |
326 | 308 | non_numeric = df.apply( |
327 | | - lambda col: pd.to_numeric(col, errors="coerce").isna().all() |
| 309 | + lambda col: pd.to_numeric(col, errors="coerce").isna().all() # type: ignore |
328 | 310 | ) |
329 | 311 |
|
330 | 312 | if non_numeric.any(): |
331 | | - df = df.set_index(df.columns[non_numeric].to_list()) |
| 313 | + df = df.set_index(df.columns[non_numeric].to_list()) # type: ignore |
332 | 314 | else: |
333 | 315 | df = df.set_index("planning_horizon") |
334 | 316 |
|
@@ -507,7 +489,7 @@ def __call__(self) -> str: |
507 | 489 | return self.body |
508 | 490 |
|
509 | 491 |
|
510 | | -class RunFailed(CommentData): |
| 492 | +class RunFailedComponent(CommentData): |
511 | 493 | """Class to generate failed run component.""" |
512 | 494 |
|
513 | 495 | def __init__(self): |
@@ -545,7 +527,7 @@ def __call__(self) -> str: |
545 | 527 | return self.body() |
546 | 528 |
|
547 | 529 |
|
548 | | -class ModelMetrics(CommentData): |
| 530 | +class ModelMetricsComponent(CommentData): |
549 | 531 | """Class to generate model metrics component.""" |
550 | 532 |
|
551 | 533 | def __init__(self): |
@@ -575,140 +557,3 @@ def body(self) -> str: |
575 | 557 | def __call__(self) -> str: |
576 | 558 | """Return text for model metrics component.""" |
577 | 559 | return self.body() |
578 | | - |
579 | | - |
580 | | -class Comment(CommentData): |
581 | | - """Class to generate pypsa validator comment for GitHub PRs.""" |
582 | | - |
583 | | - def __init__(self) -> None: |
584 | | - """Initialize comment class. It will put all text components together.""" |
585 | | - super().__init__() |
586 | | - |
587 | | - @property |
588 | | - def header(self) -> str: |
589 | | - """ |
590 | | - Header text. |
591 | | -
|
592 | | - Contains the title, identifier, and short description. |
593 | | - """ |
594 | | - return ( |
595 | | - f"" |
596 | | - f"<!-- _val-bot-id-keyword_ -->\n" |
597 | | - f"## Validator Report\n" |
598 | | - f"I am the Validator. Download all artifacts [here](https://github.com/" |
599 | | - f"{self.github_repository}/actions/runs/{self.github_run_id}).\n" |
600 | | - f"I'll be back and edit this comment for each new commit.\n\n" |
601 | | - ) |
602 | | - |
603 | | - @property |
604 | | - def config_diff(self) -> str: |
605 | | - """ |
606 | | - Config diff text. |
607 | | -
|
608 | | - Only use when there are changes in the config. |
609 | | - """ |
610 | | - return ( |
611 | | - f"<details>\n" |
612 | | - f" <summary>:warning: Config changes detected!</summary>\n" |
613 | | - f"\n" |
614 | | - f"Results may differ due to these changes:\n" |
615 | | - f"```diff\n" |
616 | | - f"{self.git_diff_config}\n" |
617 | | - f"```\n" |
618 | | - f"</details>\n\n" |
619 | | - ) |
620 | | - |
621 | | - @property |
622 | | - def subtext(self) -> str: |
623 | | - """Subtext for the comment.""" |
624 | | - if self.hash_feature: |
625 | | - hash_feature = ( |
626 | | - f"([{self.hash_feature[:7]}](https://github.com/" |
627 | | - f"{self.github_repository}/commits/{self.hash_feature})) " |
628 | | - ) |
629 | | - if self.hash_main: |
630 | | - hash_main = ( |
631 | | - f"([{self.hash_main[:7]}](https://github.com/" |
632 | | - f"{self.github_repository}/commits/{self.hash_main}))" |
633 | | - ) |
634 | | - time = ( |
635 | | - pd.Timestamp.now() |
636 | | - .tz_localize("UTC") |
637 | | - .tz_convert("Europe/Berlin") |
638 | | - .strftime("%Y-%m-%d %H:%M:%S %Z") |
639 | | - ) |
640 | | - return ( |
641 | | - f"Comparing `{self.github_head_ref}` {hash_feature}with " |
642 | | - f"`{self.github_base_ref}` {hash_main}.\n" |
643 | | - f"Branch is {self.ahead_count} commits ahead and {self.behind_count} " |
644 | | - f"commits behind.\n" |
645 | | - f"Last updated on `{time}`." |
646 | | - ) |
647 | | - |
648 | | - def dynamic_plots(self) -> str: |
649 | | - """ |
650 | | - Return a list of dynamic results plots needed for the comment. |
651 | | -
|
652 | | - Returns |
653 | | - ------- |
654 | | - str: Space separated list of dynamic plots. |
655 | | -
|
656 | | - """ |
657 | | - if self.sucessfull_run: |
658 | | - body_sucessfull = RunSuccessfull() |
659 | | - plots_string = " ".join(body_sucessfull.variables_plot_strings) |
660 | | - return plots_string |
661 | | - else: |
662 | | - "" |
663 | | - |
664 | | - def __repr__(self) -> str: |
665 | | - """Return full formatted comment.""" |
666 | | - body_benchmarks = ModelMetrics() |
667 | | - if self.sucessfull_run: |
668 | | - body_sucessfull = RunSuccessfull() |
669 | | - |
670 | | - return ( |
671 | | - f"{self.header}" |
672 | | - f"{self.config_diff if self.git_diff_config else ''}" |
673 | | - f"{body_sucessfull()}" |
674 | | - f"{body_benchmarks()}" |
675 | | - f"{self.subtext}" |
676 | | - ) |
677 | | - |
678 | | - else: |
679 | | - body_failed = RunFailed() |
680 | | - |
681 | | - return ( |
682 | | - f"{self.header}" |
683 | | - f"{body_failed()}" |
684 | | - f"{self.config_diff if self.git_diff_config else ''}" |
685 | | - f"{body_benchmarks()}" |
686 | | - f"{self.subtext}" |
687 | | - ) |
688 | | - |
689 | | - |
690 | | -def main(): |
691 | | - """ |
692 | | - Run draft comment script. |
693 | | -
|
694 | | - Command line interface for the draft comment script. Use no arguments to print the |
695 | | - comment, or use the "plots" argument to print the dynamic plots which will be needed |
696 | | - for the comment. |
697 | | - """ |
698 | | - parser = argparse.ArgumentParser(description="Process some comments.") |
699 | | - parser.add_argument( |
700 | | - "command", nargs="?", default="", help='Command to run, e.g., "plots".' |
701 | | - ) |
702 | | - args = parser.parse_args() |
703 | | - |
704 | | - comment = Comment() |
705 | | - |
706 | | - if args.command == "plots": |
707 | | - print(comment.dynamic_plots()) |
708 | | - |
709 | | - else: |
710 | | - print(comment) # noqa T201 |
711 | | - |
712 | | - |
713 | | -if __name__ == "__main__": |
714 | | - main() |
0 commit comments