ctfy.sdk.competition.team

client.competition(id)ctfy.sdk.competition.team — my team within one competition.

Captain + member management for the caller's team in a single competition: rename / leave / kick, plus the outgoing invite codes (team.invites) and the incoming join-request queue (team.requests).

  1"""``client.competition(id).team`` — my team within one competition.
  2
  3Captain + member management for the caller's team in a single competition:
  4rename / leave / kick, plus the outgoing invite codes (``team.invites``) and
  5the incoming join-request queue (``team.requests``).
  6"""
  7
  8from __future__ import annotations
  9
 10import builtins
 11from functools import cached_property
 12
 13from ctfy.sdk._helpers import _extract_items, _raise_for_status
 14from ctfy.sdk.base import BaseHttpClient
 15from ctfy.server.models import TeamDetail, TeamInviteInfo
 16
 17
 18class CompetitionTeam:
 19    """The caller's team in this competition."""
 20
 21    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
 22        self._http = http
 23        self._cid = competition_id
 24
 25    def rename(self, *, name: str | None = None, description: str | None = None) -> TeamDetail:
 26        """Captain-only: rename / re-describe the team."""
 27        body: dict[str, str] = {}
 28        if name is not None:
 29            body["name"] = name
 30        if description is not None:
 31            body["description"] = description
 32        resp = self._http.request("PATCH", f"/competitions/{self._cid}/team", json=body)
 33        _raise_for_status(resp)
 34        return TeamDetail.model_validate(resp.json())
 35
 36    def leave(self) -> None:
 37        """Drop your membership for this competition. If captain and others
 38        remain, captaincy transfers to the longest-tenured member; if you
 39        were the lone member the team is deleted."""
 40        resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave")
 41        _raise_for_status(resp)
 42
 43    def kick(self, user_id: str) -> None:
 44        """Captain-only: remove ``user_id`` from the team. Self-kick is
 45        rejected (use :meth:`leave` instead)."""
 46        resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}")
 47        _raise_for_status(resp)
 48
 49    @cached_property
 50    def invites(self) -> CompetitionInvites:
 51        """Invite codes + direct email invites the captain hands out."""
 52        return CompetitionInvites(self._http, self._cid)
 53
 54    @cached_property
 55    def requests(self) -> CompetitionRequests:
 56        """Pending join requests the captain approves / rejects."""
 57        return CompetitionRequests(self._http, self._cid)
 58
 59
 60class CompetitionInvites:
 61    """Captain-only: the team's outgoing invites for this competition."""
 62
 63    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
 64        self._http = http
 65        self._cid = competition_id
 66
 67    def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo:
 68        """Mint an invite code scoped to this comp. ``max_uses=0`` is
 69        unlimited; ``ttl_seconds=0`` never expires. Others redeem it via
 70        ``client.competition(id).register(mode="join", code=…)``."""
 71        resp = self._http.request(
 72            "POST",
 73            f"/competitions/{self._cid}/invites",
 74            json={"max_uses": max_uses, "ttl_seconds": ttl_seconds},
 75        )
 76        _raise_for_status(resp)
 77        return TeamInviteInfo.model_validate(resp.json())
 78
 79    def list(self) -> builtins.list[TeamInviteInfo]:
 80        """All outstanding invites for the caller's team in this comp."""
 81        resp = self._http.request("GET", f"/competitions/{self._cid}/invites")
 82        _raise_for_status(resp)
 83        return _extract_items(resp.json(), TeamInviteInfo)
 84
 85    def revoke(self, invite_id: str) -> None:
 86        """Invalidate a previously-minted invite."""
 87        resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}")
 88        _raise_for_status(resp)
 89
 90    def send(self, email: str) -> TeamInviteInfo:
 91        """Mint an invite addressed to one exact email. The lookup is
 92        case-insensitive but otherwise unforgiving (no fuzzy match, no
 93        enumeration vectors). The recipient accepts via
 94        :meth:`Competition.accept_invite`."""
 95        resp = self._http.request(
 96            "POST",
 97            f"/competitions/{self._cid}/invites/direct",
 98            json={"email": email},
 99        )
100        _raise_for_status(resp)
101        return TeamInviteInfo.model_validate(resp.json())
102
103
104class CompetitionRequests:
105    """Captain-only: incoming join requests for this competition."""
106
107    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
108        self._http = http
109        self._cid = competition_id
110
111    def approve(self, invite_id: str) -> TeamDetail:
112        """Approve a pending join request → the requester joins the team."""
113        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve")
114        _raise_for_status(resp)
115        return TeamDetail.model_validate(resp.json())
116
117    def reject(self, invite_id: str) -> None:
118        """Reject a pending join request → invite revoked. The requester
119        can re-open a fresh request afterwards."""
120        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject")
121        _raise_for_status(resp)
class CompetitionTeam:
19class CompetitionTeam:
20    """The caller's team in this competition."""
21
22    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
23        self._http = http
24        self._cid = competition_id
25
26    def rename(self, *, name: str | None = None, description: str | None = None) -> TeamDetail:
27        """Captain-only: rename / re-describe the team."""
28        body: dict[str, str] = {}
29        if name is not None:
30            body["name"] = name
31        if description is not None:
32            body["description"] = description
33        resp = self._http.request("PATCH", f"/competitions/{self._cid}/team", json=body)
34        _raise_for_status(resp)
35        return TeamDetail.model_validate(resp.json())
36
37    def leave(self) -> None:
38        """Drop your membership for this competition. If captain and others
39        remain, captaincy transfers to the longest-tenured member; if you
40        were the lone member the team is deleted."""
41        resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave")
42        _raise_for_status(resp)
43
44    def kick(self, user_id: str) -> None:
45        """Captain-only: remove ``user_id`` from the team. Self-kick is
46        rejected (use :meth:`leave` instead)."""
47        resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}")
48        _raise_for_status(resp)
49
50    @cached_property
51    def invites(self) -> CompetitionInvites:
52        """Invite codes + direct email invites the captain hands out."""
53        return CompetitionInvites(self._http, self._cid)
54
55    @cached_property
56    def requests(self) -> CompetitionRequests:
57        """Pending join requests the captain approves / rejects."""
58        return CompetitionRequests(self._http, self._cid)

The caller's team in this competition.

CompetitionTeam(http: ctfy.sdk.base.BaseHttpClient, competition_id: str)
22    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
23        self._http = http
24        self._cid = competition_id
def rename( self, *, name: str | None = None, description: str | None = None) -> ctfy.server.models.TeamDetail:
26    def rename(self, *, name: str | None = None, description: str | None = None) -> TeamDetail:
27        """Captain-only: rename / re-describe the team."""
28        body: dict[str, str] = {}
29        if name is not None:
30            body["name"] = name
31        if description is not None:
32            body["description"] = description
33        resp = self._http.request("PATCH", f"/competitions/{self._cid}/team", json=body)
34        _raise_for_status(resp)
35        return TeamDetail.model_validate(resp.json())

Captain-only: rename / re-describe the team.

def leave(self) -> None:
37    def leave(self) -> None:
38        """Drop your membership for this competition. If captain and others
39        remain, captaincy transfers to the longest-tenured member; if you
40        were the lone member the team is deleted."""
41        resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave")
42        _raise_for_status(resp)

Drop your membership for this competition. If captain and others remain, captaincy transfers to the longest-tenured member; if you were the lone member the team is deleted.

def kick(self, user_id: str) -> None:
44    def kick(self, user_id: str) -> None:
45        """Captain-only: remove ``user_id`` from the team. Self-kick is
46        rejected (use :meth:`leave` instead)."""
47        resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}")
48        _raise_for_status(resp)

Captain-only: remove user_id from the team. Self-kick is rejected (use leave() instead).

invites: CompetitionInvites
50    @cached_property
51    def invites(self) -> CompetitionInvites:
52        """Invite codes + direct email invites the captain hands out."""
53        return CompetitionInvites(self._http, self._cid)

Invite codes + direct email invites the captain hands out.

requests: CompetitionRequests
55    @cached_property
56    def requests(self) -> CompetitionRequests:
57        """Pending join requests the captain approves / rejects."""
58        return CompetitionRequests(self._http, self._cid)

Pending join requests the captain approves / rejects.

class CompetitionInvites:
 61class CompetitionInvites:
 62    """Captain-only: the team's outgoing invites for this competition."""
 63
 64    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
 65        self._http = http
 66        self._cid = competition_id
 67
 68    def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo:
 69        """Mint an invite code scoped to this comp. ``max_uses=0`` is
 70        unlimited; ``ttl_seconds=0`` never expires. Others redeem it via
 71        ``client.competition(id).register(mode="join", code=…)``."""
 72        resp = self._http.request(
 73            "POST",
 74            f"/competitions/{self._cid}/invites",
 75            json={"max_uses": max_uses, "ttl_seconds": ttl_seconds},
 76        )
 77        _raise_for_status(resp)
 78        return TeamInviteInfo.model_validate(resp.json())
 79
 80    def list(self) -> builtins.list[TeamInviteInfo]:
 81        """All outstanding invites for the caller's team in this comp."""
 82        resp = self._http.request("GET", f"/competitions/{self._cid}/invites")
 83        _raise_for_status(resp)
 84        return _extract_items(resp.json(), TeamInviteInfo)
 85
 86    def revoke(self, invite_id: str) -> None:
 87        """Invalidate a previously-minted invite."""
 88        resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}")
 89        _raise_for_status(resp)
 90
 91    def send(self, email: str) -> TeamInviteInfo:
 92        """Mint an invite addressed to one exact email. The lookup is
 93        case-insensitive but otherwise unforgiving (no fuzzy match, no
 94        enumeration vectors). The recipient accepts via
 95        :meth:`Competition.accept_invite`."""
 96        resp = self._http.request(
 97            "POST",
 98            f"/competitions/{self._cid}/invites/direct",
 99            json={"email": email},
100        )
101        _raise_for_status(resp)
102        return TeamInviteInfo.model_validate(resp.json())

Captain-only: the team's outgoing invites for this competition.

CompetitionInvites(http: ctfy.sdk.base.BaseHttpClient, competition_id: str)
64    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
65        self._http = http
66        self._cid = competition_id
def create( self, *, max_uses: int = 1, ttl_seconds: int = 86400) -> ctfy.server.models.TeamInviteInfo:
68    def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo:
69        """Mint an invite code scoped to this comp. ``max_uses=0`` is
70        unlimited; ``ttl_seconds=0`` never expires. Others redeem it via
71        ``client.competition(id).register(mode="join", code=…)``."""
72        resp = self._http.request(
73            "POST",
74            f"/competitions/{self._cid}/invites",
75            json={"max_uses": max_uses, "ttl_seconds": ttl_seconds},
76        )
77        _raise_for_status(resp)
78        return TeamInviteInfo.model_validate(resp.json())

Mint an invite code scoped to this comp. max_uses=0 is unlimited; ttl_seconds=0 never expires. Others redeem it via client.competition(id).register(mode="join", code=…).

def list(self) -> list[ctfy.server.models.TeamInviteInfo]:
80    def list(self) -> builtins.list[TeamInviteInfo]:
81        """All outstanding invites for the caller's team in this comp."""
82        resp = self._http.request("GET", f"/competitions/{self._cid}/invites")
83        _raise_for_status(resp)
84        return _extract_items(resp.json(), TeamInviteInfo)

All outstanding invites for the caller's team in this comp.

def revoke(self, invite_id: str) -> None:
86    def revoke(self, invite_id: str) -> None:
87        """Invalidate a previously-minted invite."""
88        resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}")
89        _raise_for_status(resp)

Invalidate a previously-minted invite.

def send(self, email: str) -> ctfy.server.models.TeamInviteInfo:
 91    def send(self, email: str) -> TeamInviteInfo:
 92        """Mint an invite addressed to one exact email. The lookup is
 93        case-insensitive but otherwise unforgiving (no fuzzy match, no
 94        enumeration vectors). The recipient accepts via
 95        :meth:`Competition.accept_invite`."""
 96        resp = self._http.request(
 97            "POST",
 98            f"/competitions/{self._cid}/invites/direct",
 99            json={"email": email},
100        )
101        _raise_for_status(resp)
102        return TeamInviteInfo.model_validate(resp.json())

Mint an invite addressed to one exact email. The lookup is case-insensitive but otherwise unforgiving (no fuzzy match, no enumeration vectors). The recipient accepts via Competition.accept_invite().

class CompetitionRequests:
105class CompetitionRequests:
106    """Captain-only: incoming join requests for this competition."""
107
108    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
109        self._http = http
110        self._cid = competition_id
111
112    def approve(self, invite_id: str) -> TeamDetail:
113        """Approve a pending join request → the requester joins the team."""
114        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve")
115        _raise_for_status(resp)
116        return TeamDetail.model_validate(resp.json())
117
118    def reject(self, invite_id: str) -> None:
119        """Reject a pending join request → invite revoked. The requester
120        can re-open a fresh request afterwards."""
121        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject")
122        _raise_for_status(resp)

Captain-only: incoming join requests for this competition.

CompetitionRequests(http: ctfy.sdk.base.BaseHttpClient, competition_id: str)
108    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
109        self._http = http
110        self._cid = competition_id
def approve(self, invite_id: str) -> ctfy.server.models.TeamDetail:
112    def approve(self, invite_id: str) -> TeamDetail:
113        """Approve a pending join request → the requester joins the team."""
114        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve")
115        _raise_for_status(resp)
116        return TeamDetail.model_validate(resp.json())

Approve a pending join request → the requester joins the team.

def reject(self, invite_id: str) -> None:
118    def reject(self, invite_id: str) -> None:
119        """Reject a pending join request → invite revoked. The requester
120        can re-open a fresh request afterwards."""
121        resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject")
122        _raise_for_status(resp)

Reject a pending join request → invite revoked. The requester can re-open a fresh request afterwards.