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.core.localized import LocalizedText 10from ctfy.sdk._helpers import _raise_for_status 11from ctfy.sdk.base import BaseHttpClient 12from ctfy.server.models import ( 13 AuthoringScaleInfo, 14 AwdMatchConfig, 15 CompetitionFormatPreset, 16 CompetitionInfo, 17 CompetitionRecords, 18 CompetitionRegistrationInfo, 19) 20 21 22class AdminCompetitionsResource: 23 """Create / update / delete competitions and inspect their roster.""" 24 25 def __init__(self, http: BaseHttpClient) -> None: 26 self._http = http 27 28 def formats(self) -> builtins.list[CompetitionFormatPreset]: 29 """The wizard's named starting points for a new competition. 30 31 Each entry is a set of axis values, not a type: apply one to 32 ``create(...)`` and then override whatever the event needs. The 33 platform stores no record of which one was used. 34 """ 35 resp = self._http.request("GET", "/admin/competition-formats") 36 _raise_for_status(resp) 37 return [CompetitionFormatPreset.model_validate(f) for f in resp.json()] 38 39 def authoring_scale(self, competition_id: str) -> AuthoringScaleInfo: 40 """What this board's ``authoring_weight`` is worth, in challenges. 41 42 A ``peer_authoring`` weight is a fixed multiplier on a curve 43 output, so on its own it says nothing: 200 is generous on a board 44 of 100-point flags and a rounding error on one of 5000. This 45 prices it against the competition's own challenge set. 46 47 Read-only, and computed from the **stored** rule and challenge 48 set — a weight you have not saved is not what this answers about. 49 """ 50 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/authoring-scale") 51 _raise_for_status(resp) 52 return AuthoringScaleInfo.model_validate(resp.json()) 53 54 def records(self, competition_id: str) -> CompetitionRecords: 55 """Every record of one competition, for scoring outside the platform. 56 57 Teams (with each agent's owner and the owner's team), solves as the 58 board ranks them, submissions without their text, agent runs, and 59 uploads with their authors — complete, never a page. 60 """ 61 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/records") 62 _raise_for_status(resp) 63 return CompetitionRecords.model_validate(resp.json()) 64 65 def list(self, offset: int = 0, limit: int = 50) -> builtins.list[CompetitionInfo]: 66 """Every competition — past, current, scheduled.""" 67 resp = self._http.request( 68 "GET", "/admin/competitions", params={"offset": offset, "limit": limit} 69 ) 70 _raise_for_status(resp) 71 return [CompetitionInfo.model_validate(c) for c in resp.json()["items"]] 72 73 def create( 74 self, 75 *, 76 title: str, 77 description: str | LocalizedText = "", 78 handbook: str | LocalizedText = "", 79 starts_at: datetime | None = None, 80 ends_at: datetime | None = None, 81 challenge_ids: builtins.list[str] | None = None, 82 status: str = "draft", 83 access: str = "public", 84 eval_standard: bool = False, 85 corpus_sha: str = "", 86 scoring_rule: str = "", 87 scoring_params: dict[str, Any] | None = None, 88 sessions: builtins.list[tuple[datetime, datetime]] | None = None, 89 awd_match: AwdMatchConfig | None = None, 90 entrants_are_agents: bool = False, 91 ) -> CompetitionInfo: 92 """Create a new competition. ``None`` time fields = no bound 93 (sent as JSON ``null``). Unknown ``challenge_ids`` → 422. 94 ``status`` defaults to ``draft`` (hidden from players); pass 95 ``"published"`` to make it immediately visible. ``access`` 96 defaults to ``public``; pass ``"private_listed"`` / 97 ``"private_hidden"`` for an invite-only competition. 98 ``eval_standard=True`` marks it an official eval standard set (only 99 such a competition may back an official EvalCampaign); ``corpus_sha`` 100 pins the corpus snapshot it was certified against. 101 ``scoring_rule`` selects the board's ranking algorithm 102 (``""`` = flag count, ``static_points``, ``dynamic_points``) with 103 ``scoring_params`` carrying its tuning; set it here where you can, 104 because scoring freezes once the competition goes live. 105 ``sessions`` declares the playable slices of the window as 106 ``(start, end)`` pairs — a three-day event running 08:00-16:00 107 daily is three of them. Supplying it also *sets* ``starts_at`` / 108 ``ends_at``, which the server derives from the envelope, and 109 opts the competition into the play gate: outside a session, 110 submissions and instance starts are refused and running 111 instances are torn down. Omit it for a single continuous 112 window, which is how every competition behaves by default. 113 ``handbook`` is the organiser's long-form rules page, markdown, 114 capped server-side — ``description`` stays the one-line blurb the 115 listing and the page header truncate. Both are localised: pass a 116 ``{locale: text}`` map to write one translation, or a bare string, 117 which the server reads as ``en``. ⚠️ **A bare string is kept in the 118 signature rather than deprecated** — every caller that predates 119 localisation sends one, and a 422 for all of them would be a 120 breaking change dressed up as a feature; the coercion is the 121 server's (``coerce_localized``), so the two cannot disagree. 122 ``entrants_are_agents=True`` marks it an agent exhibition: human 123 registration is refused server-side and the browser shows a 124 spectate affordance rather than a sign-up button.""" 125 resp = self._http.request( 126 "POST", 127 "/admin/competitions", 128 json={ 129 "title": title, 130 "description": description, 131 "handbook": handbook, 132 "starts_at": starts_at.isoformat() if starts_at else None, 133 "ends_at": ends_at.isoformat() if ends_at else None, 134 "challenge_ids": challenge_ids or [], 135 "status": status, 136 "access": access, 137 "eval_standard": eval_standard, 138 "corpus_sha": corpus_sha, 139 "scoring_rule": scoring_rule, 140 "scoring_params": scoring_params or {}, 141 # An agent exhibition: entrants are dispatched agents, so 142 # human registration is refused and the browser offers a 143 # spectate affordance. See ``CompetitionCreate``. 144 "entrants_are_agents": entrants_are_agents, 145 "sessions": [ 146 {"starts_at": s.isoformat(), "ends_at": e.isoformat()} 147 for s, e in (sessions or []) 148 ], 149 # ⚠️ **Absent, never `{}`.** The server's model is 150 # `extra="forbid"` with two required cadence fields, so 151 # an empty object is a 422 on every competition that 152 # runs no rounds. `None` here means "not configured in 153 # this request", which the upsert route can still 154 # satisfy tomorrow — never "this competition has no 155 # match". 156 **({"awd_match": awd_match.model_dump(mode="json")} if awd_match else {}), 157 }, 158 ) 159 _raise_for_status(resp) 160 return CompetitionInfo.model_validate(resp.json()) 161 162 def update(self, competition_id: str, **fields: Any) -> CompetitionInfo: 163 """PATCH any subset of ``title`` / ``description`` / ``starts_at`` / 164 ``ends_at`` / ``challenge_ids`` / ``status`` / ``access`` / 165 ``eval_standard`` / ``corpus_sha`` / ``scoring_rule`` / 166 ``scoring_params`` / ``sessions`` / ``anonymous_entry`` / 167 ``anonymous_entry_kind``. ⚠️ This is a **passthrough** — the list 168 is what the wire accepts today, so it is a pointer to 169 ``CompetitionUpdate`` rather than a gate, and :meth:`create` is 170 the narrow one (it builds its body by hand and cannot carry a 171 field it does not name). ⚠️ ``anonymous_entry`` may be *widened* 172 freely and is refused on a narrowing once entrants have relied 173 on it, ``required`` → ``optional`` included: that transition 174 un-anonymises the whole field at once. Omit a field to leave it; pass 175 ``None`` to clear. ``sessions`` is the one field where ``[]`` is 176 meaningful rather than empty: it clears the schedule and returns 177 the competition to a single continuous window. Changing the scoring fields on a live competition is 178 rejected with 409 — see ``competitions.scoring_locked``.""" 179 resp = self._http.request("PATCH", f"/admin/competitions/{competition_id}", json=fields) 180 _raise_for_status(resp) 181 return CompetitionInfo.model_validate(resp.json()) 182 183 def delete(self, competition_id: str) -> None: 184 """Hard-delete a competition. Cascades against per-comp teams, 185 memberships, invites, solve / submission rows.""" 186 resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}") 187 _raise_for_status(resp) 188 189 def registrations( 190 self, 191 competition_id: str, 192 offset: int = 0, 193 limit: int = 50, 194 ) -> builtins.list[CompetitionRegistrationInfo]: 195 """Roster for ``competition_id``. Per the per-comp refactor, a 196 team's existence with the comp scope IS the registration.""" 197 resp = self._http.request( 198 "GET", 199 f"/admin/competitions/{competition_id}/registrations", 200 params={"offset": offset, "limit": limit}, 201 ) 202 _raise_for_status(resp) 203 return [CompetitionRegistrationInfo.model_validate(r) for r in resp.json()["items"]]
23class AdminCompetitionsResource: 24 """Create / update / delete competitions and inspect their roster.""" 25 26 def __init__(self, http: BaseHttpClient) -> None: 27 self._http = http 28 29 def formats(self) -> builtins.list[CompetitionFormatPreset]: 30 """The wizard's named starting points for a new competition. 31 32 Each entry is a set of axis values, not a type: apply one to 33 ``create(...)`` and then override whatever the event needs. The 34 platform stores no record of which one was used. 35 """ 36 resp = self._http.request("GET", "/admin/competition-formats") 37 _raise_for_status(resp) 38 return [CompetitionFormatPreset.model_validate(f) for f in resp.json()] 39 40 def authoring_scale(self, competition_id: str) -> AuthoringScaleInfo: 41 """What this board's ``authoring_weight`` is worth, in challenges. 42 43 A ``peer_authoring`` weight is a fixed multiplier on a curve 44 output, so on its own it says nothing: 200 is generous on a board 45 of 100-point flags and a rounding error on one of 5000. This 46 prices it against the competition's own challenge set. 47 48 Read-only, and computed from the **stored** rule and challenge 49 set — a weight you have not saved is not what this answers about. 50 """ 51 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/authoring-scale") 52 _raise_for_status(resp) 53 return AuthoringScaleInfo.model_validate(resp.json()) 54 55 def records(self, competition_id: str) -> CompetitionRecords: 56 """Every record of one competition, for scoring outside the platform. 57 58 Teams (with each agent's owner and the owner's team), solves as the 59 board ranks them, submissions without their text, agent runs, and 60 uploads with their authors — complete, never a page. 61 """ 62 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/records") 63 _raise_for_status(resp) 64 return CompetitionRecords.model_validate(resp.json()) 65 66 def list(self, offset: int = 0, limit: int = 50) -> builtins.list[CompetitionInfo]: 67 """Every competition — past, current, scheduled.""" 68 resp = self._http.request( 69 "GET", "/admin/competitions", params={"offset": offset, "limit": limit} 70 ) 71 _raise_for_status(resp) 72 return [CompetitionInfo.model_validate(c) for c in resp.json()["items"]] 73 74 def create( 75 self, 76 *, 77 title: str, 78 description: str | LocalizedText = "", 79 handbook: str | LocalizedText = "", 80 starts_at: datetime | None = None, 81 ends_at: datetime | None = None, 82 challenge_ids: builtins.list[str] | None = None, 83 status: str = "draft", 84 access: str = "public", 85 eval_standard: bool = False, 86 corpus_sha: str = "", 87 scoring_rule: str = "", 88 scoring_params: dict[str, Any] | None = None, 89 sessions: builtins.list[tuple[datetime, datetime]] | None = None, 90 awd_match: AwdMatchConfig | None = None, 91 entrants_are_agents: bool = False, 92 ) -> CompetitionInfo: 93 """Create a new competition. ``None`` time fields = no bound 94 (sent as JSON ``null``). Unknown ``challenge_ids`` → 422. 95 ``status`` defaults to ``draft`` (hidden from players); pass 96 ``"published"`` to make it immediately visible. ``access`` 97 defaults to ``public``; pass ``"private_listed"`` / 98 ``"private_hidden"`` for an invite-only competition. 99 ``eval_standard=True`` marks it an official eval standard set (only 100 such a competition may back an official EvalCampaign); ``corpus_sha`` 101 pins the corpus snapshot it was certified against. 102 ``scoring_rule`` selects the board's ranking algorithm 103 (``""`` = flag count, ``static_points``, ``dynamic_points``) with 104 ``scoring_params`` carrying its tuning; set it here where you can, 105 because scoring freezes once the competition goes live. 106 ``sessions`` declares the playable slices of the window as 107 ``(start, end)`` pairs — a three-day event running 08:00-16:00 108 daily is three of them. Supplying it also *sets* ``starts_at`` / 109 ``ends_at``, which the server derives from the envelope, and 110 opts the competition into the play gate: outside a session, 111 submissions and instance starts are refused and running 112 instances are torn down. Omit it for a single continuous 113 window, which is how every competition behaves by default. 114 ``handbook`` is the organiser's long-form rules page, markdown, 115 capped server-side — ``description`` stays the one-line blurb the 116 listing and the page header truncate. Both are localised: pass a 117 ``{locale: text}`` map to write one translation, or a bare string, 118 which the server reads as ``en``. ⚠️ **A bare string is kept in the 119 signature rather than deprecated** — every caller that predates 120 localisation sends one, and a 422 for all of them would be a 121 breaking change dressed up as a feature; the coercion is the 122 server's (``coerce_localized``), so the two cannot disagree. 123 ``entrants_are_agents=True`` marks it an agent exhibition: human 124 registration is refused server-side and the browser shows a 125 spectate affordance rather than a sign-up button.""" 126 resp = self._http.request( 127 "POST", 128 "/admin/competitions", 129 json={ 130 "title": title, 131 "description": description, 132 "handbook": handbook, 133 "starts_at": starts_at.isoformat() if starts_at else None, 134 "ends_at": ends_at.isoformat() if ends_at else None, 135 "challenge_ids": challenge_ids or [], 136 "status": status, 137 "access": access, 138 "eval_standard": eval_standard, 139 "corpus_sha": corpus_sha, 140 "scoring_rule": scoring_rule, 141 "scoring_params": scoring_params or {}, 142 # An agent exhibition: entrants are dispatched agents, so 143 # human registration is refused and the browser offers a 144 # spectate affordance. See ``CompetitionCreate``. 145 "entrants_are_agents": entrants_are_agents, 146 "sessions": [ 147 {"starts_at": s.isoformat(), "ends_at": e.isoformat()} 148 for s, e in (sessions or []) 149 ], 150 # ⚠️ **Absent, never `{}`.** The server's model is 151 # `extra="forbid"` with two required cadence fields, so 152 # an empty object is a 422 on every competition that 153 # runs no rounds. `None` here means "not configured in 154 # this request", which the upsert route can still 155 # satisfy tomorrow — never "this competition has no 156 # match". 157 **({"awd_match": awd_match.model_dump(mode="json")} if awd_match else {}), 158 }, 159 ) 160 _raise_for_status(resp) 161 return CompetitionInfo.model_validate(resp.json()) 162 163 def update(self, competition_id: str, **fields: Any) -> CompetitionInfo: 164 """PATCH any subset of ``title`` / ``description`` / ``starts_at`` / 165 ``ends_at`` / ``challenge_ids`` / ``status`` / ``access`` / 166 ``eval_standard`` / ``corpus_sha`` / ``scoring_rule`` / 167 ``scoring_params`` / ``sessions`` / ``anonymous_entry`` / 168 ``anonymous_entry_kind``. ⚠️ This is a **passthrough** — the list 169 is what the wire accepts today, so it is a pointer to 170 ``CompetitionUpdate`` rather than a gate, and :meth:`create` is 171 the narrow one (it builds its body by hand and cannot carry a 172 field it does not name). ⚠️ ``anonymous_entry`` may be *widened* 173 freely and is refused on a narrowing once entrants have relied 174 on it, ``required`` → ``optional`` included: that transition 175 un-anonymises the whole field at once. Omit a field to leave it; pass 176 ``None`` to clear. ``sessions`` is the one field where ``[]`` is 177 meaningful rather than empty: it clears the schedule and returns 178 the competition to a single continuous window. Changing the scoring fields on a live competition is 179 rejected with 409 — see ``competitions.scoring_locked``.""" 180 resp = self._http.request("PATCH", f"/admin/competitions/{competition_id}", json=fields) 181 _raise_for_status(resp) 182 return CompetitionInfo.model_validate(resp.json()) 183 184 def delete(self, competition_id: str) -> None: 185 """Hard-delete a competition. Cascades against per-comp teams, 186 memberships, invites, solve / submission rows.""" 187 resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}") 188 _raise_for_status(resp) 189 190 def registrations( 191 self, 192 competition_id: str, 193 offset: int = 0, 194 limit: int = 50, 195 ) -> builtins.list[CompetitionRegistrationInfo]: 196 """Roster for ``competition_id``. Per the per-comp refactor, a 197 team's existence with the comp scope IS the registration.""" 198 resp = self._http.request( 199 "GET", 200 f"/admin/competitions/{competition_id}/registrations", 201 params={"offset": offset, "limit": limit}, 202 ) 203 _raise_for_status(resp) 204 return [CompetitionRegistrationInfo.model_validate(r) for r in resp.json()["items"]]
Create / update / delete competitions and inspect their roster.
29 def formats(self) -> builtins.list[CompetitionFormatPreset]: 30 """The wizard's named starting points for a new competition. 31 32 Each entry is a set of axis values, not a type: apply one to 33 ``create(...)`` and then override whatever the event needs. The 34 platform stores no record of which one was used. 35 """ 36 resp = self._http.request("GET", "/admin/competition-formats") 37 _raise_for_status(resp) 38 return [CompetitionFormatPreset.model_validate(f) for f in resp.json()]
The wizard's named starting points for a new competition.
Each entry is a set of axis values, not a type: apply one to
create(...) and then override whatever the event needs. The
platform stores no record of which one was used.
55 def records(self, competition_id: str) -> CompetitionRecords: 56 """Every record of one competition, for scoring outside the platform. 57 58 Teams (with each agent's owner and the owner's team), solves as the 59 board ranks them, submissions without their text, agent runs, and 60 uploads with their authors — complete, never a page. 61 """ 62 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/records") 63 _raise_for_status(resp) 64 return CompetitionRecords.model_validate(resp.json())
Every record of one competition, for scoring outside the platform.
Teams (with each agent's owner and the owner's team), solves as the board ranks them, submissions without their text, agent runs, and uploads with their authors — complete, never a page.
66 def list(self, offset: int = 0, limit: int = 50) -> builtins.list[CompetitionInfo]: 67 """Every competition — past, current, scheduled.""" 68 resp = self._http.request( 69 "GET", "/admin/competitions", params={"offset": offset, "limit": limit} 70 ) 71 _raise_for_status(resp) 72 return [CompetitionInfo.model_validate(c) for c in resp.json()["items"]]
Every competition — past, current, scheduled.
74 def create( 75 self, 76 *, 77 title: str, 78 description: str | LocalizedText = "", 79 handbook: str | LocalizedText = "", 80 starts_at: datetime | None = None, 81 ends_at: datetime | None = None, 82 challenge_ids: builtins.list[str] | None = None, 83 status: str = "draft", 84 access: str = "public", 85 eval_standard: bool = False, 86 corpus_sha: str = "", 87 scoring_rule: str = "", 88 scoring_params: dict[str, Any] | None = None, 89 sessions: builtins.list[tuple[datetime, datetime]] | None = None, 90 awd_match: AwdMatchConfig | None = None, 91 entrants_are_agents: bool = False, 92 ) -> CompetitionInfo: 93 """Create a new competition. ``None`` time fields = no bound 94 (sent as JSON ``null``). Unknown ``challenge_ids`` → 422. 95 ``status`` defaults to ``draft`` (hidden from players); pass 96 ``"published"`` to make it immediately visible. ``access`` 97 defaults to ``public``; pass ``"private_listed"`` / 98 ``"private_hidden"`` for an invite-only competition. 99 ``eval_standard=True`` marks it an official eval standard set (only 100 such a competition may back an official EvalCampaign); ``corpus_sha`` 101 pins the corpus snapshot it was certified against. 102 ``scoring_rule`` selects the board's ranking algorithm 103 (``""`` = flag count, ``static_points``, ``dynamic_points``) with 104 ``scoring_params`` carrying its tuning; set it here where you can, 105 because scoring freezes once the competition goes live. 106 ``sessions`` declares the playable slices of the window as 107 ``(start, end)`` pairs — a three-day event running 08:00-16:00 108 daily is three of them. Supplying it also *sets* ``starts_at`` / 109 ``ends_at``, which the server derives from the envelope, and 110 opts the competition into the play gate: outside a session, 111 submissions and instance starts are refused and running 112 instances are torn down. Omit it for a single continuous 113 window, which is how every competition behaves by default. 114 ``handbook`` is the organiser's long-form rules page, markdown, 115 capped server-side — ``description`` stays the one-line blurb the 116 listing and the page header truncate. Both are localised: pass a 117 ``{locale: text}`` map to write one translation, or a bare string, 118 which the server reads as ``en``. ⚠️ **A bare string is kept in the 119 signature rather than deprecated** — every caller that predates 120 localisation sends one, and a 422 for all of them would be a 121 breaking change dressed up as a feature; the coercion is the 122 server's (``coerce_localized``), so the two cannot disagree. 123 ``entrants_are_agents=True`` marks it an agent exhibition: human 124 registration is refused server-side and the browser shows a 125 spectate affordance rather than a sign-up button.""" 126 resp = self._http.request( 127 "POST", 128 "/admin/competitions", 129 json={ 130 "title": title, 131 "description": description, 132 "handbook": handbook, 133 "starts_at": starts_at.isoformat() if starts_at else None, 134 "ends_at": ends_at.isoformat() if ends_at else None, 135 "challenge_ids": challenge_ids or [], 136 "status": status, 137 "access": access, 138 "eval_standard": eval_standard, 139 "corpus_sha": corpus_sha, 140 "scoring_rule": scoring_rule, 141 "scoring_params": scoring_params or {}, 142 # An agent exhibition: entrants are dispatched agents, so 143 # human registration is refused and the browser offers a 144 # spectate affordance. See ``CompetitionCreate``. 145 "entrants_are_agents": entrants_are_agents, 146 "sessions": [ 147 {"starts_at": s.isoformat(), "ends_at": e.isoformat()} 148 for s, e in (sessions or []) 149 ], 150 # ⚠️ **Absent, never `{}`.** The server's model is 151 # `extra="forbid"` with two required cadence fields, so 152 # an empty object is a 422 on every competition that 153 # runs no rounds. `None` here means "not configured in 154 # this request", which the upsert route can still 155 # satisfy tomorrow — never "this competition has no 156 # match". 157 **({"awd_match": awd_match.model_dump(mode="json")} if awd_match else {}), 158 }, 159 ) 160 _raise_for_status(resp) 161 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.
handbook is the organiser's long-form rules page, markdown,
capped server-side — description stays the one-line blurb the
listing and the page header truncate. Both are localised: pass a
{locale: text} map to write one translation, or a bare string,
which the server reads as en. ⚠️ A bare string is kept in the
signature rather than deprecated — every caller that predates
localisation sends one, and a 422 for all of them would be a
breaking change dressed up as a feature; the coercion is the
server's (coerce_localized), so the two cannot disagree.
entrants_are_agents=True marks it an agent exhibition: human
registration is refused server-side and the browser shows a
spectate affordance rather than a sign-up button.
163 def update(self, competition_id: str, **fields: Any) -> CompetitionInfo: 164 """PATCH any subset of ``title`` / ``description`` / ``starts_at`` / 165 ``ends_at`` / ``challenge_ids`` / ``status`` / ``access`` / 166 ``eval_standard`` / ``corpus_sha`` / ``scoring_rule`` / 167 ``scoring_params`` / ``sessions`` / ``anonymous_entry`` / 168 ``anonymous_entry_kind``. ⚠️ This is a **passthrough** — the list 169 is what the wire accepts today, so it is a pointer to 170 ``CompetitionUpdate`` rather than a gate, and :meth:`create` is 171 the narrow one (it builds its body by hand and cannot carry a 172 field it does not name). ⚠️ ``anonymous_entry`` may be *widened* 173 freely and is refused on a narrowing once entrants have relied 174 on it, ``required`` → ``optional`` included: that transition 175 un-anonymises the whole field at once. Omit a field to leave it; pass 176 ``None`` to clear. ``sessions`` is the one field where ``[]`` is 177 meaningful rather than empty: it clears the schedule and returns 178 the competition to a single continuous window. Changing the scoring fields on a live competition is 179 rejected with 409 — see ``competitions.scoring_locked``.""" 180 resp = self._http.request("PATCH", f"/admin/competitions/{competition_id}", json=fields) 181 _raise_for_status(resp) 182 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 / anonymous_entry /
anonymous_entry_kind. ⚠️ This is a passthrough — the list
is what the wire accepts today, so it is a pointer to
CompetitionUpdate rather than a gate, and create() is
the narrow one (it builds its body by hand and cannot carry a
field it does not name). ⚠️ anonymous_entry may be widened
freely and is refused on a narrowing once entrants have relied
on it, required → optional included: that transition
un-anonymises the whole field at once. 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.
184 def delete(self, competition_id: str) -> None: 185 """Hard-delete a competition. Cascades against per-comp teams, 186 memberships, invites, solve / submission rows.""" 187 resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}") 188 _raise_for_status(resp)
Hard-delete a competition. Cascades against per-comp teams, memberships, invites, solve / submission rows.
190 def registrations( 191 self, 192 competition_id: str, 193 offset: int = 0, 194 limit: int = 50, 195 ) -> builtins.list[CompetitionRegistrationInfo]: 196 """Roster for ``competition_id``. Per the per-comp refactor, a 197 team's existence with the comp scope IS the registration.""" 198 resp = self._http.request( 199 "GET", 200 f"/admin/competitions/{competition_id}/registrations", 201 params={"offset": offset, "limit": limit}, 202 ) 203 _raise_for_status(resp) 204 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.