ctfy.sdk.admin
Operator / admin client for the ctfy platform.
AdminClient is the operator-facing counterpart to
~ctfy.sdk.client.PlatformClient, mirroring the ctfy /
ctfy-admin CLI split: the player client never surfaces admin methods, so
agents and players reading the SDK reference aren't distracted by operator
endpoints they can't call.
Reach it two ways:
client.adminon an existingPlatformClient— shares that client's transport (connection pool + bearer)::from ctfy.sdk import PlatformClient client = PlatformClient("https://ctfy.example", token="pf_xxx") client.admin.users.set_role(user_id, "admin")AdminClient.connect()for operator tooling that doesn't already hold a player client::from ctfy.sdk import AdminClient with AdminClient.connect("https://ctfy.example", token="pf_xxx") as admin: admin.observability.overview()
Every endpoint here is role-gated server-side (admin / super-admin); the SDK only relays — the token's role decides access.
1"""Operator / admin client for the ctfy platform. 2 3``AdminClient`` is the operator-facing counterpart to 4:class:`~ctfy.sdk.client.PlatformClient`, mirroring the ``ctfy`` / 5``ctfy-admin`` CLI split: the player client never surfaces admin methods, so 6agents and players reading the SDK reference aren't distracted by operator 7endpoints they can't call. 8 9Reach it two ways: 10 11* ``client.admin`` on an existing :class:`PlatformClient` — shares that 12 client's transport (connection pool + bearer):: 13 14 from ctfy.sdk import PlatformClient 15 16 client = PlatformClient("https://ctfy.example", token="pf_xxx") 17 client.admin.users.set_role(user_id, "admin") 18 19* :meth:`AdminClient.connect` for operator tooling that doesn't already hold 20 a player client:: 21 22 from ctfy.sdk import AdminClient 23 24 with AdminClient.connect("https://ctfy.example", token="pf_xxx") as admin: 25 admin.observability.overview() 26 27Every endpoint here is role-gated server-side (admin / super-admin); the SDK 28only relays — the token's role decides access. 29""" 30 31from __future__ import annotations 32 33from functools import cached_property 34from types import TracebackType 35from typing import Self 36 37from ctfy.core.constants import DEFAULT_CLIENT_TIMEOUT 38from ctfy.sdk.admin_resources.achievements import AdminAchievementsResource 39from ctfy.sdk.admin_resources.announcements import AdminAnnouncementsResource 40from ctfy.sdk.admin_resources.authoring import AdminAuthoredChallengesResource 41from ctfy.sdk.admin_resources.authoring_review import ( 42 AdminAuthoringReviewersResource, 43 AdminCompetitionAuthoredChallengesResource, 44) 45from ctfy.sdk.admin_resources.awd import AdminAwdResource 46from ctfy.sdk.admin_resources.challenges import AdminChallengesResource 47from ctfy.sdk.admin_resources.changelog import AdminChangelogResource 48from ctfy.sdk.admin_resources.competition_admins import AdminCompetitionAdminsResource 49from ctfy.sdk.admin_resources.competition_applications import ( 50 AdminCompetitionApplicationsResource, 51) 52from ctfy.sdk.admin_resources.competition_attachments import ( 53 AdminCompetitionAttachmentsResource, 54) 55from ctfy.sdk.admin_resources.competition_invites import AdminCompetitionInvitesResource 56from ctfy.sdk.admin_resources.competitions import AdminCompetitionsResource 57from ctfy.sdk.admin_resources.email import AdminEmailResource 58from ctfy.sdk.admin_resources.eval import ( 59 AdminEvalAllowancesResource, 60 AdminEvalCampaignsResource, 61 AdminEvalHarnessesResource, 62 AdminEvalRunsResource, 63 AdminEvalVendorsResource, 64 AdminLlmModelsResource, 65 AdminRegistryResource, 66) 67from ctfy.sdk.admin_resources.gauntlet import AdminGauntletResource 68from ctfy.sdk.admin_resources.instances import AdminInstancesResource 69from ctfy.sdk.admin_resources.institutions import AdminInstitutionsResource 70from ctfy.sdk.admin_resources.llm_calls import AdminLlmCallsResource 71from ctfy.sdk.admin_resources.llm_providers import AdminLlmProvidersResource 72from ctfy.sdk.admin_resources.nodes import AdminNodesResource 73from ctfy.sdk.admin_resources.observability import AdminObservabilityResource 74from ctfy.sdk.admin_resources.patches import AdminPatchesResource 75from ctfy.sdk.admin_resources.records import AdminRecordsResource 76from ctfy.sdk.admin_resources.registrations import AdminRegistrationsResource 77from ctfy.sdk.admin_resources.reports import AdminReportsResource 78from ctfy.sdk.admin_resources.series import AdminSeriesResource 79from ctfy.sdk.admin_resources.settings import AdminSettingsResource 80from ctfy.sdk.admin_resources.tasks import ( 81 AdminScheduledJobsResource, 82 AdminTasksResource, 83) 84from ctfy.sdk.admin_resources.teams import AdminTeamsResource 85from ctfy.sdk.admin_resources.users import AdminUsersResource 86from ctfy.sdk.base import BaseHttpClient 87 88__all__ = ["AdminClient"] 89 90 91class AdminClient: 92 """Admin / operator surface, grouped into resource namespaces. 93 94 Namespaces: :attr:`users`, :attr:`competitions`, :attr:`announcements`, 95 :attr:`achievements`, :attr:`challenges`, :attr:`gauntlet`, 96 :attr:`instances`, 97 :attr:`records`, :attr:`nodes`, :attr:`observability`, :attr:`settings`, 98 :attr:`competition_admins`. 99 """ 100 101 def __init__(self, http: BaseHttpClient) -> None: 102 #: Shared transport (the parent PlatformClient when reached via 103 #: ``client.admin``, or an owned client when built via ``connect``). 104 self._http = http 105 106 @classmethod 107 def connect( 108 cls, 109 server_url: str, 110 token: str = "", 111 *, 112 max_retries: int = 2, 113 timeout: int = DEFAULT_CLIENT_TIMEOUT, 114 ) -> AdminClient: 115 """Build an ``AdminClient`` that owns its own transport. 116 117 For operator tooling that doesn't already hold a 118 :class:`PlatformClient`. The returned client is a context manager 119 that closes its transport on exit. 120 """ 121 base = server_url.rstrip("/") 122 http = BaseHttpClient(f"{base}/api/v1", token, timeout=timeout, max_retries=max_retries) 123 return cls(http) 124 125 @cached_property 126 def users(self) -> AdminUsersResource: 127 return AdminUsersResource(self._http) 128 129 @cached_property 130 def competitions(self) -> AdminCompetitionsResource: 131 return AdminCompetitionsResource(self._http) 132 133 @cached_property 134 def announcements(self) -> AdminAnnouncementsResource: 135 return AdminAnnouncementsResource(self._http) 136 137 @cached_property 138 def changelog(self) -> AdminChangelogResource: 139 return AdminChangelogResource(self._http) 140 141 @cached_property 142 def institutions(self) -> AdminInstitutionsResource: 143 return AdminInstitutionsResource(self._http) 144 145 @cached_property 146 def achievements(self) -> AdminAchievementsResource: 147 return AdminAchievementsResource(self._http) 148 149 @cached_property 150 def challenges(self) -> AdminChallengesResource: 151 return AdminChallengesResource(self._http) 152 153 @cached_property 154 def gauntlet(self) -> AdminGauntletResource: 155 return AdminGauntletResource(self._http) 156 157 @cached_property 158 def email(self) -> AdminEmailResource: 159 return AdminEmailResource(self._http) 160 161 @cached_property 162 def llm_calls(self) -> AdminLlmCallsResource: 163 return AdminLlmCallsResource(self._http) 164 165 @cached_property 166 def llm_providers(self) -> AdminLlmProvidersResource: 167 return AdminLlmProvidersResource(self._http) 168 169 @cached_property 170 def patches(self) -> AdminPatchesResource: 171 return AdminPatchesResource(self._http) 172 173 @cached_property 174 def reports(self) -> AdminReportsResource: 175 return AdminReportsResource(self._http) 176 177 @cached_property 178 def awd(self) -> AdminAwdResource: 179 return AdminAwdResource(self._http) 180 181 @cached_property 182 def series(self) -> AdminSeriesResource: 183 return AdminSeriesResource(self._http) 184 185 @cached_property 186 def instances(self) -> AdminInstancesResource: 187 return AdminInstancesResource(self._http) 188 189 @cached_property 190 def records(self) -> AdminRecordsResource: 191 return AdminRecordsResource(self._http) 192 193 @cached_property 194 def nodes(self) -> AdminNodesResource: 195 return AdminNodesResource(self._http) 196 197 @cached_property 198 def observability(self) -> AdminObservabilityResource: 199 return AdminObservabilityResource(self._http) 200 201 @cached_property 202 def settings(self) -> AdminSettingsResource: 203 return AdminSettingsResource(self._http) 204 205 @cached_property 206 def competition_admins(self) -> AdminCompetitionAdminsResource: 207 return AdminCompetitionAdminsResource(self._http) 208 209 @cached_property 210 def competition_invites(self) -> AdminCompetitionInvitesResource: 211 return AdminCompetitionInvitesResource(self._http) 212 213 @cached_property 214 def competition_applications(self) -> AdminCompetitionApplicationsResource: 215 """Applications to a private competition, and the verdict on each.""" 216 return AdminCompetitionApplicationsResource(self._http) 217 218 @cached_property 219 def competition_attachments(self) -> AdminCompetitionAttachmentsResource: 220 """A competition's rules / handbook / notice files (#1808).""" 221 return AdminCompetitionAttachmentsResource(self._http) 222 223 @cached_property 224 def registrations(self) -> AdminRegistrationsResource: 225 """Entrant roster, eligibility review, and CSV export.""" 226 return AdminRegistrationsResource(self._http) 227 228 @cached_property 229 def teams(self) -> AdminTeamsResource: 230 """Enforcement against a squad — disqualify and reinstate. 231 232 Separate from ``registrations`` on purpose: that one rules on 233 eligibility (which decides prizes), this one decides whether the 234 team is in the event at all. 235 """ 236 return AdminTeamsResource(self._http) 237 238 @cached_property 239 def tasks(self) -> AdminTasksResource: 240 return AdminTasksResource(self._http) 241 242 @cached_property 243 def scheduled_jobs(self) -> AdminScheduledJobsResource: 244 return AdminScheduledJobsResource(self._http) 245 246 @cached_property 247 def authored_challenges(self) -> AdminAuthoredChallengesResource: 248 """The review queue for player-authored challenges.""" 249 return AdminAuthoredChallengesResource(self._http) 250 251 @cached_property 252 def authoring_reviewers(self) -> AdminAuthoringReviewersResource: 253 """Who may rule on one competition's uploads (super-admin).""" 254 return AdminAuthoringReviewersResource(self._http) 255 256 @cached_property 257 def competition_authored_challenges(self) -> AdminCompetitionAuthoredChallengesResource: 258 """One competition's uploads, for its delegated reviewer: read, 259 audit and rule — never publish.""" 260 return AdminCompetitionAuthoredChallengesResource(self._http) 261 262 @cached_property 263 def llm_models(self) -> AdminLlmModelsResource: 264 return AdminLlmModelsResource(self._http) 265 266 @cached_property 267 def eval_vendors(self) -> AdminEvalVendorsResource: 268 return AdminEvalVendorsResource(self._http) 269 270 @cached_property 271 def eval_harnesses(self) -> AdminEvalHarnessesResource: 272 return AdminEvalHarnessesResource(self._http) 273 274 @property 275 def eval_allowances(self) -> AdminEvalAllowancesResource: 276 return AdminEvalAllowancesResource(self._http) 277 278 @property 279 def registry(self) -> AdminRegistryResource: 280 """Registry storage: the usage report and the manual reclaim.""" 281 return AdminRegistryResource(self._http) 282 283 @cached_property 284 def eval_runs(self) -> AdminEvalRunsResource: 285 return AdminEvalRunsResource(self._http) 286 287 @cached_property 288 def eval_campaigns(self) -> AdminEvalCampaignsResource: 289 return AdminEvalCampaignsResource(self._http) 290 291 def close(self) -> None: 292 """Close the underlying transport. 293 294 Only call this on a standalone client built via :meth:`connect`; 295 when reached via ``PlatformClient.admin`` the transport is shared 296 with — and closed by — the parent player client. 297 """ 298 self._http.close() 299 300 def __enter__(self) -> Self: 301 return self 302 303 def __exit__( 304 self, 305 exc_type: type[BaseException] | None, 306 exc: BaseException | None, 307 tb: TracebackType | None, 308 ) -> None: 309 self.close()
92class AdminClient: 93 """Admin / operator surface, grouped into resource namespaces. 94 95 Namespaces: :attr:`users`, :attr:`competitions`, :attr:`announcements`, 96 :attr:`achievements`, :attr:`challenges`, :attr:`gauntlet`, 97 :attr:`instances`, 98 :attr:`records`, :attr:`nodes`, :attr:`observability`, :attr:`settings`, 99 :attr:`competition_admins`. 100 """ 101 102 def __init__(self, http: BaseHttpClient) -> None: 103 #: Shared transport (the parent PlatformClient when reached via 104 #: ``client.admin``, or an owned client when built via ``connect``). 105 self._http = http 106 107 @classmethod 108 def connect( 109 cls, 110 server_url: str, 111 token: str = "", 112 *, 113 max_retries: int = 2, 114 timeout: int = DEFAULT_CLIENT_TIMEOUT, 115 ) -> AdminClient: 116 """Build an ``AdminClient`` that owns its own transport. 117 118 For operator tooling that doesn't already hold a 119 :class:`PlatformClient`. The returned client is a context manager 120 that closes its transport on exit. 121 """ 122 base = server_url.rstrip("/") 123 http = BaseHttpClient(f"{base}/api/v1", token, timeout=timeout, max_retries=max_retries) 124 return cls(http) 125 126 @cached_property 127 def users(self) -> AdminUsersResource: 128 return AdminUsersResource(self._http) 129 130 @cached_property 131 def competitions(self) -> AdminCompetitionsResource: 132 return AdminCompetitionsResource(self._http) 133 134 @cached_property 135 def announcements(self) -> AdminAnnouncementsResource: 136 return AdminAnnouncementsResource(self._http) 137 138 @cached_property 139 def changelog(self) -> AdminChangelogResource: 140 return AdminChangelogResource(self._http) 141 142 @cached_property 143 def institutions(self) -> AdminInstitutionsResource: 144 return AdminInstitutionsResource(self._http) 145 146 @cached_property 147 def achievements(self) -> AdminAchievementsResource: 148 return AdminAchievementsResource(self._http) 149 150 @cached_property 151 def challenges(self) -> AdminChallengesResource: 152 return AdminChallengesResource(self._http) 153 154 @cached_property 155 def gauntlet(self) -> AdminGauntletResource: 156 return AdminGauntletResource(self._http) 157 158 @cached_property 159 def email(self) -> AdminEmailResource: 160 return AdminEmailResource(self._http) 161 162 @cached_property 163 def llm_calls(self) -> AdminLlmCallsResource: 164 return AdminLlmCallsResource(self._http) 165 166 @cached_property 167 def llm_providers(self) -> AdminLlmProvidersResource: 168 return AdminLlmProvidersResource(self._http) 169 170 @cached_property 171 def patches(self) -> AdminPatchesResource: 172 return AdminPatchesResource(self._http) 173 174 @cached_property 175 def reports(self) -> AdminReportsResource: 176 return AdminReportsResource(self._http) 177 178 @cached_property 179 def awd(self) -> AdminAwdResource: 180 return AdminAwdResource(self._http) 181 182 @cached_property 183 def series(self) -> AdminSeriesResource: 184 return AdminSeriesResource(self._http) 185 186 @cached_property 187 def instances(self) -> AdminInstancesResource: 188 return AdminInstancesResource(self._http) 189 190 @cached_property 191 def records(self) -> AdminRecordsResource: 192 return AdminRecordsResource(self._http) 193 194 @cached_property 195 def nodes(self) -> AdminNodesResource: 196 return AdminNodesResource(self._http) 197 198 @cached_property 199 def observability(self) -> AdminObservabilityResource: 200 return AdminObservabilityResource(self._http) 201 202 @cached_property 203 def settings(self) -> AdminSettingsResource: 204 return AdminSettingsResource(self._http) 205 206 @cached_property 207 def competition_admins(self) -> AdminCompetitionAdminsResource: 208 return AdminCompetitionAdminsResource(self._http) 209 210 @cached_property 211 def competition_invites(self) -> AdminCompetitionInvitesResource: 212 return AdminCompetitionInvitesResource(self._http) 213 214 @cached_property 215 def competition_applications(self) -> AdminCompetitionApplicationsResource: 216 """Applications to a private competition, and the verdict on each.""" 217 return AdminCompetitionApplicationsResource(self._http) 218 219 @cached_property 220 def competition_attachments(self) -> AdminCompetitionAttachmentsResource: 221 """A competition's rules / handbook / notice files (#1808).""" 222 return AdminCompetitionAttachmentsResource(self._http) 223 224 @cached_property 225 def registrations(self) -> AdminRegistrationsResource: 226 """Entrant roster, eligibility review, and CSV export.""" 227 return AdminRegistrationsResource(self._http) 228 229 @cached_property 230 def teams(self) -> AdminTeamsResource: 231 """Enforcement against a squad — disqualify and reinstate. 232 233 Separate from ``registrations`` on purpose: that one rules on 234 eligibility (which decides prizes), this one decides whether the 235 team is in the event at all. 236 """ 237 return AdminTeamsResource(self._http) 238 239 @cached_property 240 def tasks(self) -> AdminTasksResource: 241 return AdminTasksResource(self._http) 242 243 @cached_property 244 def scheduled_jobs(self) -> AdminScheduledJobsResource: 245 return AdminScheduledJobsResource(self._http) 246 247 @cached_property 248 def authored_challenges(self) -> AdminAuthoredChallengesResource: 249 """The review queue for player-authored challenges.""" 250 return AdminAuthoredChallengesResource(self._http) 251 252 @cached_property 253 def authoring_reviewers(self) -> AdminAuthoringReviewersResource: 254 """Who may rule on one competition's uploads (super-admin).""" 255 return AdminAuthoringReviewersResource(self._http) 256 257 @cached_property 258 def competition_authored_challenges(self) -> AdminCompetitionAuthoredChallengesResource: 259 """One competition's uploads, for its delegated reviewer: read, 260 audit and rule — never publish.""" 261 return AdminCompetitionAuthoredChallengesResource(self._http) 262 263 @cached_property 264 def llm_models(self) -> AdminLlmModelsResource: 265 return AdminLlmModelsResource(self._http) 266 267 @cached_property 268 def eval_vendors(self) -> AdminEvalVendorsResource: 269 return AdminEvalVendorsResource(self._http) 270 271 @cached_property 272 def eval_harnesses(self) -> AdminEvalHarnessesResource: 273 return AdminEvalHarnessesResource(self._http) 274 275 @property 276 def eval_allowances(self) -> AdminEvalAllowancesResource: 277 return AdminEvalAllowancesResource(self._http) 278 279 @property 280 def registry(self) -> AdminRegistryResource: 281 """Registry storage: the usage report and the manual reclaim.""" 282 return AdminRegistryResource(self._http) 283 284 @cached_property 285 def eval_runs(self) -> AdminEvalRunsResource: 286 return AdminEvalRunsResource(self._http) 287 288 @cached_property 289 def eval_campaigns(self) -> AdminEvalCampaignsResource: 290 return AdminEvalCampaignsResource(self._http) 291 292 def close(self) -> None: 293 """Close the underlying transport. 294 295 Only call this on a standalone client built via :meth:`connect`; 296 when reached via ``PlatformClient.admin`` the transport is shared 297 with — and closed by — the parent player client. 298 """ 299 self._http.close() 300 301 def __enter__(self) -> Self: 302 return self 303 304 def __exit__( 305 self, 306 exc_type: type[BaseException] | None, 307 exc: BaseException | None, 308 tb: TracebackType | None, 309 ) -> None: 310 self.close()
Admin / operator surface, grouped into resource namespaces.
Namespaces: users, competitions, announcements,
achievements, challenges, gauntlet,
instances,
records, nodes, observability, settings,
competition_admins.
107 @classmethod 108 def connect( 109 cls, 110 server_url: str, 111 token: str = "", 112 *, 113 max_retries: int = 2, 114 timeout: int = DEFAULT_CLIENT_TIMEOUT, 115 ) -> AdminClient: 116 """Build an ``AdminClient`` that owns its own transport. 117 118 For operator tooling that doesn't already hold a 119 :class:`PlatformClient`. The returned client is a context manager 120 that closes its transport on exit. 121 """ 122 base = server_url.rstrip("/") 123 http = BaseHttpClient(f"{base}/api/v1", token, timeout=timeout, max_retries=max_retries) 124 return cls(http)
Build an AdminClient that owns its own transport.
For operator tooling that doesn't already hold a
PlatformClient. The returned client is a context manager
that closes its transport on exit.
214 @cached_property 215 def competition_applications(self) -> AdminCompetitionApplicationsResource: 216 """Applications to a private competition, and the verdict on each.""" 217 return AdminCompetitionApplicationsResource(self._http)
Applications to a private competition, and the verdict on each.
219 @cached_property 220 def competition_attachments(self) -> AdminCompetitionAttachmentsResource: 221 """A competition's rules / handbook / notice files (#1808).""" 222 return AdminCompetitionAttachmentsResource(self._http)
A competition's rules / handbook / notice files (#1808).
224 @cached_property 225 def registrations(self) -> AdminRegistrationsResource: 226 """Entrant roster, eligibility review, and CSV export.""" 227 return AdminRegistrationsResource(self._http)
Entrant roster, eligibility review, and CSV export.
229 @cached_property 230 def teams(self) -> AdminTeamsResource: 231 """Enforcement against a squad — disqualify and reinstate. 232 233 Separate from ``registrations`` on purpose: that one rules on 234 eligibility (which decides prizes), this one decides whether the 235 team is in the event at all. 236 """ 237 return AdminTeamsResource(self._http)
Enforcement against a squad — disqualify and reinstate.
Separate from registrations on purpose: that one rules on
eligibility (which decides prizes), this one decides whether the
team is in the event at all.
279 @property 280 def registry(self) -> AdminRegistryResource: 281 """Registry storage: the usage report and the manual reclaim.""" 282 return AdminRegistryResource(self._http)
Registry storage: the usage report and the manual reclaim.
292 def close(self) -> None: 293 """Close the underlying transport. 294 295 Only call this on a standalone client built via :meth:`connect`; 296 when reached via ``PlatformClient.admin`` the transport is shared 297 with — and closed by — the parent player client. 298 """ 299 self._http.close()
Close the underlying transport.
Only call this on a standalone client built via connect();
when reached via PlatformClient.admin the transport is shared
with — and closed by — the parent player client.