ctfy.sdk.admin_resources.competitions
client.admin.competitions — competition CRUD + roster (admin).
1"""``client.admin.competitions`` — competition CRUD + roster (admin).""" 2 3from __future__ import annotations 4 5import builtins 6from datetime import datetime 7from typing import Any 8 9from ctfy.sdk._helpers import _raise_for_status 10from ctfy.sdk.base import BaseHttpClient 11from ctfy.server.models import CompetitionInfo, CompetitionRegistrationInfo 12 13 14class AdminCompetitionsResource: 15 """Create / update / delete competitions and inspect their roster.""" 16 17 def __init__(self, http: BaseHttpClient) -> None: 18 self._http = http 19 20 def list(self, offset: int = 0, limit: int = 50) -> builtins.list[CompetitionInfo]: 21 """Every competition — past, current, scheduled.""" 22 resp = self._http.request( 23 "GET", "/admin/competitions", params={"offset": offset, "limit": limit} 24 ) 25 _raise_for_status(resp) 26 return [CompetitionInfo.model_validate(c) for c in resp.json()["items"]] 27 28 def create( 29 self, 30 *, 31 title: str, 32 description: str = "", 33 starts_at: datetime | None = None, 34 ends_at: datetime | None = None, 35 challenge_ids: builtins.list[str] | None = None, 36 status: str = "draft", 37 access: str = "public", 38 eval_standard: bool = False, 39 corpus_sha: str = "", 40 scoring_rule: str = "", 41 scoring_params: dict[str, Any] | None = None, 42 sessions: builtins.list[tuple[datetime, datetime]] | None = None, 43 ) -> CompetitionInfo: 44 """Create a new competition. ``None`` time fields = no bound 45 (sent as JSON ``null``). Unknown ``challenge_ids`` → 422. 46 ``status`` defaults to ``draft`` (hidden from players); pass 47 ``"published"`` to make it immediately visible. ``access`` 48 defaults to ``public``; pass ``"private_listed"`` / 49 ``"private_hidden"`` for an invite-only competition. 50 ``eval_standard=True`` marks it an official eval standard set (only 51 such a competition may back an official EvalCampaign); ``corpus_sha`` 52 pins the corpus snapshot it was certified against. 53 ``scoring_rule`` selects the board's ranking algorithm 54 (``""`` = flag count, ``static_points``, ``dynamic_points``) with 55 ``scoring_params`` carrying its tuning; set it here where you can, 56 because scoring freezes once the competition goes live. 57 ``sessions`` declares the playable slices of the window as 58 ``(start, end)`` pairs — a three-day event running 08:00-16:00 59 daily is three of them. Supplying it also *sets* ``starts_at`` / 60 ``ends_at``, which the server derives from the envelope, and 61 opts the competition into the play gate: outside a session, 62 submissions and instance starts are refused and running 63 instances are torn down. Omit it for a single continuous 64 window, which is how every competition behaves by default.""" 65 resp = self._http.request( 66 "POST", 67 "/admin/competitions", 68 json={ 69 "title": title, 70 "description": description, 71 "starts_at": starts_at.isoformat() if starts_at else None, 72 "ends_at": ends_at.isoformat() if ends_at else None, 73 "challenge_ids": challenge_ids or [], 74 "status": status, 75 "access": access, 76 "eval_standard": eval_standard, 77 "corpus_sha": corpus_sha, 78 "scoring_rule": scoring_rule, 79 "scoring_params": scoring_params or {}, 80 "sessions": [ 81 {"starts_at": s.isoformat(), "ends_at": e.isoformat()} 82 for s, e in (sessions or []) 83 ], 84 }, 85 ) 86 _raise_for_status(resp) 87 return CompetitionInfo.model_validate(resp.json()) 88 89 def update(self, competition_id: str, **fields: Any) -> CompetitionInfo: 90 """PATCH any subset of ``title`` / ``description`` / ``starts_at`` / 91 ``ends_at`` / ``challenge_ids`` / ``status`` / ``access`` / 92 ``eval_standard`` / ``corpus_sha`` / ``scoring_rule`` / 93 ``scoring_params`` / ``sessions``. Omit a field to leave it; pass 94 ``None`` to clear. ``sessions`` is the one field where ``[]`` is 95 meaningful rather than empty: it clears the schedule and returns 96 the competition to a single continuous window. Changing the scoring fields on a live competition is 97 rejected with 409 — see ``competitions.scoring_locked``.""" 98 resp = self._http.request("PATCH", f"/admin/competitions/{competition_id}", json=fields) 99 _raise_for_status(resp) 100 return CompetitionInfo.model_validate(resp.json()) 101 102 def delete(self, competition_id: str) -> None: 103 """Hard-delete a competition. Cascades against per-comp teams, 104 memberships, invites, solve / submission rows.""" 105 resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}") 106 _raise_for_status(resp) 107 108 def registrations( 109 self, 110 competition_id: str, 111 offset: int = 0, 112 limit: int = 50, 113 ) -> builtins.list[CompetitionRegistrationInfo]: 114 """Roster for ``competition_id``. Per the per-comp refactor, a 115 team's existence with the comp scope IS the registration.""" 116 resp = self._http.request( 117 "GET", 118 f"/admin/competitions/{competition_id}/registrations", 119 params={"offset": offset, "limit": limit}, 120 ) 121 _raise_for_status(resp) 122 return [CompetitionRegistrationInfo.model_validate(r) for r in resp.json()["items"]]
15class AdminCompetitionsResource: 16 """Create / update / delete competitions and inspect their roster.""" 17 18 def __init__(self, http: BaseHttpClient) -> None: 19 self._http = http 20 21 def list(self, offset: int = 0, limit: int = 50) -> builtins.list[CompetitionInfo]: 22 """Every competition — past, current, scheduled.""" 23 resp = self._http.request( 24 "GET", "/admin/competitions", params={"offset": offset, "limit": limit} 25 ) 26 _raise_for_status(resp) 27 return [CompetitionInfo.model_validate(c) for c in resp.json()["items"]] 28 29 def create( 30 self, 31 *, 32 title: str, 33 description: str = "", 34 starts_at: datetime | None = None, 35 ends_at: datetime | None = None, 36 challenge_ids: builtins.list[str] | None = None, 37 status: str = "draft", 38 access: str = "public", 39 eval_standard: bool = False, 40 corpus_sha: str = "", 41 scoring_rule: str = "", 42 scoring_params: dict[str, Any] | None = None, 43 sessions: builtins.list[tuple[datetime, datetime]] | None = None, 44 ) -> CompetitionInfo: 45 """Create a new competition. ``None`` time fields = no bound 46 (sent as JSON ``null``). Unknown ``challenge_ids`` → 422. 47 ``status`` defaults to ``draft`` (hidden from players); pass 48 ``"published"`` to make it immediately visible. ``access`` 49 defaults to ``public``; pass ``"private_listed"`` / 50 ``"private_hidden"`` for an invite-only competition. 51 ``eval_standard=True`` marks it an official eval standard set (only 52 such a competition may back an official EvalCampaign); ``corpus_sha`` 53 pins the corpus snapshot it was certified against. 54 ``scoring_rule`` selects the board's ranking algorithm 55 (``""`` = flag count, ``static_points``, ``dynamic_points``) with 56 ``scoring_params`` carrying its tuning; set it here where you can, 57 because scoring freezes once the competition goes live. 58 ``sessions`` declares the playable slices of the window as 59 ``(start, end)`` pairs — a three-day event running 08:00-16:00 60 daily is three of them. Supplying it also *sets* ``starts_at`` / 61 ``ends_at``, which the server derives from the envelope, and 62 opts the competition into the play gate: outside a session, 63 submissions and instance starts are refused and running 64 instances are torn down. Omit it for a single continuous 65 window, which is how every competition behaves by default.""" 66 resp = self._http.request( 67 "POST", 68 "/admin/competitions", 69 json={ 70 "title": title, 71 "description": description, 72 "starts_at": starts_at.isoformat() if starts_at else None, 73 "ends_at": ends_at.isoformat() if ends_at else None, 74 "challenge_ids": challenge_ids or [], 75 "status": status, 76 "access": access, 77 "eval_standard": eval_standard, 78 "corpus_sha": corpus_sha, 79 "scoring_rule": scoring_rule, 80 "scoring_params": scoring_params or {}, 81 "sessions": [ 82 {"starts_at": s.isoformat(), "ends_at": e.isoformat()} 83 for s, e in (sessions or []) 84 ], 85 }, 86 ) 87 _raise_for_status(resp) 88 return CompetitionInfo.model_validate(resp.json()) 89 90 def update(self, competition_id: str, **fields: Any) -> CompetitionInfo: 91 """PATCH any subset of ``title`` / ``description`` / ``starts_at`` / 92 ``ends_at`` / ``challenge_ids`` / ``status`` / ``access`` / 93 ``eval_standard`` / ``corpus_sha`` / ``scoring_rule`` / 94 ``scoring_params`` / ``sessions``. Omit a field to leave it; pass 95 ``None`` to clear. ``sessions`` is the one field where ``[]`` is 96 meaningful rather than empty: it clears the schedule and returns 97 the competition to a single continuous window. Changing the scoring fields on a live competition is 98 rejected with 409 — see ``competitions.scoring_locked``.""" 99 resp = self._http.request("PATCH", f"/admin/competitions/{competition_id}", json=fields) 100 _raise_for_status(resp) 101 return CompetitionInfo.model_validate(resp.json()) 102 103 def delete(self, competition_id: str) -> None: 104 """Hard-delete a competition. Cascades against per-comp teams, 105 memberships, invites, solve / submission rows.""" 106 resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}") 107 _raise_for_status(resp) 108 109 def registrations( 110 self, 111 competition_id: str, 112 offset: int = 0, 113 limit: int = 50, 114 ) -> builtins.list[CompetitionRegistrationInfo]: 115 """Roster for ``competition_id``. Per the per-comp refactor, a 116 team's existence with the comp scope IS the registration.""" 117 resp = self._http.request( 118 "GET", 119 f"/admin/competitions/{competition_id}/registrations", 120 params={"offset": offset, "limit": limit}, 121 ) 122 _raise_for_status(resp) 123 return [CompetitionRegistrationInfo.model_validate(r) for r in resp.json()["items"]]
Create / update / delete competitions and inspect their roster.
21 def list(self, offset: int = 0, limit: int = 50) -> builtins.list[CompetitionInfo]: 22 """Every competition — past, current, scheduled.""" 23 resp = self._http.request( 24 "GET", "/admin/competitions", params={"offset": offset, "limit": limit} 25 ) 26 _raise_for_status(resp) 27 return [CompetitionInfo.model_validate(c) for c in resp.json()["items"]]
Every competition — past, current, scheduled.
29 def create( 30 self, 31 *, 32 title: str, 33 description: str = "", 34 starts_at: datetime | None = None, 35 ends_at: datetime | None = None, 36 challenge_ids: builtins.list[str] | None = None, 37 status: str = "draft", 38 access: str = "public", 39 eval_standard: bool = False, 40 corpus_sha: str = "", 41 scoring_rule: str = "", 42 scoring_params: dict[str, Any] | None = None, 43 sessions: builtins.list[tuple[datetime, datetime]] | None = None, 44 ) -> CompetitionInfo: 45 """Create a new competition. ``None`` time fields = no bound 46 (sent as JSON ``null``). Unknown ``challenge_ids`` → 422. 47 ``status`` defaults to ``draft`` (hidden from players); pass 48 ``"published"`` to make it immediately visible. ``access`` 49 defaults to ``public``; pass ``"private_listed"`` / 50 ``"private_hidden"`` for an invite-only competition. 51 ``eval_standard=True`` marks it an official eval standard set (only 52 such a competition may back an official EvalCampaign); ``corpus_sha`` 53 pins the corpus snapshot it was certified against. 54 ``scoring_rule`` selects the board's ranking algorithm 55 (``""`` = flag count, ``static_points``, ``dynamic_points``) with 56 ``scoring_params`` carrying its tuning; set it here where you can, 57 because scoring freezes once the competition goes live. 58 ``sessions`` declares the playable slices of the window as 59 ``(start, end)`` pairs — a three-day event running 08:00-16:00 60 daily is three of them. Supplying it also *sets* ``starts_at`` / 61 ``ends_at``, which the server derives from the envelope, and 62 opts the competition into the play gate: outside a session, 63 submissions and instance starts are refused and running 64 instances are torn down. Omit it for a single continuous 65 window, which is how every competition behaves by default.""" 66 resp = self._http.request( 67 "POST", 68 "/admin/competitions", 69 json={ 70 "title": title, 71 "description": description, 72 "starts_at": starts_at.isoformat() if starts_at else None, 73 "ends_at": ends_at.isoformat() if ends_at else None, 74 "challenge_ids": challenge_ids or [], 75 "status": status, 76 "access": access, 77 "eval_standard": eval_standard, 78 "corpus_sha": corpus_sha, 79 "scoring_rule": scoring_rule, 80 "scoring_params": scoring_params or {}, 81 "sessions": [ 82 {"starts_at": s.isoformat(), "ends_at": e.isoformat()} 83 for s, e in (sessions or []) 84 ], 85 }, 86 ) 87 _raise_for_status(resp) 88 return CompetitionInfo.model_validate(resp.json())
Create a new competition. None time fields = no bound
(sent as JSON null). Unknown challenge_ids → 422.
status defaults to draft (hidden from players); pass
"published" to make it immediately visible. access
defaults to public; pass "private_listed" /
"private_hidden" for an invite-only competition.
eval_standard=True marks it an official eval standard set (only
such a competition may back an official EvalCampaign); corpus_sha
pins the corpus snapshot it was certified against.
scoring_rule selects the board's ranking algorithm
("" = flag count, static_points, dynamic_points) with
scoring_params carrying its tuning; set it here where you can,
because scoring freezes once the competition goes live.
sessions declares the playable slices of the window as
(start, end) pairs — a three-day event running 08:00-16:00
daily is three of them. Supplying it also sets starts_at /
ends_at, which the server derives from the envelope, and
opts the competition into the play gate: outside a session,
submissions and instance starts are refused and running
instances are torn down. Omit it for a single continuous
window, which is how every competition behaves by default.
90 def update(self, competition_id: str, **fields: Any) -> CompetitionInfo: 91 """PATCH any subset of ``title`` / ``description`` / ``starts_at`` / 92 ``ends_at`` / ``challenge_ids`` / ``status`` / ``access`` / 93 ``eval_standard`` / ``corpus_sha`` / ``scoring_rule`` / 94 ``scoring_params`` / ``sessions``. Omit a field to leave it; pass 95 ``None`` to clear. ``sessions`` is the one field where ``[]`` is 96 meaningful rather than empty: it clears the schedule and returns 97 the competition to a single continuous window. Changing the scoring fields on a live competition is 98 rejected with 409 — see ``competitions.scoring_locked``.""" 99 resp = self._http.request("PATCH", f"/admin/competitions/{competition_id}", json=fields) 100 _raise_for_status(resp) 101 return CompetitionInfo.model_validate(resp.json())
PATCH any subset of title / description / starts_at /
ends_at / challenge_ids / status / access /
eval_standard / corpus_sha / scoring_rule /
scoring_params / sessions. Omit a field to leave it; pass
None to clear. sessions is the one field where [] is
meaningful rather than empty: it clears the schedule and returns
the competition to a single continuous window. Changing the scoring fields on a live competition is
rejected with 409 — see competitions.scoring_locked.
103 def delete(self, competition_id: str) -> None: 104 """Hard-delete a competition. Cascades against per-comp teams, 105 memberships, invites, solve / submission rows.""" 106 resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}") 107 _raise_for_status(resp)
Hard-delete a competition. Cascades against per-comp teams, memberships, invites, solve / submission rows.
109 def registrations( 110 self, 111 competition_id: str, 112 offset: int = 0, 113 limit: int = 50, 114 ) -> builtins.list[CompetitionRegistrationInfo]: 115 """Roster for ``competition_id``. Per the per-comp refactor, a 116 team's existence with the comp scope IS the registration.""" 117 resp = self._http.request( 118 "GET", 119 f"/admin/competitions/{competition_id}/registrations", 120 params={"offset": offset, "limit": limit}, 121 ) 122 _raise_for_status(resp) 123 return [CompetitionRegistrationInfo.model_validate(r) for r in resp.json()["items"]]
Roster for competition_id. Per the per-comp refactor, a
team's existence with the comp scope IS the registration.