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"]]
class AdminCompetitionsResource:
 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.

AdminCompetitionsResource(http: ctfy.sdk.base.BaseHttpClient)
18    def __init__(self, http: BaseHttpClient) -> None:
19        self._http = http
def list( self, offset: int = 0, limit: int = 50) -> list[ctfy.server.models.CompetitionInfo]:
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.

def create( self, *, title: str, description: str = '', starts_at: datetime.datetime | None = None, ends_at: datetime.datetime | None = None, challenge_ids: list[str] | None = None, status: str = 'draft', access: str = 'public', eval_standard: bool = False, corpus_sha: str = '', scoring_rule: str = '', scoring_params: dict[str, typing.Any] | None = None, sessions: list[tuple[datetime.datetime, datetime.datetime]] | None = None) -> ctfy.server.models.CompetitionInfo:
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.

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

def delete(self, competition_id: str) -> None:
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.

def registrations( self, competition_id: str, offset: int = 0, limit: int = 50) -> list[ctfy.server.models.CompetitionRegistrationInfo]:
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.