1*61046927SAndroid Build Coastguard Worker""" 2*61046927SAndroid Build Coastguard WorkerA structured logging utility supporting multiple data formats such as CSV, JSON, 3*61046927SAndroid Build Coastguard Workerand YAML. 4*61046927SAndroid Build Coastguard Worker 5*61046927SAndroid Build Coastguard WorkerThe main purpose of this script, besides having relevant information available 6*61046927SAndroid Build Coastguard Workerin a condensed and deserialized. 7*61046927SAndroid Build Coastguard Worker 8*61046927SAndroid Build Coastguard WorkerThis script defines a protocol for different file handling strategies and provides 9*61046927SAndroid Build Coastguard Workerimplementations for CSV, JSON, and YAML formats. The main class, StructuredLogger, 10*61046927SAndroid Build Coastguard Workerallows for easy interaction with log data, enabling users to load, save, increment, 11*61046927SAndroid Build Coastguard Workerset, and append fields in the log. The script also includes context managers for 12*61046927SAndroid Build Coastguard Workerfile locking and editing log data to ensure data integrity and avoid race conditions. 13*61046927SAndroid Build Coastguard Worker""" 14*61046927SAndroid Build Coastguard Worker 15*61046927SAndroid Build Coastguard Workerimport json 16*61046927SAndroid Build Coastguard Workerimport os 17*61046927SAndroid Build Coastguard Workerfrom collections.abc import MutableMapping, MutableSequence 18*61046927SAndroid Build Coastguard Workerfrom contextlib import contextmanager 19*61046927SAndroid Build Coastguard Workerfrom datetime import datetime 20*61046927SAndroid Build Coastguard Workerfrom pathlib import Path 21*61046927SAndroid Build Coastguard Workerfrom typing import Any, Protocol 22*61046927SAndroid Build Coastguard Worker 23*61046927SAndroid Build Coastguard Workerimport fire 24*61046927SAndroid Build Coastguard Workerfrom filelock import FileLock 25*61046927SAndroid Build Coastguard Worker 26*61046927SAndroid Build Coastguard Workertry: 27*61046927SAndroid Build Coastguard Worker import polars as pl 28*61046927SAndroid Build Coastguard Worker 29*61046927SAndroid Build Coastguard Worker CSV_LIB_EXCEPTION = None 30*61046927SAndroid Build Coastguard Workerexcept ImportError as e: 31*61046927SAndroid Build Coastguard Worker CSV_LIB_EXCEPTION: ImportError = e 32*61046927SAndroid Build Coastguard Worker 33*61046927SAndroid Build Coastguard Workertry: 34*61046927SAndroid Build Coastguard Worker from ruamel.yaml import YAML 35*61046927SAndroid Build Coastguard Worker 36*61046927SAndroid Build Coastguard Worker YAML_LIB_EXCEPTION = None 37*61046927SAndroid Build Coastguard Workerexcept ImportError as e: 38*61046927SAndroid Build Coastguard Worker YAML_LIB_EXCEPTION: ImportError = e 39*61046927SAndroid Build Coastguard Worker 40*61046927SAndroid Build Coastguard Worker 41*61046927SAndroid Build Coastguard Workerclass ContainerProxy: 42*61046927SAndroid Build Coastguard Worker """ 43*61046927SAndroid Build Coastguard Worker A proxy class that wraps a mutable container object (such as a dictionary or 44*61046927SAndroid Build Coastguard Worker a list) and calls a provided save_callback function whenever the container 45*61046927SAndroid Build Coastguard Worker or its contents 46*61046927SAndroid Build Coastguard Worker are changed. 47*61046927SAndroid Build Coastguard Worker """ 48*61046927SAndroid Build Coastguard Worker def __init__(self, container, save_callback): 49*61046927SAndroid Build Coastguard Worker self.container = container 50*61046927SAndroid Build Coastguard Worker self.save_callback = save_callback 51*61046927SAndroid Build Coastguard Worker 52*61046927SAndroid Build Coastguard Worker def __getitem__(self, key): 53*61046927SAndroid Build Coastguard Worker value = self.container[key] 54*61046927SAndroid Build Coastguard Worker if isinstance(value, (MutableMapping, MutableSequence)): 55*61046927SAndroid Build Coastguard Worker return ContainerProxy(value, self.save_callback) 56*61046927SAndroid Build Coastguard Worker return value 57*61046927SAndroid Build Coastguard Worker 58*61046927SAndroid Build Coastguard Worker def __setitem__(self, key, value): 59*61046927SAndroid Build Coastguard Worker self.container[key] = value 60*61046927SAndroid Build Coastguard Worker self.save_callback() 61*61046927SAndroid Build Coastguard Worker 62*61046927SAndroid Build Coastguard Worker def __delitem__(self, key): 63*61046927SAndroid Build Coastguard Worker del self.container[key] 64*61046927SAndroid Build Coastguard Worker self.save_callback() 65*61046927SAndroid Build Coastguard Worker 66*61046927SAndroid Build Coastguard Worker def __getattr__(self, name): 67*61046927SAndroid Build Coastguard Worker attr = getattr(self.container, name) 68*61046927SAndroid Build Coastguard Worker 69*61046927SAndroid Build Coastguard Worker if callable(attr): 70*61046927SAndroid Build Coastguard Worker def wrapper(*args, **kwargs): 71*61046927SAndroid Build Coastguard Worker result = attr(*args, **kwargs) 72*61046927SAndroid Build Coastguard Worker self.save_callback() 73*61046927SAndroid Build Coastguard Worker return result 74*61046927SAndroid Build Coastguard Worker 75*61046927SAndroid Build Coastguard Worker return wrapper 76*61046927SAndroid Build Coastguard Worker return attr 77*61046927SAndroid Build Coastguard Worker 78*61046927SAndroid Build Coastguard Worker def __iter__(self): 79*61046927SAndroid Build Coastguard Worker return iter(self.container) 80*61046927SAndroid Build Coastguard Worker 81*61046927SAndroid Build Coastguard Worker def __len__(self): 82*61046927SAndroid Build Coastguard Worker return len(self.container) 83*61046927SAndroid Build Coastguard Worker 84*61046927SAndroid Build Coastguard Worker def __repr__(self): 85*61046927SAndroid Build Coastguard Worker return repr(self.container) 86*61046927SAndroid Build Coastguard Worker 87*61046927SAndroid Build Coastguard Worker 88*61046927SAndroid Build Coastguard Workerclass AutoSaveDict(dict): 89*61046927SAndroid Build Coastguard Worker """ 90*61046927SAndroid Build Coastguard Worker A subclass of the built-in dict class with additional functionality to 91*61046927SAndroid Build Coastguard Worker automatically save changes to the dictionary. It maintains a timestamp of 92*61046927SAndroid Build Coastguard Worker the last modification and automatically wraps nested mutable containers 93*61046927SAndroid Build Coastguard Worker using ContainerProxy. 94*61046927SAndroid Build Coastguard Worker """ 95*61046927SAndroid Build Coastguard Worker timestamp_key = "_timestamp" 96*61046927SAndroid Build Coastguard Worker 97*61046927SAndroid Build Coastguard Worker def __init__(self, *args, save_callback, register_timestamp=True, **kwargs): 98*61046927SAndroid Build Coastguard Worker self.save_callback = save_callback 99*61046927SAndroid Build Coastguard Worker self.__register_timestamp = register_timestamp 100*61046927SAndroid Build Coastguard Worker self.__heartbeat() 101*61046927SAndroid Build Coastguard Worker super().__init__(*args, **kwargs) 102*61046927SAndroid Build Coastguard Worker self.__wrap_dictionaries() 103*61046927SAndroid Build Coastguard Worker 104*61046927SAndroid Build Coastguard Worker def __heartbeat(self): 105*61046927SAndroid Build Coastguard Worker if self.__register_timestamp: 106*61046927SAndroid Build Coastguard Worker self[AutoSaveDict.timestamp_key] = datetime.now().isoformat() 107*61046927SAndroid Build Coastguard Worker 108*61046927SAndroid Build Coastguard Worker def __save(self): 109*61046927SAndroid Build Coastguard Worker self.__heartbeat() 110*61046927SAndroid Build Coastguard Worker self.save_callback() 111*61046927SAndroid Build Coastguard Worker 112*61046927SAndroid Build Coastguard Worker def __wrap_dictionaries(self): 113*61046927SAndroid Build Coastguard Worker for key, value in self.items(): 114*61046927SAndroid Build Coastguard Worker if isinstance(value, MutableMapping) and not isinstance( 115*61046927SAndroid Build Coastguard Worker value, AutoSaveDict 116*61046927SAndroid Build Coastguard Worker ): 117*61046927SAndroid Build Coastguard Worker self[key] = AutoSaveDict( 118*61046927SAndroid Build Coastguard Worker value, save_callback=self.save_callback, register_timestamp=False 119*61046927SAndroid Build Coastguard Worker ) 120*61046927SAndroid Build Coastguard Worker 121*61046927SAndroid Build Coastguard Worker def __setitem__(self, key, value): 122*61046927SAndroid Build Coastguard Worker if isinstance(value, MutableMapping) and not isinstance(value, AutoSaveDict): 123*61046927SAndroid Build Coastguard Worker value = AutoSaveDict( 124*61046927SAndroid Build Coastguard Worker value, save_callback=self.save_callback, register_timestamp=False 125*61046927SAndroid Build Coastguard Worker ) 126*61046927SAndroid Build Coastguard Worker super().__setitem__(key, value) 127*61046927SAndroid Build Coastguard Worker 128*61046927SAndroid Build Coastguard Worker if self.__register_timestamp and key == AutoSaveDict.timestamp_key: 129*61046927SAndroid Build Coastguard Worker return 130*61046927SAndroid Build Coastguard Worker self.__save() 131*61046927SAndroid Build Coastguard Worker 132*61046927SAndroid Build Coastguard Worker def __getitem__(self, key): 133*61046927SAndroid Build Coastguard Worker value = super().__getitem__(key) 134*61046927SAndroid Build Coastguard Worker if isinstance(value, (MutableMapping, MutableSequence)): 135*61046927SAndroid Build Coastguard Worker return ContainerProxy(value, self.__save) 136*61046927SAndroid Build Coastguard Worker return value 137*61046927SAndroid Build Coastguard Worker 138*61046927SAndroid Build Coastguard Worker def __delitem__(self, key): 139*61046927SAndroid Build Coastguard Worker super().__delitem__(key) 140*61046927SAndroid Build Coastguard Worker self.__save() 141*61046927SAndroid Build Coastguard Worker 142*61046927SAndroid Build Coastguard Worker def pop(self, *args, **kwargs): 143*61046927SAndroid Build Coastguard Worker result = super().pop(*args, **kwargs) 144*61046927SAndroid Build Coastguard Worker self.__save() 145*61046927SAndroid Build Coastguard Worker return result 146*61046927SAndroid Build Coastguard Worker 147*61046927SAndroid Build Coastguard Worker def update(self, *args, **kwargs): 148*61046927SAndroid Build Coastguard Worker super().update(*args, **kwargs) 149*61046927SAndroid Build Coastguard Worker self.__wrap_dictionaries() 150*61046927SAndroid Build Coastguard Worker self.__save() 151*61046927SAndroid Build Coastguard Worker 152*61046927SAndroid Build Coastguard Worker 153*61046927SAndroid Build Coastguard Workerclass StructuredLoggerStrategy(Protocol): 154*61046927SAndroid Build Coastguard Worker def load_data(self, file_path: Path) -> dict: 155*61046927SAndroid Build Coastguard Worker pass 156*61046927SAndroid Build Coastguard Worker 157*61046927SAndroid Build Coastguard Worker def save_data(self, file_path: Path, data: dict) -> None: 158*61046927SAndroid Build Coastguard Worker pass 159*61046927SAndroid Build Coastguard Worker 160*61046927SAndroid Build Coastguard Worker 161*61046927SAndroid Build Coastguard Workerclass CSVStrategy: 162*61046927SAndroid Build Coastguard Worker def __init__(self) -> None: 163*61046927SAndroid Build Coastguard Worker if CSV_LIB_EXCEPTION: 164*61046927SAndroid Build Coastguard Worker raise RuntimeError( 165*61046927SAndroid Build Coastguard Worker "Can't parse CSV files. Missing library" 166*61046927SAndroid Build Coastguard Worker ) from CSV_LIB_EXCEPTION 167*61046927SAndroid Build Coastguard Worker 168*61046927SAndroid Build Coastguard Worker def load_data(self, file_path: Path) -> dict: 169*61046927SAndroid Build Coastguard Worker dicts: list[dict[str, Any]] = pl.read_csv( 170*61046927SAndroid Build Coastguard Worker file_path, try_parse_dates=True 171*61046927SAndroid Build Coastguard Worker ).to_dicts() 172*61046927SAndroid Build Coastguard Worker data = {} 173*61046927SAndroid Build Coastguard Worker for d in dicts: 174*61046927SAndroid Build Coastguard Worker for k, v in d.items(): 175*61046927SAndroid Build Coastguard Worker if k != AutoSaveDict.timestamp_key and k in data: 176*61046927SAndroid Build Coastguard Worker if isinstance(data[k], list): 177*61046927SAndroid Build Coastguard Worker data[k].append(v) 178*61046927SAndroid Build Coastguard Worker continue 179*61046927SAndroid Build Coastguard Worker data[k] = [data[k], v] 180*61046927SAndroid Build Coastguard Worker else: 181*61046927SAndroid Build Coastguard Worker data[k] = v 182*61046927SAndroid Build Coastguard Worker return data 183*61046927SAndroid Build Coastguard Worker 184*61046927SAndroid Build Coastguard Worker def save_data(self, file_path: Path, data: dict) -> None: 185*61046927SAndroid Build Coastguard Worker pl.DataFrame(data).write_csv(file_path) 186*61046927SAndroid Build Coastguard Worker 187*61046927SAndroid Build Coastguard Worker 188*61046927SAndroid Build Coastguard Workerclass JSONStrategy: 189*61046927SAndroid Build Coastguard Worker def load_data(self, file_path: Path) -> dict: 190*61046927SAndroid Build Coastguard Worker return json.loads(file_path.read_text()) 191*61046927SAndroid Build Coastguard Worker 192*61046927SAndroid Build Coastguard Worker def save_data(self, file_path: Path, data: dict) -> None: 193*61046927SAndroid Build Coastguard Worker with open(file_path, "w") as f: 194*61046927SAndroid Build Coastguard Worker json.dump(data, f, indent=2) 195*61046927SAndroid Build Coastguard Worker 196*61046927SAndroid Build Coastguard Worker 197*61046927SAndroid Build Coastguard Workerclass YAMLStrategy: 198*61046927SAndroid Build Coastguard Worker def __init__(self): 199*61046927SAndroid Build Coastguard Worker if YAML_LIB_EXCEPTION: 200*61046927SAndroid Build Coastguard Worker raise RuntimeError( 201*61046927SAndroid Build Coastguard Worker "Can't parse YAML files. Missing library" 202*61046927SAndroid Build Coastguard Worker ) from YAML_LIB_EXCEPTION 203*61046927SAndroid Build Coastguard Worker self.yaml = YAML() 204*61046927SAndroid Build Coastguard Worker self.yaml.indent(sequence=4, offset=2) 205*61046927SAndroid Build Coastguard Worker self.yaml.default_flow_style = False 206*61046927SAndroid Build Coastguard Worker self.yaml.representer.add_representer(AutoSaveDict, self.represent_dict) 207*61046927SAndroid Build Coastguard Worker 208*61046927SAndroid Build Coastguard Worker @classmethod 209*61046927SAndroid Build Coastguard Worker def represent_dict(cls, dumper, data): 210*61046927SAndroid Build Coastguard Worker return dumper.represent_mapping("tag:yaml.org,2002:map", data) 211*61046927SAndroid Build Coastguard Worker 212*61046927SAndroid Build Coastguard Worker def load_data(self, file_path: Path) -> dict: 213*61046927SAndroid Build Coastguard Worker return self.yaml.load(file_path.read_text()) 214*61046927SAndroid Build Coastguard Worker 215*61046927SAndroid Build Coastguard Worker def save_data(self, file_path: Path, data: dict) -> None: 216*61046927SAndroid Build Coastguard Worker with open(file_path, "w") as f: 217*61046927SAndroid Build Coastguard Worker self.yaml.dump(data, f) 218*61046927SAndroid Build Coastguard Worker 219*61046927SAndroid Build Coastguard Worker 220*61046927SAndroid Build Coastguard Workerclass StructuredLogger: 221*61046927SAndroid Build Coastguard Worker def __init__( 222*61046927SAndroid Build Coastguard Worker self, file_name: str, strategy: StructuredLoggerStrategy = None, truncate=False 223*61046927SAndroid Build Coastguard Worker ): 224*61046927SAndroid Build Coastguard Worker self.file_name: str = file_name 225*61046927SAndroid Build Coastguard Worker self.file_path = Path(self.file_name) 226*61046927SAndroid Build Coastguard Worker self._data: AutoSaveDict = AutoSaveDict(save_callback=self.save_data) 227*61046927SAndroid Build Coastguard Worker 228*61046927SAndroid Build Coastguard Worker if strategy is None: 229*61046927SAndroid Build Coastguard Worker self.strategy: StructuredLoggerStrategy = self.guess_strategy_from_file( 230*61046927SAndroid Build Coastguard Worker self.file_path 231*61046927SAndroid Build Coastguard Worker ) 232*61046927SAndroid Build Coastguard Worker else: 233*61046927SAndroid Build Coastguard Worker self.strategy = strategy 234*61046927SAndroid Build Coastguard Worker 235*61046927SAndroid Build Coastguard Worker if not self.file_path.exists(): 236*61046927SAndroid Build Coastguard Worker Path.mkdir(self.file_path.parent, exist_ok=True) 237*61046927SAndroid Build Coastguard Worker self.save_data() 238*61046927SAndroid Build Coastguard Worker return 239*61046927SAndroid Build Coastguard Worker 240*61046927SAndroid Build Coastguard Worker if truncate: 241*61046927SAndroid Build Coastguard Worker with self.get_lock(): 242*61046927SAndroid Build Coastguard Worker os.truncate(self.file_path, 0) 243*61046927SAndroid Build Coastguard Worker self.save_data() 244*61046927SAndroid Build Coastguard Worker 245*61046927SAndroid Build Coastguard Worker def load_data(self): 246*61046927SAndroid Build Coastguard Worker self._data = self.strategy.load_data(self.file_path) 247*61046927SAndroid Build Coastguard Worker 248*61046927SAndroid Build Coastguard Worker def save_data(self): 249*61046927SAndroid Build Coastguard Worker self.strategy.save_data(self.file_path, self._data) 250*61046927SAndroid Build Coastguard Worker 251*61046927SAndroid Build Coastguard Worker @property 252*61046927SAndroid Build Coastguard Worker def data(self) -> AutoSaveDict: 253*61046927SAndroid Build Coastguard Worker return self._data 254*61046927SAndroid Build Coastguard Worker 255*61046927SAndroid Build Coastguard Worker @contextmanager 256*61046927SAndroid Build Coastguard Worker def get_lock(self): 257*61046927SAndroid Build Coastguard Worker with FileLock(f"{self.file_path}.lock", timeout=10): 258*61046927SAndroid Build Coastguard Worker yield 259*61046927SAndroid Build Coastguard Worker 260*61046927SAndroid Build Coastguard Worker @contextmanager 261*61046927SAndroid Build Coastguard Worker def edit_context(self): 262*61046927SAndroid Build Coastguard Worker """ 263*61046927SAndroid Build Coastguard Worker Context manager that ensures proper loading and saving of log data when 264*61046927SAndroid Build Coastguard Worker performing multiple modifications. 265*61046927SAndroid Build Coastguard Worker """ 266*61046927SAndroid Build Coastguard Worker with self.get_lock(): 267*61046927SAndroid Build Coastguard Worker try: 268*61046927SAndroid Build Coastguard Worker self.load_data() 269*61046927SAndroid Build Coastguard Worker yield 270*61046927SAndroid Build Coastguard Worker finally: 271*61046927SAndroid Build Coastguard Worker self.save_data() 272*61046927SAndroid Build Coastguard Worker 273*61046927SAndroid Build Coastguard Worker @staticmethod 274*61046927SAndroid Build Coastguard Worker def guess_strategy_from_file(file_path: Path) -> StructuredLoggerStrategy: 275*61046927SAndroid Build Coastguard Worker file_extension = file_path.suffix.lower().lstrip(".") 276*61046927SAndroid Build Coastguard Worker return StructuredLogger.get_strategy(file_extension) 277*61046927SAndroid Build Coastguard Worker 278*61046927SAndroid Build Coastguard Worker @staticmethod 279*61046927SAndroid Build Coastguard Worker def get_strategy(strategy_name: str) -> StructuredLoggerStrategy: 280*61046927SAndroid Build Coastguard Worker strategies = { 281*61046927SAndroid Build Coastguard Worker "csv": CSVStrategy, 282*61046927SAndroid Build Coastguard Worker "json": JSONStrategy, 283*61046927SAndroid Build Coastguard Worker "yaml": YAMLStrategy, 284*61046927SAndroid Build Coastguard Worker "yml": YAMLStrategy, 285*61046927SAndroid Build Coastguard Worker } 286*61046927SAndroid Build Coastguard Worker 287*61046927SAndroid Build Coastguard Worker try: 288*61046927SAndroid Build Coastguard Worker return strategies[strategy_name]() 289*61046927SAndroid Build Coastguard Worker except KeyError as e: 290*61046927SAndroid Build Coastguard Worker raise ValueError(f"Unknown strategy for: {strategy_name}") from e 291*61046927SAndroid Build Coastguard Worker 292*61046927SAndroid Build Coastguard Worker 293*61046927SAndroid Build Coastguard Workerif __name__ == "__main__": 294*61046927SAndroid Build Coastguard Worker fire.Fire(StructuredLogger) 295