| 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) |
|
|