Add or fix a CLIST parser
Each judge is one module in src/ranking/management/modules/<host>.py exposing a
Statistic class that subclasses BaseModule. There are 85 existing modules —
always read 2–3 similar ones first and copy their conventions.
The contract
from ranking.management.modules.common import REQ, BaseModule
from ranking.management.modules.excepts import ExceptionParseStandings
class Statistic(BaseModule):
def get_standings(self, users=None, statistics=None, **kwargs):
# Contest metadata is on self: self.url, self.key, self.info,
# self.start_time, self.standings_url, self.resource, ...
data = REQ.get(self.standings_url, return_json=True) # or post=..., headers=...
if not_ok(data):
raise ExceptionParseStandings(data)
result = {}
for row_data in data['rows']:
member = str(row_data['user_id']) # stable unique handle
row = result.setdefault(member, {'member': member})
row['name'] = row_data['user_name']
row['solving'] = row_data['score'] # number of solved / score
row['place'] = row_data['rank']
row.setdefault('info', {})['avatar'] = row_data.get('avatar')
return {'result': result}
Return shape
get_standings returns a dict with at least result: a mapping of
member -> row. Common row keys:
member (required, str) — stable per-user handle/id.
place, solving, name.
problems — {short: {'result': '+', 'time': '01:23', ...}} per-problem.
info — extra per-row data (avatar, rating, country…).
Optional dict keys alongside result: problems (contest problem list),
url, options, hidden_fields, season.
Rules of the road
- Use
REQ (from ...common import REQ) for HTTP — never raw requests. It
handles cookies, proxies, retries. REQ.get(url, post=..., return_json=True, headers=...).
- Reuse helpers in
src/utils/: regex, timetools, and
clist.templatetags.extras.get_item for safe nested lookups.
- Raise
ExceptionParseStandings (from .excepts) on bad/unexpected responses,
not bare Exception.
- Keep it minimal and match the style of neighboring modules. Don't add new
dependencies.
- Optional methods only if needed:
get_users_infos(users, resource, accounts, pbar) (profile/rating data), get_source_code(contest, problem).
Good clean references to read: highload.py, algorithm_yandex.py,
codeforces_gym.py. For HTML scraping (not JSON) look at modules using
REQ.get(...) + parsing.
Test loop (safe, narrow, no junk data)
The dev container is already running. Re-parse a single contest read-only:
docker compose exec dev ./manage.py parse_statistic -r <host> -l 1 --no-update-results -s
-r <host> resource host (e.g. highload.io) -l 1 only one contest
--no-update-results don't write results -s stop on first exception
- Narrow further:
-e "<event regex>", -y <year>, -u <user>, --reparse
parse_statistic --help lists every flag.
Iterate until the standings parse cleanly, then drop --no-update-results for a
real run only if the task calls for it.
Wiring a brand-new judge
A parser file alone isn't enough for a new site: a Resource + its Module
(Module.path = ranking.management.modules.<host>) must exist in the DB. If the
resource already exists, just edit/add the .py. If it's a new resource, say so
and confirm how the resource row should be created before assuming.
When done, report
Module changed, how you tested (the exact parse_statistic command + result),
and whether real results were written or only dry-run.
1---2name: add-parser3description: Use when adding, fixing, or debugging a CLIST contest/standings parser — any task involving a judge scraper in src/ranking/management/modules/, the Statistic.get_standings contract, scraping a new site's leaderboard, or re-parsing one contest to test scraping. Keywords: parser, scraper, module, standings, judge, resource, get_standings.4---56# Add or fix a CLIST parser78Each judge is one module in `src/ranking/management/modules/<host>.py` exposing a9`Statistic` class that subclasses `BaseModule`. There are 85 existing modules —10**always read 2–3 similar ones first and copy their conventions.**1112## The contract1314```python15from ranking.management.modules.common import REQ, BaseModule16from ranking.management.modules.excepts import ExceptionParseStandings171819class Statistic(BaseModule):20 def get_standings(self, users=None, statistics=None, **kwargs):21 # Contest metadata is on self: self.url, self.key, self.info,22 # self.start_time, self.standings_url, self.resource, ...23 data = REQ.get(self.standings_url, return_json=True) # or post=..., headers=...24 if not_ok(data):25 raise ExceptionParseStandings(data)2627 result = {}28 for row_data in data['rows']:29 member = str(row_data['user_id']) # stable unique handle30 row = result.setdefault(member, {'member': member})31 row['name'] = row_data['user_name']32 row['solving'] = row_data['score'] # number of solved / score33 row['place'] = row_data['rank']34 row.setdefault('info', {})['avatar'] = row_data.get('avatar')35 return {'result': result}36```3738### Return shape3940`get_standings` returns a dict with at least `result`: a mapping of41`member -> row`. Common row keys:4243- `member` (**required**, str) — stable per-user handle/id.44- `place`, `solving`, `name`.45- `problems` — `{short: {'result': '+', 'time': '01:23', ...}}` per-problem.46- `info` — extra per-row data (avatar, rating, country…).4748Optional dict keys alongside `result`: `problems` (contest problem list),49`url`, `options`, `hidden_fields`, `season`.5051### Rules of the road5253- Use `REQ` (`from ...common import REQ`) for HTTP — never raw `requests`. It54 handles cookies, proxies, retries. `REQ.get(url, post=..., return_json=True,55 headers=...)`.56- Reuse helpers in `src/utils/`: `regex`, `timetools`, and57 `clist.templatetags.extras.get_item` for safe nested lookups.58- Raise `ExceptionParseStandings` (from `.excepts`) on bad/unexpected responses,59 not bare `Exception`.60- Keep it minimal and match the style of neighboring modules. Don't add new61 dependencies.62- Optional methods only if needed: `get_users_infos(users, resource, accounts,63 pbar)` (profile/rating data), `get_source_code(contest, problem)`.6465Good clean references to read: `highload.py`, `algorithm_yandex.py`,66`codeforces_gym.py`. For HTML scraping (not JSON) look at modules using67`REQ.get(...)` + parsing.6869## Test loop (safe, narrow, no junk data)7071The `dev` container is already running. Re-parse a single contest read-only:7273```bash74docker compose exec dev ./manage.py parse_statistic -r <host> -l 1 --no-update-results -s75```7677- `-r <host>` resource host (e.g. `highload.io`) `-l 1` only one contest78- `--no-update-results` don't write results `-s` stop on first exception79- Narrow further: `-e "<event regex>"`, `-y <year>`, `-u <user>`, `--reparse`80- `parse_statistic --help` lists every flag.8182Iterate until the standings parse cleanly, then drop `--no-update-results` for a83real run only if the task calls for it.8485## Wiring a brand-new judge8687A parser file alone isn't enough for a *new* site: a `Resource` + its `Module`88(`Module.path = ranking.management.modules.<host>`) must exist in the DB. If the89resource already exists, just edit/add the `.py`. If it's a new resource, say so90and confirm how the resource row should be created before assuming.9192## When done, report9394Module changed, how you tested (the exact `parse_statistic` command + result),95and whether real results were written or only dry-run.