ctfy.sdk.resources.competitions
client.competitions — discover competitions (list / get).
Acting within a competition — register, your team, invites, challenges,
instances, submissions, standings — lives on the scoped handle
client.competition(id) (ctfy.sdk.competition.Competition).
1"""``client.competitions`` — discover competitions (list / get). 2 3Acting *within* a competition — register, your team, invites, challenges, 4instances, submissions, standings — lives on the scoped handle 5``client.competition(id)`` (:class:`ctfy.sdk.competition.Competition`). 6""" 7 8from __future__ import annotations 9 10import builtins 11from typing import Any 12 13from ctfy.sdk._helpers import _extract_items, _raise_for_status 14from ctfy.sdk.base import BaseHttpClient 15from ctfy.server.models import CompetitionDetail, CompetitionInfo, VirtualGhostBoard 16 17 18class CompetitionsResource: 19 """Browse competitions. To act within one, use ``client.competition(id)``.""" 20 21 def __init__(self, http: BaseHttpClient) -> None: 22 self._http = http 23 24 def list( 25 self, phase: str = "", offset: int = 0, limit: int = 50 26 ) -> builtins.list[CompetitionInfo]: 27 """Public competition list. ``phase`` ∈ ``"" | "upcoming" | 28 "running" | "past"`` ("" = all). Start here, then scope into one 29 with ``client.competition(comp.id)``.""" 30 params: dict[str, Any] = {"offset": offset, "limit": limit} 31 if phase: 32 params["phase"] = phase 33 resp = self._http.request("GET", "/competitions", params=params) 34 _raise_for_status(resp) 35 return _extract_items(resp.json(), CompetitionInfo) 36 37 def get(self, competition_id: str) -> CompetitionDetail: 38 """Full competition detail including resolved challenge summaries 39 (``.challenges``). Same as ``client.competition(id).detail()``.""" 40 resp = self._http.request("GET", f"/competitions/{competition_id}") 41 _raise_for_status(resp) 42 return CompetitionDetail.model_validate(resp.json()) 43 44 def start_virtual(self, competition_id: str) -> CompetitionInfo: 45 """Replay a finished contest on your own clock. 46 47 Returns the *replay*, which is an ordinary competition you are 48 already registered in — so every other call takes its 49 ``.id`` unchanged: launch instances, submit answers, submit 50 patches, read its board. 51 52 **Idempotent.** Asking twice returns the replay you already 53 started rather than minting a second sitting, so a retry cannot 54 split one attempt across two competitions. 55 56 Refused (400) when a replay is not defined: the source has not 57 finished, has no window, is multi-session, never ran, or is 58 itself a replay. The error code says which. 59 """ 60 resp = self._http.request("POST", f"/competitions/{competition_id}/virtual") 61 _raise_for_status(resp) 62 return CompetitionInfo.model_validate(resp.json()) 63 64 def ghosts(self, competition_id: str) -> VirtualGhostBoard: 65 """The source contest's standings at the instant this replay has 66 reached, with you merged in and ranked among them. 67 68 Takes the **replay's** id, not the source's. At T+40 minutes 69 into your run you see the contest as it stood at T+40 minutes on 70 the day — which is what makes a replay a race rather than an 71 exercise, since a board holding only your own solves answers 72 nothing. 73 74 Refused (400 ``not_a_virtual_sitting``) on an ordinary 75 competition. A source that has since been deleted costs the 76 ghosts and nothing else: the reply comes back with no items and 77 the run itself keeps working. 78 """ 79 resp = self._http.request("GET", f"/competitions/{competition_id}/ghosts") 80 _raise_for_status(resp) 81 return VirtualGhostBoard.model_validate(resp.json())
19class CompetitionsResource: 20 """Browse competitions. To act within one, use ``client.competition(id)``.""" 21 22 def __init__(self, http: BaseHttpClient) -> None: 23 self._http = http 24 25 def list( 26 self, phase: str = "", offset: int = 0, limit: int = 50 27 ) -> builtins.list[CompetitionInfo]: 28 """Public competition list. ``phase`` ∈ ``"" | "upcoming" | 29 "running" | "past"`` ("" = all). Start here, then scope into one 30 with ``client.competition(comp.id)``.""" 31 params: dict[str, Any] = {"offset": offset, "limit": limit} 32 if phase: 33 params["phase"] = phase 34 resp = self._http.request("GET", "/competitions", params=params) 35 _raise_for_status(resp) 36 return _extract_items(resp.json(), CompetitionInfo) 37 38 def get(self, competition_id: str) -> CompetitionDetail: 39 """Full competition detail including resolved challenge summaries 40 (``.challenges``). Same as ``client.competition(id).detail()``.""" 41 resp = self._http.request("GET", f"/competitions/{competition_id}") 42 _raise_for_status(resp) 43 return CompetitionDetail.model_validate(resp.json()) 44 45 def start_virtual(self, competition_id: str) -> CompetitionInfo: 46 """Replay a finished contest on your own clock. 47 48 Returns the *replay*, which is an ordinary competition you are 49 already registered in — so every other call takes its 50 ``.id`` unchanged: launch instances, submit answers, submit 51 patches, read its board. 52 53 **Idempotent.** Asking twice returns the replay you already 54 started rather than minting a second sitting, so a retry cannot 55 split one attempt across two competitions. 56 57 Refused (400) when a replay is not defined: the source has not 58 finished, has no window, is multi-session, never ran, or is 59 itself a replay. The error code says which. 60 """ 61 resp = self._http.request("POST", f"/competitions/{competition_id}/virtual") 62 _raise_for_status(resp) 63 return CompetitionInfo.model_validate(resp.json()) 64 65 def ghosts(self, competition_id: str) -> VirtualGhostBoard: 66 """The source contest's standings at the instant this replay has 67 reached, with you merged in and ranked among them. 68 69 Takes the **replay's** id, not the source's. At T+40 minutes 70 into your run you see the contest as it stood at T+40 minutes on 71 the day — which is what makes a replay a race rather than an 72 exercise, since a board holding only your own solves answers 73 nothing. 74 75 Refused (400 ``not_a_virtual_sitting``) on an ordinary 76 competition. A source that has since been deleted costs the 77 ghosts and nothing else: the reply comes back with no items and 78 the run itself keeps working. 79 """ 80 resp = self._http.request("GET", f"/competitions/{competition_id}/ghosts") 81 _raise_for_status(resp) 82 return VirtualGhostBoard.model_validate(resp.json())
Browse competitions. To act within one, use client.competition(id).
25 def list( 26 self, phase: str = "", offset: int = 0, limit: int = 50 27 ) -> builtins.list[CompetitionInfo]: 28 """Public competition list. ``phase`` ∈ ``"" | "upcoming" | 29 "running" | "past"`` ("" = all). Start here, then scope into one 30 with ``client.competition(comp.id)``.""" 31 params: dict[str, Any] = {"offset": offset, "limit": limit} 32 if phase: 33 params["phase"] = phase 34 resp = self._http.request("GET", "/competitions", params=params) 35 _raise_for_status(resp) 36 return _extract_items(resp.json(), CompetitionInfo)
Public competition list. phase ∈ "" | "upcoming" |
"running" | "past" ("" = all). Start here, then scope into one
with client.competition(comp.id).
38 def get(self, competition_id: str) -> CompetitionDetail: 39 """Full competition detail including resolved challenge summaries 40 (``.challenges``). Same as ``client.competition(id).detail()``.""" 41 resp = self._http.request("GET", f"/competitions/{competition_id}") 42 _raise_for_status(resp) 43 return CompetitionDetail.model_validate(resp.json())
Full competition detail including resolved challenge summaries
(ctfy.sdk.resources.challenges). Same as client.competition(id).detail().
45 def start_virtual(self, competition_id: str) -> CompetitionInfo: 46 """Replay a finished contest on your own clock. 47 48 Returns the *replay*, which is an ordinary competition you are 49 already registered in — so every other call takes its 50 ``.id`` unchanged: launch instances, submit answers, submit 51 patches, read its board. 52 53 **Idempotent.** Asking twice returns the replay you already 54 started rather than minting a second sitting, so a retry cannot 55 split one attempt across two competitions. 56 57 Refused (400) when a replay is not defined: the source has not 58 finished, has no window, is multi-session, never ran, or is 59 itself a replay. The error code says which. 60 """ 61 resp = self._http.request("POST", f"/competitions/{competition_id}/virtual") 62 _raise_for_status(resp) 63 return CompetitionInfo.model_validate(resp.json())
Replay a finished contest on your own clock.
Returns the replay, which is an ordinary competition you are
already registered in — so every other call takes its
.id unchanged: launch instances, submit answers, submit
patches, read its board.
Idempotent. Asking twice returns the replay you already started rather than minting a second sitting, so a retry cannot split one attempt across two competitions.
Refused (400) when a replay is not defined: the source has not finished, has no window, is multi-session, never ran, or is itself a replay. The error code says which.
65 def ghosts(self, competition_id: str) -> VirtualGhostBoard: 66 """The source contest's standings at the instant this replay has 67 reached, with you merged in and ranked among them. 68 69 Takes the **replay's** id, not the source's. At T+40 minutes 70 into your run you see the contest as it stood at T+40 minutes on 71 the day — which is what makes a replay a race rather than an 72 exercise, since a board holding only your own solves answers 73 nothing. 74 75 Refused (400 ``not_a_virtual_sitting``) on an ordinary 76 competition. A source that has since been deleted costs the 77 ghosts and nothing else: the reply comes back with no items and 78 the run itself keeps working. 79 """ 80 resp = self._http.request("GET", f"/competitions/{competition_id}/ghosts") 81 _raise_for_status(resp) 82 return VirtualGhostBoard.model_validate(resp.json())
The source contest's standings at the instant this replay has reached, with you merged in and ranked among them.
Takes the replay's id, not the source's. At T+40 minutes into your run you see the contest as it stood at T+40 minutes on the day — which is what makes a replay a race rather than an exercise, since a board holding only your own solves answers nothing.
Refused (400 not_a_virtual_sitting) on an ordinary
competition. A source that has since been deleted costs the
ghosts and nothing else: the reply comes back with no items and
the run itself keeps working.