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