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())
class CompetitionsResource:
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).

CompetitionsResource(http: ctfy.sdk.base.BaseHttpClient)
22    def __init__(self, http: BaseHttpClient) -> None:
23        self._http = http
def list( self, phase: str = '', offset: int = 0, limit: int = 50) -> list[ctfy.server.models.CompetitionInfo]:
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).

def get( self, competition_id: str) -> ctfy.server.models.CompetitionDetail:
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().

def start_virtual( self, competition_id: str) -> ctfy.server.models.CompetitionInfo:
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.

def ghosts( self, competition_id: str) -> ctfy.server.models.VirtualGhostBoard:
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.