ctfy.sdk.admin_resources.competition_invites

client.admin.competition_invites — private-competition participation allowlist (admin).

 1"""``client.admin.competition_invites`` — private-competition
 2participation allowlist (admin)."""
 3
 4from __future__ import annotations
 5
 6from ctfy.sdk._helpers import PagedList, _extract_items, _raise_for_status
 7from ctfy.sdk.base import BaseHttpClient
 8from ctfy.server.models import BulkInviteResponse, CompetitionInviteInfo
 9
10
11class AdminCompetitionInvitesResource:
12    """Invite / revoke users on a private competition's participation
13    allowlist. Managed by any competition admin (global or per-comp)."""
14
15    def __init__(self, http: BaseHttpClient) -> None:
16        self._http = http
17
18    def list(
19        self, competition_id: str, offset: int = 0, limit: int = 50
20    ) -> PagedList[CompetitionInviteInfo]:
21        """Everyone invited to participate in one competition, and how
22        far each got.
23
24        ``status`` is the funnel — ``pending_signup`` (an address nobody
25        has claimed) / ``signed_up`` (an account, but no team for this
26        competition) / ``registered`` (in, with ``team_id`` and
27        ``registered_at`` set). The middle one is what "who did I invite
28        that never turned up" actually means; ``pending_signup`` the
29        boolean is derived from it and kept for older callers.
30
31        A claimed email invitation appears once, on its allowlist row,
32        carrying the *original* invitation's ``invited_at`` /
33        ``invited_by`` and the moment it was claimed as ``signed_up_at``.
34        """
35        resp = self._http.request(
36            "GET",
37            f"/admin/competitions/{competition_id}/invites",
38            params={"offset": offset, "limit": limit},
39        )
40        _raise_for_status(resp)
41        return _extract_items(resp.json(), CompetitionInviteInfo)
42
43    def invite(
44        self, competition_id: str, *, user_id: str = "", email: str = ""
45    ) -> CompetitionInviteInfo:
46        """Invite someone to participate.
47
48        Identify the target by ``user_id`` or ``email``. An ``email``
49        with no account yet is recorded against the address and claimed
50        when its owner first signs in with a verified address — the
51        returned row carries ``pending_signup=True`` in that case. A
52        ``user_id`` that matches nothing is still a 404: an id names an
53        account or nothing.
54        """
55        resp = self._http.request(
56            "POST",
57            f"/admin/competitions/{competition_id}/invites",
58            json={"user_id": user_id, "email": email},
59        )
60        _raise_for_status(resp)
61        return CompetitionInviteInfo.model_validate(resp.json())
62
63    def invite_bulk(self, competition_id: str, emails: str) -> BulkInviteResponse:
64        """Invite a pasted list of addresses in one call.
65
66        ``emails`` is the raw blob — newline / comma / semicolon
67        separated. Splitting happens server-side so every client agrees
68        on the rules. The response classifies each address as
69        ``invited`` / ``pending`` / ``already`` / ``invalid``.
70        """
71        resp = self._http.request(
72            "POST",
73            f"/admin/competitions/{competition_id}/invites/bulk",
74            json={"emails": emails},
75        )
76        _raise_for_status(resp)
77        return BulkInviteResponse.model_validate(resp.json())
78
79    def revoke(self, competition_id: str, user_id: str) -> None:
80        """Remove a user from the participation allowlist."""
81        resp = self._http.request(
82            "DELETE", f"/admin/competitions/{competition_id}/invites/{user_id}"
83        )
84        _raise_for_status(resp)
85
86    def revoke_email(self, competition_id: str, email: str) -> None:
87        """Withdraw an invitation still waiting for its person."""
88        resp = self._http.request(
89            "DELETE", f"/admin/competitions/{competition_id}/invites/by-email/{email}"
90        )
91        _raise_for_status(resp)
class AdminCompetitionInvitesResource:
12class AdminCompetitionInvitesResource:
13    """Invite / revoke users on a private competition's participation
14    allowlist. Managed by any competition admin (global or per-comp)."""
15
16    def __init__(self, http: BaseHttpClient) -> None:
17        self._http = http
18
19    def list(
20        self, competition_id: str, offset: int = 0, limit: int = 50
21    ) -> PagedList[CompetitionInviteInfo]:
22        """Everyone invited to participate in one competition, and how
23        far each got.
24
25        ``status`` is the funnel — ``pending_signup`` (an address nobody
26        has claimed) / ``signed_up`` (an account, but no team for this
27        competition) / ``registered`` (in, with ``team_id`` and
28        ``registered_at`` set). The middle one is what "who did I invite
29        that never turned up" actually means; ``pending_signup`` the
30        boolean is derived from it and kept for older callers.
31
32        A claimed email invitation appears once, on its allowlist row,
33        carrying the *original* invitation's ``invited_at`` /
34        ``invited_by`` and the moment it was claimed as ``signed_up_at``.
35        """
36        resp = self._http.request(
37            "GET",
38            f"/admin/competitions/{competition_id}/invites",
39            params={"offset": offset, "limit": limit},
40        )
41        _raise_for_status(resp)
42        return _extract_items(resp.json(), CompetitionInviteInfo)
43
44    def invite(
45        self, competition_id: str, *, user_id: str = "", email: str = ""
46    ) -> CompetitionInviteInfo:
47        """Invite someone to participate.
48
49        Identify the target by ``user_id`` or ``email``. An ``email``
50        with no account yet is recorded against the address and claimed
51        when its owner first signs in with a verified address — the
52        returned row carries ``pending_signup=True`` in that case. A
53        ``user_id`` that matches nothing is still a 404: an id names an
54        account or nothing.
55        """
56        resp = self._http.request(
57            "POST",
58            f"/admin/competitions/{competition_id}/invites",
59            json={"user_id": user_id, "email": email},
60        )
61        _raise_for_status(resp)
62        return CompetitionInviteInfo.model_validate(resp.json())
63
64    def invite_bulk(self, competition_id: str, emails: str) -> BulkInviteResponse:
65        """Invite a pasted list of addresses in one call.
66
67        ``emails`` is the raw blob — newline / comma / semicolon
68        separated. Splitting happens server-side so every client agrees
69        on the rules. The response classifies each address as
70        ``invited`` / ``pending`` / ``already`` / ``invalid``.
71        """
72        resp = self._http.request(
73            "POST",
74            f"/admin/competitions/{competition_id}/invites/bulk",
75            json={"emails": emails},
76        )
77        _raise_for_status(resp)
78        return BulkInviteResponse.model_validate(resp.json())
79
80    def revoke(self, competition_id: str, user_id: str) -> None:
81        """Remove a user from the participation allowlist."""
82        resp = self._http.request(
83            "DELETE", f"/admin/competitions/{competition_id}/invites/{user_id}"
84        )
85        _raise_for_status(resp)
86
87    def revoke_email(self, competition_id: str, email: str) -> None:
88        """Withdraw an invitation still waiting for its person."""
89        resp = self._http.request(
90            "DELETE", f"/admin/competitions/{competition_id}/invites/by-email/{email}"
91        )
92        _raise_for_status(resp)

Invite / revoke users on a private competition's participation allowlist. Managed by any competition admin (global or per-comp).

AdminCompetitionInvitesResource(http: ctfy.sdk.base.BaseHttpClient)
16    def __init__(self, http: BaseHttpClient) -> None:
17        self._http = http
def list( self, competition_id: str, offset: int = 0, limit: int = 50) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.CompetitionInviteInfo]:
19    def list(
20        self, competition_id: str, offset: int = 0, limit: int = 50
21    ) -> PagedList[CompetitionInviteInfo]:
22        """Everyone invited to participate in one competition, and how
23        far each got.
24
25        ``status`` is the funnel — ``pending_signup`` (an address nobody
26        has claimed) / ``signed_up`` (an account, but no team for this
27        competition) / ``registered`` (in, with ``team_id`` and
28        ``registered_at`` set). The middle one is what "who did I invite
29        that never turned up" actually means; ``pending_signup`` the
30        boolean is derived from it and kept for older callers.
31
32        A claimed email invitation appears once, on its allowlist row,
33        carrying the *original* invitation's ``invited_at`` /
34        ``invited_by`` and the moment it was claimed as ``signed_up_at``.
35        """
36        resp = self._http.request(
37            "GET",
38            f"/admin/competitions/{competition_id}/invites",
39            params={"offset": offset, "limit": limit},
40        )
41        _raise_for_status(resp)
42        return _extract_items(resp.json(), CompetitionInviteInfo)

Everyone invited to participate in one competition, and how far each got.

status is the funnel — pending_signup (an address nobody has claimed) / signed_up (an account, but no team for this competition) / registered (in, with team_id and registered_at set). The middle one is what "who did I invite that never turned up" actually means; pending_signup the boolean is derived from it and kept for older callers.

A claimed email invitation appears once, on its allowlist row, carrying the original invitation's invited_at / invited_by and the moment it was claimed as signed_up_at.

def invite( self, competition_id: str, *, user_id: str = '', email: str = '') -> ctfy.server.models.CompetitionInviteInfo:
44    def invite(
45        self, competition_id: str, *, user_id: str = "", email: str = ""
46    ) -> CompetitionInviteInfo:
47        """Invite someone to participate.
48
49        Identify the target by ``user_id`` or ``email``. An ``email``
50        with no account yet is recorded against the address and claimed
51        when its owner first signs in with a verified address — the
52        returned row carries ``pending_signup=True`` in that case. A
53        ``user_id`` that matches nothing is still a 404: an id names an
54        account or nothing.
55        """
56        resp = self._http.request(
57            "POST",
58            f"/admin/competitions/{competition_id}/invites",
59            json={"user_id": user_id, "email": email},
60        )
61        _raise_for_status(resp)
62        return CompetitionInviteInfo.model_validate(resp.json())

Invite someone to participate.

Identify the target by user_id or email. An email with no account yet is recorded against the address and claimed when its owner first signs in with a verified address — the returned row carries pending_signup=True in that case. A user_id that matches nothing is still a 404: an id names an account or nothing.

def invite_bulk( self, competition_id: str, emails: str) -> ctfy.server.models.BulkInviteResponse:
64    def invite_bulk(self, competition_id: str, emails: str) -> BulkInviteResponse:
65        """Invite a pasted list of addresses in one call.
66
67        ``emails`` is the raw blob — newline / comma / semicolon
68        separated. Splitting happens server-side so every client agrees
69        on the rules. The response classifies each address as
70        ``invited`` / ``pending`` / ``already`` / ``invalid``.
71        """
72        resp = self._http.request(
73            "POST",
74            f"/admin/competitions/{competition_id}/invites/bulk",
75            json={"emails": emails},
76        )
77        _raise_for_status(resp)
78        return BulkInviteResponse.model_validate(resp.json())

Invite a pasted list of addresses in one call.

emails is the raw blob — newline / comma / semicolon separated. Splitting happens server-side so every client agrees on the rules. The response classifies each address as invited / pending / already / invalid.

def revoke(self, competition_id: str, user_id: str) -> None:
80    def revoke(self, competition_id: str, user_id: str) -> None:
81        """Remove a user from the participation allowlist."""
82        resp = self._http.request(
83            "DELETE", f"/admin/competitions/{competition_id}/invites/{user_id}"
84        )
85        _raise_for_status(resp)

Remove a user from the participation allowlist.

def revoke_email(self, competition_id: str, email: str) -> None:
87    def revoke_email(self, competition_id: str, email: str) -> None:
88        """Withdraw an invitation still waiting for its person."""
89        resp = self._http.request(
90            "DELETE", f"/admin/competitions/{competition_id}/invites/by-email/{email}"
91        )
92        _raise_for_status(resp)

Withdraw an invitation still waiting for its person.