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    ) -> CompetitionInfo:
39        """Create a new competition. ``None`` time fields = no bound
40        (sent as JSON ``null``). Unknown ``challenge_ids`` → 422.
41        ``status`` defaults to ``draft`` (hidden from players); pass
42        ``"published"`` to make it immediately visible. ``access``
43        defaults to ``public``; pass ``"private_listed"`` /
44        ``"private_hidden"`` for an invite-only competition."""
45        resp = self._http.request(
46            "POST",
47            "/admin/competitions",
48            json={
49                "title": title,
50                "description": description,
51                "starts_at": starts_at.isoformat() if starts_at else None,
52                "ends_at": ends_at.isoformat() if ends_at else None,
53                "challenge_ids": challenge_ids or [],
54                "status": status,
55                "access": access,
56            },
57        )
58        _raise_for_status(resp)
59        return CompetitionInfo.model_validate(resp.json())
60
61    def update(self, competition_id: str, **fields: Any) -> CompetitionInfo:
62        """PATCH any subset of ``title`` / ``description`` / ``starts_at`` /
63        ``ends_at`` / ``challenge_ids``. Omit a field to leave it; pass
64        ``None`` to clear."""
65        resp = self._http.request("PATCH", f"/admin/competitions/{competition_id}", json=fields)
66        _raise_for_status(resp)
67        return CompetitionInfo.model_validate(resp.json())
68
69    def delete(self, competition_id: str) -> None:
70        """Hard-delete a competition. Cascades against per-comp teams,
71        memberships, invites, solve / submission rows."""
72        resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}")
73        _raise_for_status(resp)
74
75    def registrations(
76        self,
77        competition_id: str,
78        offset: int = 0,
79        limit: int = 50,
80    ) -> builtins.list[CompetitionRegistrationInfo]:
81        """Roster for ``competition_id``. Per the per-comp refactor, a
82        team's existence with the comp scope IS the registration."""
83        resp = self._http.request(
84            "GET",
85            f"/admin/competitions/{competition_id}/registrations",
86            params={"offset": offset, "limit": limit},
87        )
88        _raise_for_status(resp)
89        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    ) -> CompetitionInfo:
40        """Create a new competition. ``None`` time fields = no bound
41        (sent as JSON ``null``). Unknown ``challenge_ids`` → 422.
42        ``status`` defaults to ``draft`` (hidden from players); pass
43        ``"published"`` to make it immediately visible. ``access``
44        defaults to ``public``; pass ``"private_listed"`` /
45        ``"private_hidden"`` for an invite-only competition."""
46        resp = self._http.request(
47            "POST",
48            "/admin/competitions",
49            json={
50                "title": title,
51                "description": description,
52                "starts_at": starts_at.isoformat() if starts_at else None,
53                "ends_at": ends_at.isoformat() if ends_at else None,
54                "challenge_ids": challenge_ids or [],
55                "status": status,
56                "access": access,
57            },
58        )
59        _raise_for_status(resp)
60        return CompetitionInfo.model_validate(resp.json())
61
62    def update(self, competition_id: str, **fields: Any) -> CompetitionInfo:
63        """PATCH any subset of ``title`` / ``description`` / ``starts_at`` /
64        ``ends_at`` / ``challenge_ids``. Omit a field to leave it; pass
65        ``None`` to clear."""
66        resp = self._http.request("PATCH", f"/admin/competitions/{competition_id}", json=fields)
67        _raise_for_status(resp)
68        return CompetitionInfo.model_validate(resp.json())
69
70    def delete(self, competition_id: str) -> None:
71        """Hard-delete a competition. Cascades against per-comp teams,
72        memberships, invites, solve / submission rows."""
73        resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}")
74        _raise_for_status(resp)
75
76    def registrations(
77        self,
78        competition_id: str,
79        offset: int = 0,
80        limit: int = 50,
81    ) -> builtins.list[CompetitionRegistrationInfo]:
82        """Roster for ``competition_id``. Per the per-comp refactor, a
83        team's existence with the comp scope IS the registration."""
84        resp = self._http.request(
85            "GET",
86            f"/admin/competitions/{competition_id}/registrations",
87            params={"offset": offset, "limit": limit},
88        )
89        _raise_for_status(resp)
90        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') -> 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    ) -> CompetitionInfo:
40        """Create a new competition. ``None`` time fields = no bound
41        (sent as JSON ``null``). Unknown ``challenge_ids`` → 422.
42        ``status`` defaults to ``draft`` (hidden from players); pass
43        ``"published"`` to make it immediately visible. ``access``
44        defaults to ``public``; pass ``"private_listed"`` /
45        ``"private_hidden"`` for an invite-only competition."""
46        resp = self._http.request(
47            "POST",
48            "/admin/competitions",
49            json={
50                "title": title,
51                "description": description,
52                "starts_at": starts_at.isoformat() if starts_at else None,
53                "ends_at": ends_at.isoformat() if ends_at else None,
54                "challenge_ids": challenge_ids or [],
55                "status": status,
56                "access": access,
57            },
58        )
59        _raise_for_status(resp)
60        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.

def update( self, competition_id: str, **fields: Any) -> ctfy.server.models.CompetitionInfo:
62    def update(self, competition_id: str, **fields: Any) -> CompetitionInfo:
63        """PATCH any subset of ``title`` / ``description`` / ``starts_at`` /
64        ``ends_at`` / ``challenge_ids``. Omit a field to leave it; pass
65        ``None`` to clear."""
66        resp = self._http.request("PATCH", f"/admin/competitions/{competition_id}", json=fields)
67        _raise_for_status(resp)
68        return CompetitionInfo.model_validate(resp.json())

PATCH any subset of title / description / starts_at / ends_at / challenge_ids. Omit a field to leave it; pass None to clear.

def delete(self, competition_id: str) -> None:
70    def delete(self, competition_id: str) -> None:
71        """Hard-delete a competition. Cascades against per-comp teams,
72        memberships, invites, solve / submission rows."""
73        resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}")
74        _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]:
76    def registrations(
77        self,
78        competition_id: str,
79        offset: int = 0,
80        limit: int = 50,
81    ) -> builtins.list[CompetitionRegistrationInfo]:
82        """Roster for ``competition_id``. Per the per-comp refactor, a
83        team's existence with the comp scope IS the registration."""
84        resp = self._http.request(
85            "GET",
86            f"/admin/competitions/{competition_id}/registrations",
87            params={"offset": offset, "limit": limit},
88        )
89        _raise_for_status(resp)
90        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.