-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
general: add 'destructive parsing' (kinda what we were doing in my.co…
…re.konsume) to my.experimental also some cleanup for my.codeforces and my.topcoder
- Loading branch information
Showing
4 changed files
with
192 additions
and
106 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,86 +1,80 @@ | ||
from my.config import codeforces as config # type: ignore[attr-defined] | ||
|
||
|
||
from dataclasses import dataclass | ||
from datetime import datetime, timezone | ||
from functools import cached_property | ||
import json | ||
from typing import NamedTuple, Dict, Iterator | ||
|
||
|
||
from my.core import get_files, Res | ||
from my.core.konsume import ignore, wrap | ||
from pathlib import Path | ||
from typing import Dict, Iterator, Sequence | ||
|
||
from my.core import get_files, Res, datetime_aware | ||
from my.core.common import assert_never | ||
|
||
Cid = int | ||
|
||
class Contest(NamedTuple): | ||
cid: Cid | ||
when: datetime | ||
from my.config import codeforces as config # type: ignore[attr-defined] | ||
|
||
@classmethod | ||
def make(cls, j) -> 'Contest': | ||
return cls( | ||
cid=j['id'], | ||
when=datetime.fromtimestamp(j['startTimeSeconds'], tz=timezone.utc), | ||
) | ||
|
||
Cmap = Dict[Cid, Contest] | ||
def inputs() -> Sequence[Path]: | ||
return get_files(config.export_path) | ||
|
||
|
||
def get_contests() -> Cmap: | ||
last = max(get_files(config.export_path, 'allcontests*.json')) | ||
j = json.loads(last.read_text()) | ||
d = {} | ||
for c in j['result']: | ||
cc = Contest.make(c) | ||
d[cc.cid] = cc | ||
return d | ||
ContestId = int | ||
|
||
|
||
class Competition(NamedTuple): | ||
contest_id: Cid | ||
contest: str | ||
cmap: Cmap | ||
@dataclass | ||
class Contest: | ||
contest_id: ContestId | ||
when: datetime_aware | ||
name: str | ||
|
||
@cached_property | ||
def uid(self) -> Cid: | ||
return self.contest_id | ||
|
||
def __hash__(self): | ||
return hash(self.contest_id) | ||
|
||
@cached_property | ||
def when(self) -> datetime: | ||
return self.cmap[self.uid].when | ||
@dataclass | ||
class Competition: | ||
contest: Contest | ||
old_rating: int | ||
new_rating: int | ||
|
||
@cached_property | ||
def summary(self) -> str: | ||
return f'participated in {self.contest}' # TODO | ||
|
||
@classmethod | ||
def make(cls, cmap, json) -> Iterator[Res['Competition']]: | ||
# TODO try here?? | ||
contest_id = json['contestId'].zoom().value | ||
contest = json['contestName'].zoom().value | ||
yield cls( | ||
contest_id=contest_id, | ||
contest=contest, | ||
cmap=cmap, | ||
) | ||
# TODO ytry??? | ||
ignore(json, 'rank', 'oldRating', 'newRating') | ||
def when(self) -> datetime_aware: | ||
return self.contest.when | ||
|
||
|
||
# todo not sure if parser is the best name? hmm | ||
class Parser: | ||
def __init__(self, *, inputs: Sequence[Path]) -> None: | ||
self.inputs = inputs | ||
self.contests: Dict[ContestId, Contest] = {} | ||
|
||
def _parse_allcontests(self, p: Path) -> Iterator[Contest]: | ||
j = json.loads(p.read_text()) | ||
for c in j['result']: | ||
yield Contest( | ||
contest_id=c['id'], | ||
when=datetime.fromtimestamp(c['startTimeSeconds'], tz=timezone.utc), | ||
name=c['name'], | ||
) | ||
|
||
def _parse_competitions(self, p: Path) -> Iterator[Competition]: | ||
j = json.loads(p.read_text()) | ||
for c in j['result']: | ||
contest_id = c['contestId'] | ||
contest = self.contests[contest_id] | ||
yield Competition( | ||
contest=contest, | ||
old_rating=c['oldRating'], | ||
new_rating=c['newRating'], | ||
) | ||
|
||
def parse(self) -> Iterator[Res[Competition]]: | ||
for path in inputs(): | ||
if 'allcontests' in path.name: | ||
# these contain information about all CF contests along with useful metadata | ||
for contest in self._parse_allcontests(path): | ||
# TODO some method to assert on mismatch if it exists? not sure | ||
self.contests[contest.contest_id] = contest | ||
elif 'codeforces' in path.name: | ||
# these contain only contests the user participated in | ||
yield from self._parse_competitions(path) | ||
else: | ||
raise RuntimeError("shouldn't happen") # TODO switch to compat.assert_never | ||
|
||
|
||
def data() -> Iterator[Res[Competition]]: | ||
cmap = get_contests() | ||
last = max(get_files(config.export_path, 'codeforces*.json')) | ||
|
||
with wrap(json.loads(last.read_text())) as j: | ||
j['status'].ignore() # type: ignore[index] | ||
res = j['result'].zoom() # type: ignore[index] | ||
|
||
for c in list(res): # TODO maybe we want 'iter' method?? | ||
ignore(c, 'handle', 'ratingUpdateTimeSeconds') | ||
yield from Competition.make(cmap=cmap, json=c) | ||
c.consume() | ||
# TODO maybe if they are all empty, no need to consume?? | ||
return Parser(inputs=inputs()).parse() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
from dataclasses import dataclass | ||
from typing import Any, Iterator, List, Tuple | ||
|
||
from my.core import assert_never | ||
from my.core.compat import NoneType | ||
|
||
|
||
# TODO Popper? not sure | ||
@dataclass | ||
class Helper: | ||
manager: 'Manager' | ||
item: Any # todo realistically, list or dict? could at least type as indexable or something | ||
path: Tuple[str, ...] | ||
|
||
def pop_if_primitive(self, *keys: str) -> None: | ||
""" | ||
The idea that primitive TODO | ||
""" | ||
item = self.item | ||
for k in keys: | ||
v = item[k] | ||
if isinstance(v, (str, bool, float, int, NoneType)): | ||
item.pop(k) # todo kinda unfortunate to get dict item twice.. but not sure if can avoid? | ||
|
||
def check(self, key: str, expected: Any) -> None: | ||
actual = self.item.pop(key) | ||
assert actual == expected, (key, actual, expected) | ||
|
||
def zoom(self, key: str) -> 'Helper': | ||
return self.manager.helper(item=self.item.pop(key), path=self.path + (key,)) | ||
|
||
|
||
def is_empty(x) -> bool: | ||
if isinstance(x, dict): | ||
return len(x) == 0 | ||
elif isinstance(x, list): | ||
return all(map(is_empty, x)) | ||
else: | ||
assert_never(x) | ||
|
||
|
||
class Manager: | ||
def __init__(self) -> None: | ||
self.helpers: List[Helper] = [] | ||
|
||
def helper(self, item: Any, *, path: Tuple[str, ...] = ()) -> Helper: | ||
res = Helper(manager=self, item=item, path=path) | ||
self.helpers.append(res) | ||
return res | ||
|
||
def check(self) -> Iterator[Exception]: | ||
remaining = [] | ||
for h in self.helpers: | ||
# TODO recursively check it's primitive? | ||
if is_empty(h.item): | ||
continue | ||
remaining.append((h.path, h.item)) | ||
if len(remaining) == 0: | ||
return | ||
yield RuntimeError(f'Unparsed items remaining: {remaining}') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters