ctfy.sdk.admin_resources.env_sharing
client.admin.env_sharing -- how many teams stand on one environment,
the groups behind it, and the health of the shared environments (#2129 B).
For a competition's organisers (global admins and the event's own competition admins). ⚠️ Everything here is the secret the feature keeps from players: a group number, a group's members and a group's size are never on a player-facing call.
1"""``client.admin.env_sharing`` -- how many teams stand on one environment, 2the groups behind it, and the health of the shared environments (#2129 B). 3 4For a competition's organisers (global admins and the event's own 5competition admins). ⚠️ Everything here is the secret the feature keeps 6from players: a group number, a group's members and a group's size are 7never on a player-facing call. 8""" 9 10from __future__ import annotations 11 12from typing import Any 13 14from ctfy.core.enums import EnvShareMode, GroupingMethod 15from ctfy.sdk._helpers import _raise_for_status 16from ctfy.sdk.base import BaseHttpClient 17from ctfy.server.models import EnvSharingView, GroupingTable, SharedEnvTable 18 19 20class AdminEnvSharingResource: 21 """Read and edit one competition's environment sharing.""" 22 23 def __init__(self, http: BaseHttpClient) -> None: 24 self._http = http 25 26 def get(self, competition_id: str) -> EnvSharingView: 27 """The default, the per-challenge overrides (with each challenge's 28 declaration and what blocks sharing it), the cost of each mode in 29 environments per challenge, and the grouping's state.""" 30 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/env-sharing") 31 _raise_for_status(resp) 32 return EnvSharingView.model_validate(resp.json()) 33 34 def update( 35 self, 36 competition_id: str, 37 *, 38 mode: EnvShareMode | None = None, 39 group_size: int | None = None, 40 overrides: dict[str, EnvShareMode | None] | None = None, 41 confirm: bool = False, 42 ) -> EnvSharingView: 43 """Set the default and/or per-challenge overrides. 44 45 Partial: only what you pass changes; an override of ``None`` follows 46 the competition again. After the competition has started a change 47 needs ``confirm=True`` and affects only environments started after 48 it -- running ones are left as they are. A challenge that cannot be 49 shared (engagement, defence, judged) refuses an explicit override 50 (422 ``shared_environment_unsupported``). 51 """ 52 payload: dict[str, Any] = {"confirm": confirm, "overrides": {}} 53 if mode is not None: 54 payload["mode"] = EnvShareMode(mode).value 55 if group_size is not None: 56 payload["group_size"] = group_size 57 for cid, m in (overrides or {}).items(): 58 payload["overrides"][cid] = None if m is None else EnvShareMode(m).value 59 resp = self._http.request( 60 "PATCH", f"/admin/competitions/{competition_id}/env-sharing", json=payload 61 ) 62 _raise_for_status(resp) 63 return EnvSharingView.model_validate(resp.json()) 64 65 def groups(self, competition_id: str) -> GroupingTable: 66 """Who is in which group.""" 67 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/env-sharing/groups") 68 _raise_for_status(resp) 69 return GroupingTable.model_validate(resp.json()) 70 71 def draw( 72 self, 73 competition_id: str, 74 *, 75 method: GroupingMethod = GroupingMethod.RANDOM, 76 by_field: str = "", 77 seed: int | None = None, 78 csv: str = "", 79 force: bool = False, 80 ) -> EnvSharingView: 81 """(Re-)divide the teams: ``random`` (the default), ``spread`` by a 82 registration field with more than one value, or ``csv`` -- lines of 83 ``team id, group number``. Refused (409 ``groups_in_use``) once a 84 grouped environment exists unless ``force``, which stops those boxes 85 and discards their flags.""" 86 payload: dict[str, Any] = { 87 "method": GroupingMethod(method).value, 88 "by_field": by_field, 89 "csv": csv, 90 "force": force, 91 } 92 if seed is not None: 93 payload["seed"] = seed 94 resp = self._http.request( 95 "POST", f"/admin/competitions/{competition_id}/env-sharing/groups", json=payload 96 ) 97 _raise_for_status(resp) 98 return EnvSharingView.model_validate(resp.json()) 99 100 def export_groups(self, competition_id: str) -> str: 101 """The grouping as CSV text (``team_id,group,team_name,value``).""" 102 resp = self._http.request( 103 "GET", f"/admin/competitions/{competition_id}/env-sharing/groups.csv" 104 ) 105 _raise_for_status(resp) 106 return resp.text 107 108 def environments(self, competition_id: str) -> SharedEnvTable: 109 """Every shared environment: what it is doing now, its last 24 hours, 110 its open reports and its restarts.""" 111 resp = self._http.request( 112 "GET", f"/admin/competitions/{competition_id}/shared-environments" 113 ) 114 _raise_for_status(resp) 115 return SharedEnvTable.model_validate(resp.json()) 116 117 def reset(self, competition_id: str, challenge_id: str, group_key: str) -> SharedEnvTable: 118 """Stop one shared environment and bring a fresh one up with the same 119 flags; resolves its open reports. ``group_key`` is ``all`` or ``g<N>`` 120 as the table shows it.""" 121 resp = self._http.request( 122 "POST", 123 f"/admin/competitions/{competition_id}/shared-environments/reset", 124 json={"challenge_id": challenge_id, "group_key": group_key}, 125 ) 126 _raise_for_status(resp) 127 return SharedEnvTable.model_validate(resp.json()) 128 129 def dismiss_reports( 130 self, competition_id: str, challenge_id: str, group_key: str 131 ) -> SharedEnvTable: 132 """Mark an environment's reports seen without resetting it.""" 133 resp = self._http.request( 134 "POST", 135 f"/admin/competitions/{competition_id}/shared-environments/dismiss-reports", 136 json={"challenge_id": challenge_id, "group_key": group_key}, 137 ) 138 _raise_for_status(resp) 139 return SharedEnvTable.model_validate(resp.json())
21class AdminEnvSharingResource: 22 """Read and edit one competition's environment sharing.""" 23 24 def __init__(self, http: BaseHttpClient) -> None: 25 self._http = http 26 27 def get(self, competition_id: str) -> EnvSharingView: 28 """The default, the per-challenge overrides (with each challenge's 29 declaration and what blocks sharing it), the cost of each mode in 30 environments per challenge, and the grouping's state.""" 31 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/env-sharing") 32 _raise_for_status(resp) 33 return EnvSharingView.model_validate(resp.json()) 34 35 def update( 36 self, 37 competition_id: str, 38 *, 39 mode: EnvShareMode | None = None, 40 group_size: int | None = None, 41 overrides: dict[str, EnvShareMode | None] | None = None, 42 confirm: bool = False, 43 ) -> EnvSharingView: 44 """Set the default and/or per-challenge overrides. 45 46 Partial: only what you pass changes; an override of ``None`` follows 47 the competition again. After the competition has started a change 48 needs ``confirm=True`` and affects only environments started after 49 it -- running ones are left as they are. A challenge that cannot be 50 shared (engagement, defence, judged) refuses an explicit override 51 (422 ``shared_environment_unsupported``). 52 """ 53 payload: dict[str, Any] = {"confirm": confirm, "overrides": {}} 54 if mode is not None: 55 payload["mode"] = EnvShareMode(mode).value 56 if group_size is not None: 57 payload["group_size"] = group_size 58 for cid, m in (overrides or {}).items(): 59 payload["overrides"][cid] = None if m is None else EnvShareMode(m).value 60 resp = self._http.request( 61 "PATCH", f"/admin/competitions/{competition_id}/env-sharing", json=payload 62 ) 63 _raise_for_status(resp) 64 return EnvSharingView.model_validate(resp.json()) 65 66 def groups(self, competition_id: str) -> GroupingTable: 67 """Who is in which group.""" 68 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/env-sharing/groups") 69 _raise_for_status(resp) 70 return GroupingTable.model_validate(resp.json()) 71 72 def draw( 73 self, 74 competition_id: str, 75 *, 76 method: GroupingMethod = GroupingMethod.RANDOM, 77 by_field: str = "", 78 seed: int | None = None, 79 csv: str = "", 80 force: bool = False, 81 ) -> EnvSharingView: 82 """(Re-)divide the teams: ``random`` (the default), ``spread`` by a 83 registration field with more than one value, or ``csv`` -- lines of 84 ``team id, group number``. Refused (409 ``groups_in_use``) once a 85 grouped environment exists unless ``force``, which stops those boxes 86 and discards their flags.""" 87 payload: dict[str, Any] = { 88 "method": GroupingMethod(method).value, 89 "by_field": by_field, 90 "csv": csv, 91 "force": force, 92 } 93 if seed is not None: 94 payload["seed"] = seed 95 resp = self._http.request( 96 "POST", f"/admin/competitions/{competition_id}/env-sharing/groups", json=payload 97 ) 98 _raise_for_status(resp) 99 return EnvSharingView.model_validate(resp.json()) 100 101 def export_groups(self, competition_id: str) -> str: 102 """The grouping as CSV text (``team_id,group,team_name,value``).""" 103 resp = self._http.request( 104 "GET", f"/admin/competitions/{competition_id}/env-sharing/groups.csv" 105 ) 106 _raise_for_status(resp) 107 return resp.text 108 109 def environments(self, competition_id: str) -> SharedEnvTable: 110 """Every shared environment: what it is doing now, its last 24 hours, 111 its open reports and its restarts.""" 112 resp = self._http.request( 113 "GET", f"/admin/competitions/{competition_id}/shared-environments" 114 ) 115 _raise_for_status(resp) 116 return SharedEnvTable.model_validate(resp.json()) 117 118 def reset(self, competition_id: str, challenge_id: str, group_key: str) -> SharedEnvTable: 119 """Stop one shared environment and bring a fresh one up with the same 120 flags; resolves its open reports. ``group_key`` is ``all`` or ``g<N>`` 121 as the table shows it.""" 122 resp = self._http.request( 123 "POST", 124 f"/admin/competitions/{competition_id}/shared-environments/reset", 125 json={"challenge_id": challenge_id, "group_key": group_key}, 126 ) 127 _raise_for_status(resp) 128 return SharedEnvTable.model_validate(resp.json()) 129 130 def dismiss_reports( 131 self, competition_id: str, challenge_id: str, group_key: str 132 ) -> SharedEnvTable: 133 """Mark an environment's reports seen without resetting it.""" 134 resp = self._http.request( 135 "POST", 136 f"/admin/competitions/{competition_id}/shared-environments/dismiss-reports", 137 json={"challenge_id": challenge_id, "group_key": group_key}, 138 ) 139 _raise_for_status(resp) 140 return SharedEnvTable.model_validate(resp.json())
Read and edit one competition's environment sharing.
27 def get(self, competition_id: str) -> EnvSharingView: 28 """The default, the per-challenge overrides (with each challenge's 29 declaration and what blocks sharing it), the cost of each mode in 30 environments per challenge, and the grouping's state.""" 31 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/env-sharing") 32 _raise_for_status(resp) 33 return EnvSharingView.model_validate(resp.json())
The default, the per-challenge overrides (with each challenge's declaration and what blocks sharing it), the cost of each mode in environments per challenge, and the grouping's state.
35 def update( 36 self, 37 competition_id: str, 38 *, 39 mode: EnvShareMode | None = None, 40 group_size: int | None = None, 41 overrides: dict[str, EnvShareMode | None] | None = None, 42 confirm: bool = False, 43 ) -> EnvSharingView: 44 """Set the default and/or per-challenge overrides. 45 46 Partial: only what you pass changes; an override of ``None`` follows 47 the competition again. After the competition has started a change 48 needs ``confirm=True`` and affects only environments started after 49 it -- running ones are left as they are. A challenge that cannot be 50 shared (engagement, defence, judged) refuses an explicit override 51 (422 ``shared_environment_unsupported``). 52 """ 53 payload: dict[str, Any] = {"confirm": confirm, "overrides": {}} 54 if mode is not None: 55 payload["mode"] = EnvShareMode(mode).value 56 if group_size is not None: 57 payload["group_size"] = group_size 58 for cid, m in (overrides or {}).items(): 59 payload["overrides"][cid] = None if m is None else EnvShareMode(m).value 60 resp = self._http.request( 61 "PATCH", f"/admin/competitions/{competition_id}/env-sharing", json=payload 62 ) 63 _raise_for_status(resp) 64 return EnvSharingView.model_validate(resp.json())
Set the default and/or per-challenge overrides.
Partial: only what you pass changes; an override of None follows
the competition again. After the competition has started a change
needs confirm=True and affects only environments started after
it -- running ones are left as they are. A challenge that cannot be
shared (engagement, defence, judged) refuses an explicit override
(422 shared_environment_unsupported).
66 def groups(self, competition_id: str) -> GroupingTable: 67 """Who is in which group.""" 68 resp = self._http.request("GET", f"/admin/competitions/{competition_id}/env-sharing/groups") 69 _raise_for_status(resp) 70 return GroupingTable.model_validate(resp.json())
Who is in which group.
72 def draw( 73 self, 74 competition_id: str, 75 *, 76 method: GroupingMethod = GroupingMethod.RANDOM, 77 by_field: str = "", 78 seed: int | None = None, 79 csv: str = "", 80 force: bool = False, 81 ) -> EnvSharingView: 82 """(Re-)divide the teams: ``random`` (the default), ``spread`` by a 83 registration field with more than one value, or ``csv`` -- lines of 84 ``team id, group number``. Refused (409 ``groups_in_use``) once a 85 grouped environment exists unless ``force``, which stops those boxes 86 and discards their flags.""" 87 payload: dict[str, Any] = { 88 "method": GroupingMethod(method).value, 89 "by_field": by_field, 90 "csv": csv, 91 "force": force, 92 } 93 if seed is not None: 94 payload["seed"] = seed 95 resp = self._http.request( 96 "POST", f"/admin/competitions/{competition_id}/env-sharing/groups", json=payload 97 ) 98 _raise_for_status(resp) 99 return EnvSharingView.model_validate(resp.json())
(Re-)divide the teams: random (the default), spread by a
registration field with more than one value, or csv -- lines of
team id, group number. Refused (409 groups_in_use) once a
grouped environment exists unless force, which stops those boxes
and discards their flags.
101 def export_groups(self, competition_id: str) -> str: 102 """The grouping as CSV text (``team_id,group,team_name,value``).""" 103 resp = self._http.request( 104 "GET", f"/admin/competitions/{competition_id}/env-sharing/groups.csv" 105 ) 106 _raise_for_status(resp) 107 return resp.text
The grouping as CSV text (team_id,group,team_name,value).
109 def environments(self, competition_id: str) -> SharedEnvTable: 110 """Every shared environment: what it is doing now, its last 24 hours, 111 its open reports and its restarts.""" 112 resp = self._http.request( 113 "GET", f"/admin/competitions/{competition_id}/shared-environments" 114 ) 115 _raise_for_status(resp) 116 return SharedEnvTable.model_validate(resp.json())
Every shared environment: what it is doing now, its last 24 hours, its open reports and its restarts.
118 def reset(self, competition_id: str, challenge_id: str, group_key: str) -> SharedEnvTable: 119 """Stop one shared environment and bring a fresh one up with the same 120 flags; resolves its open reports. ``group_key`` is ``all`` or ``g<N>`` 121 as the table shows it.""" 122 resp = self._http.request( 123 "POST", 124 f"/admin/competitions/{competition_id}/shared-environments/reset", 125 json={"challenge_id": challenge_id, "group_key": group_key}, 126 ) 127 _raise_for_status(resp) 128 return SharedEnvTable.model_validate(resp.json())
Stop one shared environment and bring a fresh one up with the same
flags; resolves its open reports. group_key is all or g<N>
as the table shows it.
130 def dismiss_reports( 131 self, competition_id: str, challenge_id: str, group_key: str 132 ) -> SharedEnvTable: 133 """Mark an environment's reports seen without resetting it.""" 134 resp = self._http.request( 135 "POST", 136 f"/admin/competitions/{competition_id}/shared-environments/dismiss-reports", 137 json={"challenge_id": challenge_id, "group_key": group_key}, 138 ) 139 _raise_for_status(resp) 140 return SharedEnvTable.model_validate(resp.json())
Mark an environment's reports seen without resetting it.