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"]
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.
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).
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.
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.
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.
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.
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.
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.
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.