from __future__ import annotations from abc import ABC, abstractmethod import collections.abc import json from pathlib import Path import typing from typing import * class Challenge[P, S, V](ABC): @abstractmethod def generate(self, seed: int) -> tuple[P, V]: raise NotImplementedError() @abstractmethod def verify(self, problem: P, solution: S, secrets: V) -> bool: raise NotImplementedError() class Solver[P, S](ABC): @abstractmethod def solve(self, problem: P) -> S: raise NotImplementedError() def _is_callable_hint(ty: Any) -> bool: return typing.get_origin(ty) is collections.abc.Callable or ty is collections.abc.Callable def _is_serde(ty: type) -> bool: return isinstance(ty, type) and issubclass(ty, Serde) and (ty is not Serde) def _convert_from(val: Any, ty: Any) -> Any: if ty is Any: return val origin = typing.get_origin(ty) args = typing.get_args(ty) if origin is Union: if val is None and type(None) in args: return None for variant in args: if variant is type(None): continue try: return _convert_from(val, variant) except (TypeError, KeyError): continue raise TypeError(f'value {val!r} does not match any variant of {ty}') if origin is list: if not isinstance(val, list): raise TypeError(f'expected list, got {type(val)}') if args: return [_convert_from(item, args[0]) for item in val] return val if origin is dict: if not isinstance(val, dict): raise TypeError(f'expected dict, got {type(val)}') if len(args) == 2: return {_convert_from(k, args[0]): _convert_from(v, args[1]) for k, v in val.items()} return val if origin is tuple: if not isinstance(val, (list, tuple)): raise TypeError(f'expected tuple, got {type(val)}') if args: return tuple((_convert_from(item, a) for item, a in zip(val, args))) return tuple(val) if _is_serde(ty): if isinstance(val, ty): return val if isinstance(val, dict): return ty.from_dict(val) raise TypeError(f'expected dict or {ty.__name__}, got {type(val)}') if isinstance(ty, type) and (not isinstance(val, ty)): raise TypeError(f'expected type `{ty.__name__}` but got `{type(val).__name__}`') return val def _convert_to(val: Any) -> Any: if isinstance(val, Serde): return val.to_dict() if isinstance(val, list): return [_convert_to(item) for item in val] if isinstance(val, dict): return {k: _convert_to(v) for k, v in val.items()} if isinstance(val, tuple): return [_convert_to(item) for item in val] return val class Serde: @classmethod def from_dict(cls, data: dict[str, Any]) -> Self: hints = typing.get_type_hints(cls) defaults = {key: val for key in dir(cls) if not key.startswith('__') and (not isinstance((val := getattr(cls, key)), Callable))} args = list() for key, ty in hints.items(): if _is_callable_hint(ty): continue if key in data: val = data[key] elif key in defaults: val = defaults[key] else: raise KeyError(f"missing expected key '{key}'") args.append(_convert_from(val, ty)) return cls(*args) def to_dict(self) -> dict[str, Any]: hints = typing.get_type_hints(type(self)) result = {} for key, ty in hints.items(): if _is_callable_hint(ty): continue result[key] = _convert_to(getattr(self, key)) return result @classmethod def from_json(cls, json_str: str) -> Self: return cls.from_dict(json.loads(json_str)) def to_json(self) -> str: return json.dumps(self.to_dict()) @classmethod def from_json_file(cls, json_file: Path) -> Self: with json_file.open('r') as infile: return cls.from_dict(json.load(infile)) def to_json_file(self, out: Path) -> None: with out.open('w') as outfile: json.dump(self.to_dict(), outfile)