ctfy.sdk.admin_resources.exclusion_groups

client.admin.exclusion_groups -- registration exclusion groups (#2301).

  1"""``client.admin.exclusion_groups`` -- registration exclusion groups (#2301)."""
  2
  3from __future__ import annotations
  4
  5import builtins
  6from datetime import datetime
  7
  8from ctfy.sdk._helpers import _raise_for_status
  9from ctfy.sdk.base import BaseHttpClient
 10from ctfy.server.models import (
 11    CompetitionGroupRef,
 12    ExclusionConflictReport,
 13    ExclusionGroupInfo,
 14    ExclusionGroupPage,
 15    ExclusionNoticeChannel,
 16    ExclusionNoticeResult,
 17)
 18
 19
 20class AdminExclusionGroupsResource:
 21    """Sets of competitions in which one person may hold a team in only one.
 22
 23    A person who holds a team in one member is refused (409
 24    ``registration_exclusive``) at every door of every other member --
 25    self-registration, joining a team, accepting an invitation, an approved
 26    join request and the organiser's own add. Organisers are not exempt.
 27    Leaving frees the person; once the group's competitions have started a
 28    switch is refused unless the group sets ``switch_after_start``.
 29
 30    A competition may be in several groups (#2882); exclusion is pairwise
 31    within each group, so two competitions that share no group never refuse
 32    each other. Creating or editing one group never moves a competition out of
 33    another.
 34
 35    Platform administrators only: a group changes who may register in several
 36    events at once.
 37    """
 38
 39    def __init__(self, http: BaseHttpClient) -> None:
 40        self._http = http
 41
 42    def list(self) -> ExclusionGroupPage:
 43        """Every group with its member competitions (unwindowed)."""
 44        resp = self._http.request("GET", "/admin/exclusion-groups")
 45        _raise_for_status(resp)
 46        return ExclusionGroupPage.model_validate(resp.json())
 47
 48    def get(self, group_id: str) -> ExclusionGroupInfo:
 49        resp = self._http.request("GET", f"/admin/exclusion-groups/{group_id}")
 50        _raise_for_status(resp)
 51        return ExclusionGroupInfo.model_validate(resp.json())
 52
 53    def create(
 54        self,
 55        name: str,
 56        *,
 57        competition_ids: builtins.list[str] | None = None,
 58        switch_after_start: bool = False,
 59    ) -> ExclusionGroupInfo:
 60        """Create a group. A competition named here keeps every group it was already in."""
 61        resp = self._http.request(
 62            "POST",
 63            "/admin/exclusion-groups",
 64            json={
 65                "name": name,
 66                "competition_ids": competition_ids or [],
 67                "switch_after_start": switch_after_start,
 68            },
 69        )
 70        _raise_for_status(resp)
 71        return ExclusionGroupInfo.model_validate(resp.json())
 72
 73    def update(
 74        self,
 75        group_id: str,
 76        *,
 77        name: str | None = None,
 78        competition_ids: builtins.list[str] | None = None,
 79        switch_after_start: bool | None = None,
 80    ) -> ExclusionGroupInfo:
 81        """Edit a group. Partial: what you leave out is left alone."""
 82        body: dict[str, object] = {}
 83        if name is not None:
 84            body["name"] = name
 85        if competition_ids is not None:
 86            body["competition_ids"] = competition_ids
 87        if switch_after_start is not None:
 88            body["switch_after_start"] = switch_after_start
 89        resp = self._http.request("PATCH", f"/admin/exclusion-groups/{group_id}", json=body)
 90        _raise_for_status(resp)
 91        return ExclusionGroupInfo.model_validate(resp.json())
 92
 93    def delete(self, group_id: str) -> ExclusionGroupInfo:
 94        """Dissolve a group, answering with the row removed. Its competitions stay."""
 95        resp = self._http.request("DELETE", f"/admin/exclusion-groups/{group_id}")
 96        _raise_for_status(resp)
 97        return ExclusionGroupInfo.model_validate(resp.json())
 98
 99    def competition_groups(self, competition_id: str) -> builtins.list[str]:
100        """The groups a competition is in, by id; ``[]`` is none."""
101        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/exclusion-group")
102        _raise_for_status(resp)
103        return CompetitionGroupRef.model_validate(resp.json()).group_ids
104
105    def set_competition_groups(
106        self, competition_id: str, group_ids: builtins.list[str]
107    ) -> builtins.list[str]:
108        """Make ``group_ids`` exactly the groups a competition is in (``[]``: none).
109
110        Only this competition's memberships change; every other member of each
111        group stays where it was.
112        """
113        resp = self._http.request(
114            "PUT",
115            f"/admin/competitions/{competition_id}/exclusion-group",
116            json={"group_ids": group_ids},
117        )
118        _raise_for_status(resp)
119        return CompetitionGroupRef.model_validate(resp.json()).group_ids
120
121    def conflicts(self, *, group_id: str = "") -> ExclusionConflictReport:
122        """Accounts holding seats in two members of **one** group right now
123        (#2931), per group, with each seat's competition, team, teammates and
124        review state. Never over the union of groups."""
125        params = {"group_id": group_id} if group_id else None
126        resp = self._http.request("GET", "/admin/exclusion-groups/conflicts", params=params)
127        _raise_for_status(resp)
128        return ExclusionConflictReport.model_validate(resp.json())
129
130    def notify_conflicts(
131        self,
132        *,
133        deadline: datetime | None = None,
134        group_id: str = "",
135        team_ids: builtins.list[str] | None = None,
136        channels: builtins.list[ExclusionNoticeChannel] | None = None,
137        dry_run: bool = True,
138    ) -> ExclusionNoticeResult:
139        """Tell every member of each conflicting team to withdraw from one
140        event by ``deadline``, on ``channels`` (default: inbox and email),
141        once per team crossing per channel.
142
143        **Dry run unless ``dry_run=False``.** ``deadline`` defaults to the
144        ``exclusion_conflict_deadline`` setting; it must name its offset, and
145        only a dry run may go without one.
146        """
147        body: dict[str, object] = {"group_id": group_id, "dry_run": dry_run}
148        if channels is not None:
149            body["channels"] = channels
150        if deadline is not None:
151            body["deadline"] = deadline.isoformat()
152        if team_ids is not None:
153            body["team_ids"] = team_ids
154        resp = self._http.request("POST", "/admin/exclusion-groups/conflicts/notify", json=body)
155        _raise_for_status(resp)
156        return ExclusionNoticeResult.model_validate(resp.json())
157
158
159__all__: builtins.list[str] = ["AdminExclusionGroupsResource"]
class AdminExclusionGroupsResource:
 21class AdminExclusionGroupsResource:
 22    """Sets of competitions in which one person may hold a team in only one.
 23
 24    A person who holds a team in one member is refused (409
 25    ``registration_exclusive``) at every door of every other member --
 26    self-registration, joining a team, accepting an invitation, an approved
 27    join request and the organiser's own add. Organisers are not exempt.
 28    Leaving frees the person; once the group's competitions have started a
 29    switch is refused unless the group sets ``switch_after_start``.
 30
 31    A competition may be in several groups (#2882); exclusion is pairwise
 32    within each group, so two competitions that share no group never refuse
 33    each other. Creating or editing one group never moves a competition out of
 34    another.
 35
 36    Platform administrators only: a group changes who may register in several
 37    events at once.
 38    """
 39
 40    def __init__(self, http: BaseHttpClient) -> None:
 41        self._http = http
 42
 43    def list(self) -> ExclusionGroupPage:
 44        """Every group with its member competitions (unwindowed)."""
 45        resp = self._http.request("GET", "/admin/exclusion-groups")
 46        _raise_for_status(resp)
 47        return ExclusionGroupPage.model_validate(resp.json())
 48
 49    def get(self, group_id: str) -> ExclusionGroupInfo:
 50        resp = self._http.request("GET", f"/admin/exclusion-groups/{group_id}")
 51        _raise_for_status(resp)
 52        return ExclusionGroupInfo.model_validate(resp.json())
 53
 54    def create(
 55        self,
 56        name: str,
 57        *,
 58        competition_ids: builtins.list[str] | None = None,
 59        switch_after_start: bool = False,
 60    ) -> ExclusionGroupInfo:
 61        """Create a group. A competition named here keeps every group it was already in."""
 62        resp = self._http.request(
 63            "POST",
 64            "/admin/exclusion-groups",
 65            json={
 66                "name": name,
 67                "competition_ids": competition_ids or [],
 68                "switch_after_start": switch_after_start,
 69            },
 70        )
 71        _raise_for_status(resp)
 72        return ExclusionGroupInfo.model_validate(resp.json())
 73
 74    def update(
 75        self,
 76        group_id: str,
 77        *,
 78        name: str | None = None,
 79        competition_ids: builtins.list[str] | None = None,
 80        switch_after_start: bool | None = None,
 81    ) -> ExclusionGroupInfo:
 82        """Edit a group. Partial: what you leave out is left alone."""
 83        body: dict[str, object] = {}
 84        if name is not None:
 85            body["name"] = name
 86        if competition_ids is not None:
 87            body["competition_ids"] = competition_ids
 88        if switch_after_start is not None:
 89            body["switch_after_start"] = switch_after_start
 90        resp = self._http.request("PATCH", f"/admin/exclusion-groups/{group_id}", json=body)
 91        _raise_for_status(resp)
 92        return ExclusionGroupInfo.model_validate(resp.json())
 93
 94    def delete(self, group_id: str) -> ExclusionGroupInfo:
 95        """Dissolve a group, answering with the row removed. Its competitions stay."""
 96        resp = self._http.request("DELETE", f"/admin/exclusion-groups/{group_id}")
 97        _raise_for_status(resp)
 98        return ExclusionGroupInfo.model_validate(resp.json())
 99
100    def competition_groups(self, competition_id: str) -> builtins.list[str]:
101        """The groups a competition is in, by id; ``[]`` is none."""
102        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/exclusion-group")
103        _raise_for_status(resp)
104        return CompetitionGroupRef.model_validate(resp.json()).group_ids
105
106    def set_competition_groups(
107        self, competition_id: str, group_ids: builtins.list[str]
108    ) -> builtins.list[str]:
109        """Make ``group_ids`` exactly the groups a competition is in (``[]``: none).
110
111        Only this competition's memberships change; every other member of each
112        group stays where it was.
113        """
114        resp = self._http.request(
115            "PUT",
116            f"/admin/competitions/{competition_id}/exclusion-group",
117            json={"group_ids": group_ids},
118        )
119        _raise_for_status(resp)
120        return CompetitionGroupRef.model_validate(resp.json()).group_ids
121
122    def conflicts(self, *, group_id: str = "") -> ExclusionConflictReport:
123        """Accounts holding seats in two members of **one** group right now
124        (#2931), per group, with each seat's competition, team, teammates and
125        review state. Never over the union of groups."""
126        params = {"group_id": group_id} if group_id else None
127        resp = self._http.request("GET", "/admin/exclusion-groups/conflicts", params=params)
128        _raise_for_status(resp)
129        return ExclusionConflictReport.model_validate(resp.json())
130
131    def notify_conflicts(
132        self,
133        *,
134        deadline: datetime | None = None,
135        group_id: str = "",
136        team_ids: builtins.list[str] | None = None,
137        channels: builtins.list[ExclusionNoticeChannel] | None = None,
138        dry_run: bool = True,
139    ) -> ExclusionNoticeResult:
140        """Tell every member of each conflicting team to withdraw from one
141        event by ``deadline``, on ``channels`` (default: inbox and email),
142        once per team crossing per channel.
143
144        **Dry run unless ``dry_run=False``.** ``deadline`` defaults to the
145        ``exclusion_conflict_deadline`` setting; it must name its offset, and
146        only a dry run may go without one.
147        """
148        body: dict[str, object] = {"group_id": group_id, "dry_run": dry_run}
149        if channels is not None:
150            body["channels"] = channels
151        if deadline is not None:
152            body["deadline"] = deadline.isoformat()
153        if team_ids is not None:
154            body["team_ids"] = team_ids
155        resp = self._http.request("POST", "/admin/exclusion-groups/conflicts/notify", json=body)
156        _raise_for_status(resp)
157        return ExclusionNoticeResult.model_validate(resp.json())

Sets of competitions in which one person may hold a team in only one.

A person who holds a team in one member is refused (409 registration_exclusive) at every door of every other member -- self-registration, joining a team, accepting an invitation, an approved join request and the organiser's own add. Organisers are not exempt. Leaving frees the person; once the group's competitions have started a switch is refused unless the group sets switch_after_start.

A competition may be in several groups (#2882); exclusion is pairwise within each group, so two competitions that share no group never refuse each other. Creating or editing one group never moves a competition out of another.

Platform administrators only: a group changes who may register in several events at once.

AdminExclusionGroupsResource(http: ctfy.sdk.base.BaseHttpClient)
40    def __init__(self, http: BaseHttpClient) -> None:
41        self._http = http
def list(self) -> ctfy.server.models.ExclusionGroupPage:
43    def list(self) -> ExclusionGroupPage:
44        """Every group with its member competitions (unwindowed)."""
45        resp = self._http.request("GET", "/admin/exclusion-groups")
46        _raise_for_status(resp)
47        return ExclusionGroupPage.model_validate(resp.json())

Every group with its member competitions (unwindowed).

def get(self, group_id: str) -> ctfy.server.models.ExclusionGroupInfo:
49    def get(self, group_id: str) -> ExclusionGroupInfo:
50        resp = self._http.request("GET", f"/admin/exclusion-groups/{group_id}")
51        _raise_for_status(resp)
52        return ExclusionGroupInfo.model_validate(resp.json())
def create( self, name: str, *, competition_ids: list[str] | None = None, switch_after_start: bool = False) -> ctfy.server.models.ExclusionGroupInfo:
54    def create(
55        self,
56        name: str,
57        *,
58        competition_ids: builtins.list[str] | None = None,
59        switch_after_start: bool = False,
60    ) -> ExclusionGroupInfo:
61        """Create a group. A competition named here keeps every group it was already in."""
62        resp = self._http.request(
63            "POST",
64            "/admin/exclusion-groups",
65            json={
66                "name": name,
67                "competition_ids": competition_ids or [],
68                "switch_after_start": switch_after_start,
69            },
70        )
71        _raise_for_status(resp)
72        return ExclusionGroupInfo.model_validate(resp.json())

Create a group. A competition named here keeps every group it was already in.

def update( self, group_id: str, *, name: str | None = None, competition_ids: list[str] | None = None, switch_after_start: bool | None = None) -> ctfy.server.models.ExclusionGroupInfo:
74    def update(
75        self,
76        group_id: str,
77        *,
78        name: str | None = None,
79        competition_ids: builtins.list[str] | None = None,
80        switch_after_start: bool | None = None,
81    ) -> ExclusionGroupInfo:
82        """Edit a group. Partial: what you leave out is left alone."""
83        body: dict[str, object] = {}
84        if name is not None:
85            body["name"] = name
86        if competition_ids is not None:
87            body["competition_ids"] = competition_ids
88        if switch_after_start is not None:
89            body["switch_after_start"] = switch_after_start
90        resp = self._http.request("PATCH", f"/admin/exclusion-groups/{group_id}", json=body)
91        _raise_for_status(resp)
92        return ExclusionGroupInfo.model_validate(resp.json())

Edit a group. Partial: what you leave out is left alone.

def delete(self, group_id: str) -> ctfy.server.models.ExclusionGroupInfo:
94    def delete(self, group_id: str) -> ExclusionGroupInfo:
95        """Dissolve a group, answering with the row removed. Its competitions stay."""
96        resp = self._http.request("DELETE", f"/admin/exclusion-groups/{group_id}")
97        _raise_for_status(resp)
98        return ExclusionGroupInfo.model_validate(resp.json())

Dissolve a group, answering with the row removed. Its competitions stay.

def competition_groups(self, competition_id: str) -> list[str]:
100    def competition_groups(self, competition_id: str) -> builtins.list[str]:
101        """The groups a competition is in, by id; ``[]`` is none."""
102        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/exclusion-group")
103        _raise_for_status(resp)
104        return CompetitionGroupRef.model_validate(resp.json()).group_ids

The groups a competition is in, by id; [] is none.

def set_competition_groups(self, competition_id: str, group_ids: list[str]) -> list[str]:
106    def set_competition_groups(
107        self, competition_id: str, group_ids: builtins.list[str]
108    ) -> builtins.list[str]:
109        """Make ``group_ids`` exactly the groups a competition is in (``[]``: none).
110
111        Only this competition's memberships change; every other member of each
112        group stays where it was.
113        """
114        resp = self._http.request(
115            "PUT",
116            f"/admin/competitions/{competition_id}/exclusion-group",
117            json={"group_ids": group_ids},
118        )
119        _raise_for_status(resp)
120        return CompetitionGroupRef.model_validate(resp.json()).group_ids

Make group_ids exactly the groups a competition is in ([]: none).

Only this competition's memberships change; every other member of each group stays where it was.

def conflicts( self, *, group_id: str = '') -> ctfy.server.models.ExclusionConflictReport:
122    def conflicts(self, *, group_id: str = "") -> ExclusionConflictReport:
123        """Accounts holding seats in two members of **one** group right now
124        (#2931), per group, with each seat's competition, team, teammates and
125        review state. Never over the union of groups."""
126        params = {"group_id": group_id} if group_id else None
127        resp = self._http.request("GET", "/admin/exclusion-groups/conflicts", params=params)
128        _raise_for_status(resp)
129        return ExclusionConflictReport.model_validate(resp.json())

Accounts holding seats in two members of one group right now (#2931), per group, with each seat's competition, team, teammates and review state. Never over the union of groups.

def notify_conflicts( self, *, deadline: datetime.datetime | None = None, group_id: str = '', team_ids: list[str] | None = None, channels: list[typing.Literal['inbox', 'email']] | None = None, dry_run: bool = True) -> ctfy.server.models.ExclusionNoticeResult:
131    def notify_conflicts(
132        self,
133        *,
134        deadline: datetime | None = None,
135        group_id: str = "",
136        team_ids: builtins.list[str] | None = None,
137        channels: builtins.list[ExclusionNoticeChannel] | None = None,
138        dry_run: bool = True,
139    ) -> ExclusionNoticeResult:
140        """Tell every member of each conflicting team to withdraw from one
141        event by ``deadline``, on ``channels`` (default: inbox and email),
142        once per team crossing per channel.
143
144        **Dry run unless ``dry_run=False``.** ``deadline`` defaults to the
145        ``exclusion_conflict_deadline`` setting; it must name its offset, and
146        only a dry run may go without one.
147        """
148        body: dict[str, object] = {"group_id": group_id, "dry_run": dry_run}
149        if channels is not None:
150            body["channels"] = channels
151        if deadline is not None:
152            body["deadline"] = deadline.isoformat()
153        if team_ids is not None:
154            body["team_ids"] = team_ids
155        resp = self._http.request("POST", "/admin/exclusion-groups/conflicts/notify", json=body)
156        _raise_for_status(resp)
157        return ExclusionNoticeResult.model_validate(resp.json())

Tell every member of each conflicting team to withdraw from one event by deadline, on channels (default: inbox and email), once per team crossing per channel.

Dry run unless dry_run=False. deadline defaults to the exclusion_conflict_deadline setting; it must name its offset, and only a dry run may go without one.