"""Base class for flagging checks.""" from functools import partial from typing import Callable, Collection, Self from pandas import DataFrame as Df from .... import logger from ....constants import Flags from ....logic.audit.logic.common import OutputColumns, SFCols from ....typings import Dataclass from ....utils.html import bold from ..logic import common from . import conditions logger = logger.new_logger(__name__) class _FlagChecks: """Base class for flagging checks. Must be subclassed to implement specific checks for each audit type. """ def __init__(self, df: Df, flagger: Callable) -> None: self.df = df self.flagger = flagger self.add_flag = partial( common.left_join, left_df=df, on=[SFCols.UPC, SFCols.ISRC], ) self._condition_has_asset_id = ( conditions.has_asset_id(self.df) if SFCols.ASSET_ID in df.columns else None ) self._condition_has_isrc = ( conditions.has_isrc(self.df) if SFCols.ISRC in df.columns else None ) def no_asset_id(self) -> Self: """Flag in place rows with no asset ID.""" df = self.df df[OutputColumns.AUDIT_FLAG_NO_ASSET_ID] = ~self._condition_has_asset_id.astype( bool ) def details(row): lock_reason = row.get(SFCols.LOCK_REASON) if isinstance(lock_reason, str) and lock_reason.strip(): bold_text = bold("Locked in MRR") text = f"{bold_text} (reason: {lock_reason})" return text flag_rows = df[df[OutputColumns.AUDIT_FLAG_NO_ASSET_ID]] if len(flag_rows): common.add_flag_db( flag_rows, self.flagger, Flags.NO_ASSET_ID, details=details ) return self class _FlagCleanUp: """Base class for cleaning up flagged rows. Cleanup means performing operations on the applied flags to remove flags which e.g. are incompatible with each other. This class leverages Pandas to perform the cleanup operations, as directly performing operations on a list of dictionaries would be less performant. """ def __init__(self, flags: list[dict | object], flag_col: str = "text") -> None: """Initialize the cleanup class with a list of flags. Args: flags: list of flags to clean up. Can be provided EITHER as a list of dictionaries or a list of dataclasses. flag_col (str): The key in the dictionary or dataclass where the flag is stored. Flag in this sense is a string, e.g. "NO_ASSET_ID". """ self._df = Df(flags) self._flags = flags self._flag_col = flag_col @property def flags(self) -> list[dict | Dataclass]: """Returns the cleaned list of flags in the original order. Optimized to minimize overhead when filtering by index. """ flags = [self._flags[i] for i in self._df.index.values] return flags def _only_one_flag( self, col: SFCols, *, flag: Flags, discard_flags: Collection[Flags] ) -> None: """For each unique value in `column`, all rows with that value will be considered a flag group. If `flag` is present in `flag_col` for any row in a group, all the rows with a flag included in `discard_flags` will be discarded. Example use case: - If an Asset ID has a CAN REACTIVATE flag, it should not have any of those flags: NO ACTIVE REFERENCES REASONS ORCHARD, NO ACTIVE REFERENCES REASONS THIRD PARTY. Args: col: The column to group by. flag: The flag to check for. discard_flags: The flags to discard. """ if not discard_flags: raise ValueError("Flags to discard cannot be empty.") if self._df.empty: return None flag_col = self._flag_col relevant_groups = self._df[self._df[flag_col] == flag][col].unique() discard_mask = (self._df[col].isin(relevant_groups)) & ( self._df[flag_col].isin(discard_flags) ) self._df = self._df[~discard_mask].reset_index(drop=True) logger.debug( f"Flag cleanup: Only one flag. Column: {col}, Flag column: {flag_col}," f" Flag: {flag}, Discard flags: {discard_flags}. Number of rows " f"removed: {discard_mask.sum()}." )