ctfy.sdk.competition

client.competition(id) — a handle scoped to one competition.

Collects the operations that are inherently competition-scoped — the ones that used to repeat competition_id on every call: registration, your team (captain tooling, invites, join-requests), the competition's challenge list, team search, and standings. competition_id is named once (client.competition(id)).

Discovery of competitions stays on client.competitions (.list() / .get(id)). Instance lifecycle (client.instances) and answer submission (client.submissions) stay flat: they're keyed by instance_id and the server infers the competition, so they don't belong to a comp handle.

comp = client.competition("spring-ctf")
comp.register(mode="solo")
board = comp.scoreboard()
comp.team.invites.create(max_uses=3)
  1"""``client.competition(id)`` — a handle scoped to one competition.
  2
  3Collects the operations that are *inherently* competition-scoped — the ones
  4that used to repeat ``competition_id`` on every call: registration, your team
  5(captain tooling, invites, join-requests), the competition's challenge list,
  6team search, and standings. ``competition_id`` is named once
  7(``client.competition(id)``).
  8
  9Discovery of competitions stays on ``client.competitions`` (``.list()`` /
 10``.get(id)``). Instance lifecycle (``client.instances``) and answer submission
 11(``client.submissions``) stay flat: they're keyed by ``instance_id`` and the
 12server infers the competition, so they don't belong to a comp handle.
 13
 14    comp = client.competition("spring-ctf")
 15    comp.register(mode="solo")
 16    board = comp.scoreboard()
 17    comp.team.invites.create(max_uses=3)
 18"""
 19
 20from __future__ import annotations
 21
 22from functools import cached_property
 23from typing import Any, Literal
 24
 25from ctfy.sdk._helpers import PagedList, _extract_items, _raise_for_status
 26from ctfy.sdk.base import BaseHttpClient
 27from ctfy.sdk.competition.team import CompetitionTeam
 28from ctfy.server.models import (
 29    AdminSolveMatrix,
 30    ChallengeInfo,
 31    CompetitionChallengeBreakdown,
 32    CompetitionDetail,
 33    CompetitionRoster,
 34    CompetitionScoreDistribution,
 35    CompetitionScoreHistory,
 36    ScoreboardEntry,
 37    TeamDetail,
 38    TeamInfo,
 39    TeamInviteInfo,
 40)
 41
 42__all__ = ["Competition"]
 43
 44
 45class Competition:
 46    """A competition-scoped view: registration, your team, the comp's
 47    challenges, team search, and standings — all within one competition."""
 48
 49    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
 50        self._http = http
 51        self._cid = competition_id
 52
 53    @property
 54    def id(self) -> str:
 55        """The competition id this handle is bound to."""
 56        return self._cid
 57
 58    # -- info -------------------------------------------------------------
 59
 60    def detail(self) -> CompetitionDetail:
 61        """Full competition detail including resolved challenge summaries
 62        (``.challenges``) in the admin's curated order."""
 63        resp = self._http.request("GET", f"/competitions/{self._cid}")
 64        _raise_for_status(resp)
 65        return CompetitionDetail.model_validate(resp.json())
 66
 67    def challenges(
 68        self,
 69        difficulty: str | None = None,
 70        tag: str = "",
 71        q: str = "",
 72        offset: int = 0,
 73        limit: int = 50,
 74    ) -> PagedList[ChallengeInfo]:
 75        """Challenges scoped to this competition, curated-order sorted."""
 76        params: dict[str, Any] = {"offset": offset, "limit": limit}
 77        if difficulty:
 78            params["difficulty"] = difficulty
 79        if tag:
 80            params["tag"] = tag
 81        if q:
 82            params["q"] = q
 83        resp = self._http.request("GET", f"/competitions/{self._cid}/challenges", params=params)
 84        _raise_for_status(resp)
 85        return _extract_items(resp.json(), ChallengeInfo)
 86
 87    def search_teams(self, q: str = "", offset: int = 0, limit: int = 50) -> PagedList[TeamInfo]:
 88        """Substring search over team names registered in this competition
 89        (the join-a-team picker)."""
 90        params: dict[str, Any] = {"offset": offset, "limit": limit}
 91        if q:
 92            params["q"] = q
 93        resp = self._http.request("GET", f"/competitions/{self._cid}/teams/search", params=params)
 94        _raise_for_status(resp)
 95        return _extract_items(resp.json(), TeamInfo)
 96
 97    # -- register / join --------------------------------------------------
 98
 99    def register(
100        self,
101        mode: Literal["solo", "create", "join"] = "solo",
102        *,
103        name: str = "",
104        description: str = "",
105        code: str = "",
106    ) -> TeamDetail:
107        """Get on a team for this competition.
108
109        - ``mode="solo"`` — mint a 1-person team auto-named after you
110          (idempotent on (you, competition)).
111        - ``mode="create"`` — mint a multi-user team you captain; requires
112          ``name``. 409 if you already have a team for this comp.
113        - ``mode="join"`` — redeem a captain's invite ``code`` to join their
114          team. 409 if the code was for a different comp or you already have
115          a team here.
116        """
117        if mode == "join":
118            if not code:
119                raise ValueError('register(mode="join") requires a `code`')
120            resp = self._http.request(
121                "POST", f"/competitions/{self._cid}/teams/redeem", json={"code": code}
122            )
123        else:
124            if mode == "create" and not name:
125                raise ValueError('register(mode="create") requires a `name`')
126            body: dict[str, str] = {"mode": mode}
127            if mode == "create":
128                body["name"] = name
129                body["description"] = description
130            resp = self._http.request("POST", f"/competitions/{self._cid}/teams", json=body)
131        _raise_for_status(resp)
132        return TeamDetail.model_validate(resp.json())
133
134    def request_to_join(self, team_id: str) -> TeamInviteInfo:
135        """Ask to join ``team_id`` in this competition. The captain sees it
136        in their inbox and approves / rejects (``team.requests``)."""
137        resp = self._http.request("POST", f"/competitions/{self._cid}/teams/{team_id}/join-request")
138        _raise_for_status(resp)
139        return TeamInviteInfo.model_validate(resp.json())
140
141    def accept_invite(self, invite_id: str) -> TeamDetail:
142        """Accept a direct invite sent to you → join the captain's team."""
143        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/accept")
144        _raise_for_status(resp)
145        return TeamDetail.model_validate(resp.json())
146
147    def decline_invite(self, invite_id: str) -> None:
148        """Decline a direct invite sent to you (only the named target may)."""
149        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/decline")
150        _raise_for_status(resp)
151
152    # -- standings --------------------------------------------------------
153
154    def scoreboard(self, offset: int = 0, limit: int = 50) -> PagedList[ScoreboardEntry]:
155        """Ranked standings for this competition."""
156        resp = self._http.request(
157            "GET",
158            f"/competitions/{self._cid}/scoreboard",
159            params={"offset": offset, "limit": limit},
160        )
161        _raise_for_status(resp)
162        return _extract_items(resp.json(), ScoreboardEntry)
163
164    def score_history(self, *, top: int = 10) -> CompetitionScoreHistory:
165        """Score-progression series for the top ``top`` teams."""
166        resp = self._http.request(
167            "GET", f"/competitions/{self._cid}/score-history", params={"top": top}
168        )
169        _raise_for_status(resp)
170        return CompetitionScoreHistory.model_validate(resp.json())
171
172    def score_distribution(self) -> CompetitionScoreDistribution:
173        """Score histogram buckets for this competition."""
174        resp = self._http.request("GET", f"/competitions/{self._cid}/scoreboard/distribution")
175        _raise_for_status(resp)
176        return CompetitionScoreDistribution.model_validate(resp.json())
177
178    def solve_matrix(self) -> AdminSolveMatrix:
179        """Per-team × per-challenge solve grid scoped to this competition."""
180        resp = self._http.request("GET", f"/competitions/{self._cid}/solve-matrix")
181        _raise_for_status(resp)
182        return AdminSolveMatrix.model_validate(resp.json())
183
184    def roster(self) -> CompetitionRoster:
185        """Every registered team in this competition with its members.
186
187        One request whatever the field size — the per-team
188        :meth:`ctfy.sdk.resources.teams.TeamsResource.get` answers the
189        same question one team at a time, which for a 400-team event is
190        400 round trips. Carries public identity only (display name +
191        avatar), never the entrant dossier.
192        """
193        resp = self._http.request("GET", f"/competitions/{self._cid}/roster")
194        _raise_for_status(resp)
195        return CompetitionRoster.model_validate(resp.json())
196
197    def challenge_breakdown(self) -> CompetitionChallengeBreakdown:
198        """Per-challenge solve / attempt counts for this competition."""
199        resp = self._http.request("GET", f"/competitions/{self._cid}/challenge-breakdown")
200        _raise_for_status(resp)
201        return CompetitionChallengeBreakdown.model_validate(resp.json())
202
203    # -- my team ----------------------------------------------------------
204
205    @cached_property
206    def team(self) -> CompetitionTeam:
207        """My team in this competition (rename / leave / kick + invites + requests)."""
208        return CompetitionTeam(self._http, self._cid)
class Competition:
 46class Competition:
 47    """A competition-scoped view: registration, your team, the comp's
 48    challenges, team search, and standings — all within one competition."""
 49
 50    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
 51        self._http = http
 52        self._cid = competition_id
 53
 54    @property
 55    def id(self) -> str:
 56        """The competition id this handle is bound to."""
 57        return self._cid
 58
 59    # -- info -------------------------------------------------------------
 60
 61    def detail(self) -> CompetitionDetail:
 62        """Full competition detail including resolved challenge summaries
 63        (``.challenges``) in the admin's curated order."""
 64        resp = self._http.request("GET", f"/competitions/{self._cid}")
 65        _raise_for_status(resp)
 66        return CompetitionDetail.model_validate(resp.json())
 67
 68    def challenges(
 69        self,
 70        difficulty: str | None = None,
 71        tag: str = "",
 72        q: str = "",
 73        offset: int = 0,
 74        limit: int = 50,
 75    ) -> PagedList[ChallengeInfo]:
 76        """Challenges scoped to this competition, curated-order sorted."""
 77        params: dict[str, Any] = {"offset": offset, "limit": limit}
 78        if difficulty:
 79            params["difficulty"] = difficulty
 80        if tag:
 81            params["tag"] = tag
 82        if q:
 83            params["q"] = q
 84        resp = self._http.request("GET", f"/competitions/{self._cid}/challenges", params=params)
 85        _raise_for_status(resp)
 86        return _extract_items(resp.json(), ChallengeInfo)
 87
 88    def search_teams(self, q: str = "", offset: int = 0, limit: int = 50) -> PagedList[TeamInfo]:
 89        """Substring search over team names registered in this competition
 90        (the join-a-team picker)."""
 91        params: dict[str, Any] = {"offset": offset, "limit": limit}
 92        if q:
 93            params["q"] = q
 94        resp = self._http.request("GET", f"/competitions/{self._cid}/teams/search", params=params)
 95        _raise_for_status(resp)
 96        return _extract_items(resp.json(), TeamInfo)
 97
 98    # -- register / join --------------------------------------------------
 99
100    def register(
101        self,
102        mode: Literal["solo", "create", "join"] = "solo",
103        *,
104        name: str = "",
105        description: str = "",
106        code: str = "",
107    ) -> TeamDetail:
108        """Get on a team for this competition.
109
110        - ``mode="solo"`` — mint a 1-person team auto-named after you
111          (idempotent on (you, competition)).
112        - ``mode="create"`` — mint a multi-user team you captain; requires
113          ``name``. 409 if you already have a team for this comp.
114        - ``mode="join"`` — redeem a captain's invite ``code`` to join their
115          team. 409 if the code was for a different comp or you already have
116          a team here.
117        """
118        if mode == "join":
119            if not code:
120                raise ValueError('register(mode="join") requires a `code`')
121            resp = self._http.request(
122                "POST", f"/competitions/{self._cid}/teams/redeem", json={"code": code}
123            )
124        else:
125            if mode == "create" and not name:
126                raise ValueError('register(mode="create") requires a `name`')
127            body: dict[str, str] = {"mode": mode}
128            if mode == "create":
129                body["name"] = name
130                body["description"] = description
131            resp = self._http.request("POST", f"/competitions/{self._cid}/teams", json=body)
132        _raise_for_status(resp)
133        return TeamDetail.model_validate(resp.json())
134
135    def request_to_join(self, team_id: str) -> TeamInviteInfo:
136        """Ask to join ``team_id`` in this competition. The captain sees it
137        in their inbox and approves / rejects (``team.requests``)."""
138        resp = self._http.request("POST", f"/competitions/{self._cid}/teams/{team_id}/join-request")
139        _raise_for_status(resp)
140        return TeamInviteInfo.model_validate(resp.json())
141
142    def accept_invite(self, invite_id: str) -> TeamDetail:
143        """Accept a direct invite sent to you → join the captain's team."""
144        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/accept")
145        _raise_for_status(resp)
146        return TeamDetail.model_validate(resp.json())
147
148    def decline_invite(self, invite_id: str) -> None:
149        """Decline a direct invite sent to you (only the named target may)."""
150        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/decline")
151        _raise_for_status(resp)
152
153    # -- standings --------------------------------------------------------
154
155    def scoreboard(self, offset: int = 0, limit: int = 50) -> PagedList[ScoreboardEntry]:
156        """Ranked standings for this competition."""
157        resp = self._http.request(
158            "GET",
159            f"/competitions/{self._cid}/scoreboard",
160            params={"offset": offset, "limit": limit},
161        )
162        _raise_for_status(resp)
163        return _extract_items(resp.json(), ScoreboardEntry)
164
165    def score_history(self, *, top: int = 10) -> CompetitionScoreHistory:
166        """Score-progression series for the top ``top`` teams."""
167        resp = self._http.request(
168            "GET", f"/competitions/{self._cid}/score-history", params={"top": top}
169        )
170        _raise_for_status(resp)
171        return CompetitionScoreHistory.model_validate(resp.json())
172
173    def score_distribution(self) -> CompetitionScoreDistribution:
174        """Score histogram buckets for this competition."""
175        resp = self._http.request("GET", f"/competitions/{self._cid}/scoreboard/distribution")
176        _raise_for_status(resp)
177        return CompetitionScoreDistribution.model_validate(resp.json())
178
179    def solve_matrix(self) -> AdminSolveMatrix:
180        """Per-team × per-challenge solve grid scoped to this competition."""
181        resp = self._http.request("GET", f"/competitions/{self._cid}/solve-matrix")
182        _raise_for_status(resp)
183        return AdminSolveMatrix.model_validate(resp.json())
184
185    def roster(self) -> CompetitionRoster:
186        """Every registered team in this competition with its members.
187
188        One request whatever the field size — the per-team
189        :meth:`ctfy.sdk.resources.teams.TeamsResource.get` answers the
190        same question one team at a time, which for a 400-team event is
191        400 round trips. Carries public identity only (display name +
192        avatar), never the entrant dossier.
193        """
194        resp = self._http.request("GET", f"/competitions/{self._cid}/roster")
195        _raise_for_status(resp)
196        return CompetitionRoster.model_validate(resp.json())
197
198    def challenge_breakdown(self) -> CompetitionChallengeBreakdown:
199        """Per-challenge solve / attempt counts for this competition."""
200        resp = self._http.request("GET", f"/competitions/{self._cid}/challenge-breakdown")
201        _raise_for_status(resp)
202        return CompetitionChallengeBreakdown.model_validate(resp.json())
203
204    # -- my team ----------------------------------------------------------
205
206    @cached_property
207    def team(self) -> CompetitionTeam:
208        """My team in this competition (rename / leave / kick + invites + requests)."""
209        return CompetitionTeam(self._http, self._cid)

A competition-scoped view: registration, your team, the comp's challenges, team search, and standings — all within one competition.

Competition(http: ctfy.sdk.base.BaseHttpClient, competition_id: str)
50    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
51        self._http = http
52        self._cid = competition_id
id: str
54    @property
55    def id(self) -> str:
56        """The competition id this handle is bound to."""
57        return self._cid

The competition id this handle is bound to.

def detail(self) -> ctfy.server.models.CompetitionDetail:
61    def detail(self) -> CompetitionDetail:
62        """Full competition detail including resolved challenge summaries
63        (``.challenges``) in the admin's curated order."""
64        resp = self._http.request("GET", f"/competitions/{self._cid}")
65        _raise_for_status(resp)
66        return CompetitionDetail.model_validate(resp.json())

Full competition detail including resolved challenge summaries (.challenges) in the admin's curated order.

def challenges( self, difficulty: str | None = None, tag: str = '', q: str = '', offset: int = 0, limit: int = 50) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.ChallengeInfo]:
68    def challenges(
69        self,
70        difficulty: str | None = None,
71        tag: str = "",
72        q: str = "",
73        offset: int = 0,
74        limit: int = 50,
75    ) -> PagedList[ChallengeInfo]:
76        """Challenges scoped to this competition, curated-order sorted."""
77        params: dict[str, Any] = {"offset": offset, "limit": limit}
78        if difficulty:
79            params["difficulty"] = difficulty
80        if tag:
81            params["tag"] = tag
82        if q:
83            params["q"] = q
84        resp = self._http.request("GET", f"/competitions/{self._cid}/challenges", params=params)
85        _raise_for_status(resp)
86        return _extract_items(resp.json(), ChallengeInfo)

Challenges scoped to this competition, curated-order sorted.

def search_teams( self, q: str = '', offset: int = 0, limit: int = 50) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.TeamInfo]:
88    def search_teams(self, q: str = "", offset: int = 0, limit: int = 50) -> PagedList[TeamInfo]:
89        """Substring search over team names registered in this competition
90        (the join-a-team picker)."""
91        params: dict[str, Any] = {"offset": offset, "limit": limit}
92        if q:
93            params["q"] = q
94        resp = self._http.request("GET", f"/competitions/{self._cid}/teams/search", params=params)
95        _raise_for_status(resp)
96        return _extract_items(resp.json(), TeamInfo)

Substring search over team names registered in this competition (the join-a-team picker).

def register( self, mode: Literal['solo', 'create', 'join'] = 'solo', *, name: str = '', description: str = '', code: str = '') -> ctfy.server.models.TeamDetail:
100    def register(
101        self,
102        mode: Literal["solo", "create", "join"] = "solo",
103        *,
104        name: str = "",
105        description: str = "",
106        code: str = "",
107    ) -> TeamDetail:
108        """Get on a team for this competition.
109
110        - ``mode="solo"`` — mint a 1-person team auto-named after you
111          (idempotent on (you, competition)).
112        - ``mode="create"`` — mint a multi-user team you captain; requires
113          ``name``. 409 if you already have a team for this comp.
114        - ``mode="join"`` — redeem a captain's invite ``code`` to join their
115          team. 409 if the code was for a different comp or you already have
116          a team here.
117        """
118        if mode == "join":
119            if not code:
120                raise ValueError('register(mode="join") requires a `code`')
121            resp = self._http.request(
122                "POST", f"/competitions/{self._cid}/teams/redeem", json={"code": code}
123            )
124        else:
125            if mode == "create" and not name:
126                raise ValueError('register(mode="create") requires a `name`')
127            body: dict[str, str] = {"mode": mode}
128            if mode == "create":
129                body["name"] = name
130                body["description"] = description
131            resp = self._http.request("POST", f"/competitions/{self._cid}/teams", json=body)
132        _raise_for_status(resp)
133        return TeamDetail.model_validate(resp.json())

Get on a team for this competition.

  • mode="solo" — mint a 1-person team auto-named after you (idempotent on (you, competition)).
  • mode="create" — mint a multi-user team you captain; requires name. 409 if you already have a team for this comp.
  • mode="join" — redeem a captain's invite code to join their team. 409 if the code was for a different comp or you already have a team here.
def request_to_join(self, team_id: str) -> ctfy.server.models.TeamInviteInfo:
135    def request_to_join(self, team_id: str) -> TeamInviteInfo:
136        """Ask to join ``team_id`` in this competition. The captain sees it
137        in their inbox and approves / rejects (``team.requests``)."""
138        resp = self._http.request("POST", f"/competitions/{self._cid}/teams/{team_id}/join-request")
139        _raise_for_status(resp)
140        return TeamInviteInfo.model_validate(resp.json())

Ask to join team_id in this competition. The captain sees it in their inbox and approves / rejects (team.requests).

def accept_invite(self, invite_id: str) -> ctfy.server.models.TeamDetail:
142    def accept_invite(self, invite_id: str) -> TeamDetail:
143        """Accept a direct invite sent to you → join the captain's team."""
144        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/accept")
145        _raise_for_status(resp)
146        return TeamDetail.model_validate(resp.json())

Accept a direct invite sent to you → join the captain's team.

def decline_invite(self, invite_id: str) -> None:
148    def decline_invite(self, invite_id: str) -> None:
149        """Decline a direct invite sent to you (only the named target may)."""
150        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/decline")
151        _raise_for_status(resp)

Decline a direct invite sent to you (only the named target may).

def scoreboard( self, offset: int = 0, limit: int = 50) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.ScoreboardEntry]:
155    def scoreboard(self, offset: int = 0, limit: int = 50) -> PagedList[ScoreboardEntry]:
156        """Ranked standings for this competition."""
157        resp = self._http.request(
158            "GET",
159            f"/competitions/{self._cid}/scoreboard",
160            params={"offset": offset, "limit": limit},
161        )
162        _raise_for_status(resp)
163        return _extract_items(resp.json(), ScoreboardEntry)

Ranked standings for this competition.

def score_history( self, *, top: int = 10) -> ctfy.server.models.CompetitionScoreHistory:
165    def score_history(self, *, top: int = 10) -> CompetitionScoreHistory:
166        """Score-progression series for the top ``top`` teams."""
167        resp = self._http.request(
168            "GET", f"/competitions/{self._cid}/score-history", params={"top": top}
169        )
170        _raise_for_status(resp)
171        return CompetitionScoreHistory.model_validate(resp.json())

Score-progression series for the top top teams.

def score_distribution(self) -> ctfy.server.models.CompetitionScoreDistribution:
173    def score_distribution(self) -> CompetitionScoreDistribution:
174        """Score histogram buckets for this competition."""
175        resp = self._http.request("GET", f"/competitions/{self._cid}/scoreboard/distribution")
176        _raise_for_status(resp)
177        return CompetitionScoreDistribution.model_validate(resp.json())

Score histogram buckets for this competition.

def solve_matrix(self) -> ctfy.server.models.AdminSolveMatrix:
179    def solve_matrix(self) -> AdminSolveMatrix:
180        """Per-team × per-challenge solve grid scoped to this competition."""
181        resp = self._http.request("GET", f"/competitions/{self._cid}/solve-matrix")
182        _raise_for_status(resp)
183        return AdminSolveMatrix.model_validate(resp.json())

Per-team × per-challenge solve grid scoped to this competition.

def roster(self) -> ctfy.server.models.CompetitionRoster:
185    def roster(self) -> CompetitionRoster:
186        """Every registered team in this competition with its members.
187
188        One request whatever the field size — the per-team
189        :meth:`ctfy.sdk.resources.teams.TeamsResource.get` answers the
190        same question one team at a time, which for a 400-team event is
191        400 round trips. Carries public identity only (display name +
192        avatar), never the entrant dossier.
193        """
194        resp = self._http.request("GET", f"/competitions/{self._cid}/roster")
195        _raise_for_status(resp)
196        return CompetitionRoster.model_validate(resp.json())

Every registered team in this competition with its members.

One request whatever the field size — the per-team ctfy.sdk.resources.teams.TeamsResource.get() answers the same question one team at a time, which for a 400-team event is 400 round trips. Carries public identity only (display name + avatar), never the entrant dossier.

def challenge_breakdown(self) -> ctfy.server.models.CompetitionChallengeBreakdown:
198    def challenge_breakdown(self) -> CompetitionChallengeBreakdown:
199        """Per-challenge solve / attempt counts for this competition."""
200        resp = self._http.request("GET", f"/competitions/{self._cid}/challenge-breakdown")
201        _raise_for_status(resp)
202        return CompetitionChallengeBreakdown.model_validate(resp.json())

Per-challenge solve / attempt counts for this competition.

206    @cached_property
207    def team(self) -> CompetitionTeam:
208        """My team in this competition (rename / leave / kick + invites + requests)."""
209        return CompetitionTeam(self._http, self._cid)

My team in this competition (rename / leave / kick + invites + requests).