Skip to content

Commit

Permalink
refactor: improve structure and adds custom no logs error (#21)
Browse files Browse the repository at this point in the history
* refactor: improve structure

* fix

* fix

* fix: dont fail for missing logs
  • Loading branch information
lkstrp authored Sep 16, 2024
1 parent 4a3d2dd commit 240ed1f
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 190 deletions.
6 changes: 3 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ runs:
# Get static plot list (from input)
read -a plots_array_input <<< ${{ inputs.plots }}
# Get dynamic plot list (from comment script)
read -a plots_array_dynamic <<< "$(python scripts/draft_comment.py plots)"
read -a plots_array_dynamic <<< "$(python src/draft_comment.py plots)"
plots_array=("${plots_array_input[@]}" "${plots_array_dynamic[@]}")
Expand All @@ -315,7 +315,7 @@ runs:
done
# Get benchmark plot list (from benchmark script)
read -a plots_array_benchmark <<< "$(python scripts/plot_benchmarks.py)"
read -a plots_array_benchmark <<< "$(python src/plot_benchmarks.py)"
mkdir -p _validation-images/benchmarks
Expand Down Expand Up @@ -356,7 +356,7 @@ runs:
# Create comment
# Note: The script uses many env variables. See the script for more details.
python scripts/draft_comment.py > $GITHUB_WORKSPACE/comment.txt
python src/draft_comment.py > $GITHUB_WORKSPACE/comment.txt
cat $GITHUB_WORKSPACE/comment.txt >> $GITHUB_STEP_SUMMARY
Expand Down
193 changes: 19 additions & 174 deletions scripts/draft_comment.py → src/comment_components.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,17 @@
"""
Draft comment for pypsa-validator GitHub PRs.
"""Text components to generate validator report."""

Script can be called via command line or imported as a module.
"""

import argparse
import os
import re
from dataclasses import dataclass
from pathlib import Path

import numpy as np
import pandas as pd

from metrics import min_max_normalized_mae, normalized_root_mean_square_error
from numpy.typing import ArrayLike
from utils import get_env_var


def create_numeric_mask(arr: ArrayLike) -> np.ndarray:
"""
Create a mask where True indicates numeric and finite values.
Parameters
----------
arr : array-like
Input array
Returns
-------
np.ndarray: Numeric mask where True indicates numeric and finite sort_values
"""
arr = np.array(arr)
return np.vectorize(lambda x: isinstance(x, (int, float)) and np.isfinite(x))(arr)


@dataclass
class CommentData:
"""Class to store data for comment generation."""
Expand Down Expand Up @@ -74,12 +51,15 @@ def errors(self, branch_type: str) -> list:
logs = list(
Path(f"{self.dir_artifacts}/logs/{branch_type}/.snakemake/log/").glob("*")
)
if len(logs) != 1:
if len(logs) > 1:
msg = (
"Expected exactly one log file in snakemake/log directory "
"({branch_type} branch)."
"Expected exactly one log fiie in snakemake/log directory "
f"({branch_type} branch). Found {len(logs)}."
)
raise ValueError(msg)
elif len(logs) == 0:
inpt_erros = ['no_logs_found']
return inpt_erros

with logs[0].open() as file:
log = file.read()
Expand Down Expand Up @@ -150,7 +130,7 @@ def create_details_block(summary: str, content: str) -> str:
return ""


class RunSuccessfull(CommentData):
class RunSuccessfullComponent(CommentData):
"""Class to generate successfull run component."""

def __init__(self):
Expand Down Expand Up @@ -315,20 +295,22 @@ def _format_csvs_dir_files(self, df: pd.DataFrame) -> pd.DataFrame:
# Set header
df.columns = df.iloc[header_row_index]
# Fill nan values in header
df.columns = [
f"col{i+1}" if pd.isna(col) or col == "" or col is None else col
for i, col in enumerate(df.columns)
]
df.columns = pd.Index(
[
f"col{i+1}" if pd.isna(col) or col == "" or col is None else col
for i, col in enumerate(df.columns)
]
)
# Remove all rows before header
df = df.iloc[header_row_index + 1 :]

# Make non-numeric values the index
non_numeric = df.apply(
lambda col: pd.to_numeric(col, errors="coerce").isna().all()
lambda col: pd.to_numeric(col, errors="coerce").isna().all() # type: ignore
)

if non_numeric.any():
df = df.set_index(df.columns[non_numeric].to_list())
df = df.set_index(df.columns[non_numeric].to_list()) # type: ignore
else:
df = df.set_index("planning_horizon")

Expand Down Expand Up @@ -507,7 +489,7 @@ def __call__(self) -> str:
return self.body


class RunFailed(CommentData):
class RunFailedComponent(CommentData):
"""Class to generate failed run component."""

def __init__(self):
Expand Down Expand Up @@ -545,7 +527,7 @@ def __call__(self) -> str:
return self.body()


class ModelMetrics(CommentData):
class ModelMetricsComponent(CommentData):
"""Class to generate model metrics component."""

def __init__(self):
Expand Down Expand Up @@ -575,140 +557,3 @@ def body(self) -> str:
def __call__(self) -> str:
"""Return text for model metrics component."""
return self.body()


class Comment(CommentData):
"""Class to generate pypsa validator comment for GitHub PRs."""

def __init__(self) -> None:
"""Initialize comment class. It will put all text components together."""
super().__init__()

@property
def header(self) -> str:
"""
Header text.
Contains the title, identifier, and short description.
"""
return (
f""
f"<!-- _val-bot-id-keyword_ -->\n"
f"## Validator Report\n"
f"I am the Validator. Download all artifacts [here](https://github.com/"
f"{self.github_repository}/actions/runs/{self.github_run_id}).\n"
f"I'll be back and edit this comment for each new commit.\n\n"
)

@property
def config_diff(self) -> str:
"""
Config diff text.
Only use when there are changes in the config.
"""
return (
f"<details>\n"
f" <summary>:warning: Config changes detected!</summary>\n"
f"\n"
f"Results may differ due to these changes:\n"
f"```diff\n"
f"{self.git_diff_config}\n"
f"```\n"
f"</details>\n\n"
)

@property
def subtext(self) -> str:
"""Subtext for the comment."""
if self.hash_feature:
hash_feature = (
f"([{self.hash_feature[:7]}](https://github.com/"
f"{self.github_repository}/commits/{self.hash_feature})) "
)
if self.hash_main:
hash_main = (
f"([{self.hash_main[:7]}](https://github.com/"
f"{self.github_repository}/commits/{self.hash_main}))"
)
time = (
pd.Timestamp.now()
.tz_localize("UTC")
.tz_convert("Europe/Berlin")
.strftime("%Y-%m-%d %H:%M:%S %Z")
)
return (
f"Comparing `{self.github_head_ref}` {hash_feature}with "
f"`{self.github_base_ref}` {hash_main}.\n"
f"Branch is {self.ahead_count} commits ahead and {self.behind_count} "
f"commits behind.\n"
f"Last updated on `{time}`."
)

def dynamic_plots(self) -> str:
"""
Return a list of dynamic results plots needed for the comment.
Returns
-------
str: Space separated list of dynamic plots.
"""
if self.sucessfull_run:
body_sucessfull = RunSuccessfull()
plots_string = " ".join(body_sucessfull.variables_plot_strings)
return plots_string
else:
""

def __repr__(self) -> str:
"""Return full formatted comment."""
body_benchmarks = ModelMetrics()
if self.sucessfull_run:
body_sucessfull = RunSuccessfull()

return (
f"{self.header}"
f"{self.config_diff if self.git_diff_config else ''}"
f"{body_sucessfull()}"
f"{body_benchmarks()}"
f"{self.subtext}"
)

else:
body_failed = RunFailed()

return (
f"{self.header}"
f"{body_failed()}"
f"{self.config_diff if self.git_diff_config else ''}"
f"{body_benchmarks()}"
f"{self.subtext}"
)


def main():
"""
Run draft comment script.
Command line interface for the draft comment script. Use no arguments to print the
comment, or use the "plots" argument to print the dynamic plots which will be needed
for the comment.
"""
parser = argparse.ArgumentParser(description="Process some comments.")
parser.add_argument(
"command", nargs="?", default="", help='Command to run, e.g., "plots".'
)
args = parser.parse_args()

comment = Comment()

if args.command == "plots":
print(comment.dynamic_plots())

else:
print(comment) # noqa T201


if __name__ == "__main__":
main()
Loading

0 comments on commit 240ed1f

Please sign in to comment.