ctfy.sdk.resources.me
client.me — the calling user's profile, inbox, account + progress.
1"""``client.me`` — the calling user's profile, inbox, account + progress.""" 2 3from __future__ import annotations 4 5from typing import Any 6 7from ctfy.sdk._helpers import _raise_for_status 8from ctfy.sdk.base import BaseHttpClient 9from ctfy.server.models import ( 10 InboxResponse, 11 MeResponse, 12 MilestoneProgress, 13 MyAchievementsResponse, 14 MySolveSummary, 15 StarGazerVerifyResponse, 16) 17 18 19class MeResource: 20 """Self-service surface for the authenticated caller.""" 21 22 def __init__(self, http: BaseHttpClient) -> None: 23 self._http = http 24 25 def get(self) -> MeResponse: 26 """Get the calling user's profile + auth context. 27 28 Per-competition team memberships ride along on 29 ``competition_teams``; there is no global "current team" any 30 more — pick the row whose ``competition_id`` matches the 31 scope you care about. 32 """ 33 resp = self._http.request("GET", "/me") 34 _raise_for_status(resp) 35 return MeResponse.model_validate(resp.json()) 36 37 def inbox(self) -> InboxResponse: 38 """Pending team invites, join requests, direct invites, announcements.""" 39 resp = self._http.request("GET", "/me/inbox") 40 _raise_for_status(resp) 41 return InboxResponse.model_validate(resp.json()) 42 43 def read_announcement(self, announcement_id: str) -> None: 44 """Mark one announcement as read for the calling user.""" 45 resp = self._http.request("POST", f"/me/announcements/{announcement_id}/read") 46 _raise_for_status(resp) 47 48 def read_all_announcements(self) -> None: 49 """Mark every relevant announcement as read.""" 50 resp = self._http.request("POST", "/me/announcements/read-all") 51 _raise_for_status(resp) 52 53 def update_profile(self, **fields: Any) -> MeResponse: 54 """PATCH the caller's profile. 55 56 Pass any subset of ``bio`` / ``country`` / ``website_url`` / 57 ``timezone`` / ``social_links``. Omit a key to leave it unchanged; 58 pass ``None`` to clear it. Returns the updated :class:`MeResponse`. 59 60 Example:: 61 62 client.me.update_profile(bio="hacking goblins", country="JP") 63 client.me.update_profile(website_url=None) # clear it 64 """ 65 resp = self._http.request("PATCH", "/me/profile", json=fields) 66 _raise_for_status(resp) 67 return MeResponse.model_validate(resp.json()) 68 69 def update_visibility(self, overrides: dict[str, bool]) -> MeResponse: 70 """PATCH per-field privacy flags on the caller's profile. 71 72 Keys outside :data:`PROFILE_VISIBILITY_KEYS` are silently dropped 73 server-side. Returns the updated :class:`MeResponse`. 74 """ 75 resp = self._http.request("PATCH", "/me/profile/visibility", json=overrides) 76 _raise_for_status(resp) 77 return MeResponse.model_validate(resp.json()) 78 79 def export_account(self) -> bytes: 80 """Download a ZIP archive of every piece of data the platform 81 holds about the caller (user, identities, tokens, memberships, 82 submissions, solves, activity). Returns raw bytes — write to a 83 file with ``Path(...).write_bytes(client.me.export_account())``.""" 84 resp = self._http.request("GET", "/me/export") 85 _raise_for_status(resp) 86 return resp.content 87 88 def delete_account(self, confirm_display_name: str) -> None: 89 """Self-delete the calling user. ``confirm_display_name`` must 90 match the caller's display name (falls back to email when empty) 91 exactly — typo gate against fat-fingered destruction.""" 92 resp = self._http.request( 93 "DELETE", 94 "/me", 95 json={"confirm_display_name": confirm_display_name}, 96 ) 97 _raise_for_status(resp) 98 99 def achievements(self, competition_id: str = "") -> MyAchievementsResponse: 100 """Caller's unlocked + locked badges (with progress hints).""" 101 params: dict[str, str] = {} 102 if competition_id: 103 params["competition_id"] = competition_id 104 resp = self._http.request("GET", "/me/achievements", params=params) 105 _raise_for_status(resp) 106 return MyAchievementsResponse.model_validate(resp.json()) 107 108 def solves(self) -> list[MySolveSummary]: 109 """Per-challenge solve summary across every team the caller has played for.""" 110 resp = self._http.request("GET", "/me/solves") 111 _raise_for_status(resp) 112 return [MySolveSummary.model_validate(s) for s in resp.json()] 113 114 def milestone_progress(self, *, competition_id: str = "") -> list[MilestoneProgress]: 115 """Partial-solve progress per challenge for the caller. 116 117 One row per challenge with at least one captured question; 118 ``solved_question_ids`` enumerates which milestones the user 119 has cleared, ``total_questions`` is the spec's declared count. 120 With ``competition_id`` set the result narrows to solves 121 stamped against the caller's team in that comp. 122 """ 123 params: dict[str, str] = {} 124 if competition_id: 125 params["competition_id"] = competition_id 126 resp = self._http.request("GET", "/me/milestone-progress", params=params) 127 _raise_for_status(resp) 128 return [MilestoneProgress.model_validate(s) for s in resp.json()] 129 130 def verify_star_gazer(self, competition_id: str = "") -> StarGazerVerifyResponse: 131 """Verify the caller starred the ctfy GitHub repo and grant the 132 ``star_gazer`` badge if so. 133 134 Friendly failures (no GitHub identity, repo not configured, not 135 yet starred, GitHub rate-limited) come back as 200 with 136 ``verified=False`` + an actionable ``reason``.""" 137 params = {"competition_id": competition_id} if competition_id else None 138 resp = self._http.request("POST", "/me/star-gazer/verify", params=params) 139 _raise_for_status(resp) 140 return StarGazerVerifyResponse.model_validate(resp.json())
20class MeResource: 21 """Self-service surface for the authenticated caller.""" 22 23 def __init__(self, http: BaseHttpClient) -> None: 24 self._http = http 25 26 def get(self) -> MeResponse: 27 """Get the calling user's profile + auth context. 28 29 Per-competition team memberships ride along on 30 ``competition_teams``; there is no global "current team" any 31 more — pick the row whose ``competition_id`` matches the 32 scope you care about. 33 """ 34 resp = self._http.request("GET", "/me") 35 _raise_for_status(resp) 36 return MeResponse.model_validate(resp.json()) 37 38 def inbox(self) -> InboxResponse: 39 """Pending team invites, join requests, direct invites, announcements.""" 40 resp = self._http.request("GET", "/me/inbox") 41 _raise_for_status(resp) 42 return InboxResponse.model_validate(resp.json()) 43 44 def read_announcement(self, announcement_id: str) -> None: 45 """Mark one announcement as read for the calling user.""" 46 resp = self._http.request("POST", f"/me/announcements/{announcement_id}/read") 47 _raise_for_status(resp) 48 49 def read_all_announcements(self) -> None: 50 """Mark every relevant announcement as read.""" 51 resp = self._http.request("POST", "/me/announcements/read-all") 52 _raise_for_status(resp) 53 54 def update_profile(self, **fields: Any) -> MeResponse: 55 """PATCH the caller's profile. 56 57 Pass any subset of ``bio`` / ``country`` / ``website_url`` / 58 ``timezone`` / ``social_links``. Omit a key to leave it unchanged; 59 pass ``None`` to clear it. Returns the updated :class:`MeResponse`. 60 61 Example:: 62 63 client.me.update_profile(bio="hacking goblins", country="JP") 64 client.me.update_profile(website_url=None) # clear it 65 """ 66 resp = self._http.request("PATCH", "/me/profile", json=fields) 67 _raise_for_status(resp) 68 return MeResponse.model_validate(resp.json()) 69 70 def update_visibility(self, overrides: dict[str, bool]) -> MeResponse: 71 """PATCH per-field privacy flags on the caller's profile. 72 73 Keys outside :data:`PROFILE_VISIBILITY_KEYS` are silently dropped 74 server-side. Returns the updated :class:`MeResponse`. 75 """ 76 resp = self._http.request("PATCH", "/me/profile/visibility", json=overrides) 77 _raise_for_status(resp) 78 return MeResponse.model_validate(resp.json()) 79 80 def export_account(self) -> bytes: 81 """Download a ZIP archive of every piece of data the platform 82 holds about the caller (user, identities, tokens, memberships, 83 submissions, solves, activity). Returns raw bytes — write to a 84 file with ``Path(...).write_bytes(client.me.export_account())``.""" 85 resp = self._http.request("GET", "/me/export") 86 _raise_for_status(resp) 87 return resp.content 88 89 def delete_account(self, confirm_display_name: str) -> None: 90 """Self-delete the calling user. ``confirm_display_name`` must 91 match the caller's display name (falls back to email when empty) 92 exactly — typo gate against fat-fingered destruction.""" 93 resp = self._http.request( 94 "DELETE", 95 "/me", 96 json={"confirm_display_name": confirm_display_name}, 97 ) 98 _raise_for_status(resp) 99 100 def achievements(self, competition_id: str = "") -> MyAchievementsResponse: 101 """Caller's unlocked + locked badges (with progress hints).""" 102 params: dict[str, str] = {} 103 if competition_id: 104 params["competition_id"] = competition_id 105 resp = self._http.request("GET", "/me/achievements", params=params) 106 _raise_for_status(resp) 107 return MyAchievementsResponse.model_validate(resp.json()) 108 109 def solves(self) -> list[MySolveSummary]: 110 """Per-challenge solve summary across every team the caller has played for.""" 111 resp = self._http.request("GET", "/me/solves") 112 _raise_for_status(resp) 113 return [MySolveSummary.model_validate(s) for s in resp.json()] 114 115 def milestone_progress(self, *, competition_id: str = "") -> list[MilestoneProgress]: 116 """Partial-solve progress per challenge for the caller. 117 118 One row per challenge with at least one captured question; 119 ``solved_question_ids`` enumerates which milestones the user 120 has cleared, ``total_questions`` is the spec's declared count. 121 With ``competition_id`` set the result narrows to solves 122 stamped against the caller's team in that comp. 123 """ 124 params: dict[str, str] = {} 125 if competition_id: 126 params["competition_id"] = competition_id 127 resp = self._http.request("GET", "/me/milestone-progress", params=params) 128 _raise_for_status(resp) 129 return [MilestoneProgress.model_validate(s) for s in resp.json()] 130 131 def verify_star_gazer(self, competition_id: str = "") -> StarGazerVerifyResponse: 132 """Verify the caller starred the ctfy GitHub repo and grant the 133 ``star_gazer`` badge if so. 134 135 Friendly failures (no GitHub identity, repo not configured, not 136 yet starred, GitHub rate-limited) come back as 200 with 137 ``verified=False`` + an actionable ``reason``.""" 138 params = {"competition_id": competition_id} if competition_id else None 139 resp = self._http.request("POST", "/me/star-gazer/verify", params=params) 140 _raise_for_status(resp) 141 return StarGazerVerifyResponse.model_validate(resp.json())
Self-service surface for the authenticated caller.
26 def get(self) -> MeResponse: 27 """Get the calling user's profile + auth context. 28 29 Per-competition team memberships ride along on 30 ``competition_teams``; there is no global "current team" any 31 more — pick the row whose ``competition_id`` matches the 32 scope you care about. 33 """ 34 resp = self._http.request("GET", "/me") 35 _raise_for_status(resp) 36 return MeResponse.model_validate(resp.json())
Get the calling user's profile + auth context.
Per-competition team memberships ride along on
competition_teams; there is no global "current team" any
more — pick the row whose competition_id matches the
scope you care about.
38 def inbox(self) -> InboxResponse: 39 """Pending team invites, join requests, direct invites, announcements.""" 40 resp = self._http.request("GET", "/me/inbox") 41 _raise_for_status(resp) 42 return InboxResponse.model_validate(resp.json())
Pending team invites, join requests, direct invites, announcements.
44 def read_announcement(self, announcement_id: str) -> None: 45 """Mark one announcement as read for the calling user.""" 46 resp = self._http.request("POST", f"/me/announcements/{announcement_id}/read") 47 _raise_for_status(resp)
Mark one announcement as read for the calling user.
49 def read_all_announcements(self) -> None: 50 """Mark every relevant announcement as read.""" 51 resp = self._http.request("POST", "/me/announcements/read-all") 52 _raise_for_status(resp)
Mark every relevant announcement as read.
54 def update_profile(self, **fields: Any) -> MeResponse: 55 """PATCH the caller's profile. 56 57 Pass any subset of ``bio`` / ``country`` / ``website_url`` / 58 ``timezone`` / ``social_links``. Omit a key to leave it unchanged; 59 pass ``None`` to clear it. Returns the updated :class:`MeResponse`. 60 61 Example:: 62 63 client.me.update_profile(bio="hacking goblins", country="JP") 64 client.me.update_profile(website_url=None) # clear it 65 """ 66 resp = self._http.request("PATCH", "/me/profile", json=fields) 67 _raise_for_status(resp) 68 return MeResponse.model_validate(resp.json())
PATCH the caller's profile.
Pass any subset of bio / country / website_url /
timezone / social_links. Omit a key to leave it unchanged;
pass None to clear it. Returns the updated MeResponse.
Example::
client.me.update_profile(bio="hacking goblins", country="JP")
client.me.update_profile(website_url=None) # clear it
70 def update_visibility(self, overrides: dict[str, bool]) -> MeResponse: 71 """PATCH per-field privacy flags on the caller's profile. 72 73 Keys outside :data:`PROFILE_VISIBILITY_KEYS` are silently dropped 74 server-side. Returns the updated :class:`MeResponse`. 75 """ 76 resp = self._http.request("PATCH", "/me/profile/visibility", json=overrides) 77 _raise_for_status(resp) 78 return MeResponse.model_validate(resp.json())
PATCH per-field privacy flags on the caller's profile.
Keys outside PROFILE_VISIBILITY_KEYS are silently dropped
server-side. Returns the updated MeResponse.
80 def export_account(self) -> bytes: 81 """Download a ZIP archive of every piece of data the platform 82 holds about the caller (user, identities, tokens, memberships, 83 submissions, solves, activity). Returns raw bytes — write to a 84 file with ``Path(...).write_bytes(client.me.export_account())``.""" 85 resp = self._http.request("GET", "/me/export") 86 _raise_for_status(resp) 87 return resp.content
Download a ZIP archive of every piece of data the platform
holds about the caller (user, identities, tokens, memberships,
submissions, solves, activity). Returns raw bytes — write to a
file with Path(...).write_bytes(client.me.export_account()).
89 def delete_account(self, confirm_display_name: str) -> None: 90 """Self-delete the calling user. ``confirm_display_name`` must 91 match the caller's display name (falls back to email when empty) 92 exactly — typo gate against fat-fingered destruction.""" 93 resp = self._http.request( 94 "DELETE", 95 "/me", 96 json={"confirm_display_name": confirm_display_name}, 97 ) 98 _raise_for_status(resp)
Self-delete the calling user. confirm_display_name must
match the caller's display name (falls back to email when empty)
exactly — typo gate against fat-fingered destruction.
100 def achievements(self, competition_id: str = "") -> MyAchievementsResponse: 101 """Caller's unlocked + locked badges (with progress hints).""" 102 params: dict[str, str] = {} 103 if competition_id: 104 params["competition_id"] = competition_id 105 resp = self._http.request("GET", "/me/achievements", params=params) 106 _raise_for_status(resp) 107 return MyAchievementsResponse.model_validate(resp.json())
Caller's unlocked + locked badges (with progress hints).
109 def solves(self) -> list[MySolveSummary]: 110 """Per-challenge solve summary across every team the caller has played for.""" 111 resp = self._http.request("GET", "/me/solves") 112 _raise_for_status(resp) 113 return [MySolveSummary.model_validate(s) for s in resp.json()]
Per-challenge solve summary across every team the caller has played for.
115 def milestone_progress(self, *, competition_id: str = "") -> list[MilestoneProgress]: 116 """Partial-solve progress per challenge for the caller. 117 118 One row per challenge with at least one captured question; 119 ``solved_question_ids`` enumerates which milestones the user 120 has cleared, ``total_questions`` is the spec's declared count. 121 With ``competition_id`` set the result narrows to solves 122 stamped against the caller's team in that comp. 123 """ 124 params: dict[str, str] = {} 125 if competition_id: 126 params["competition_id"] = competition_id 127 resp = self._http.request("GET", "/me/milestone-progress", params=params) 128 _raise_for_status(resp) 129 return [MilestoneProgress.model_validate(s) for s in resp.json()]
Partial-solve progress per challenge for the caller.
One row per challenge with at least one captured question;
solved_question_ids enumerates which milestones the user
has cleared, total_questions is the spec's declared count.
With competition_id set the result narrows to solves
stamped against the caller's team in that comp.
131 def verify_star_gazer(self, competition_id: str = "") -> StarGazerVerifyResponse: 132 """Verify the caller starred the ctfy GitHub repo and grant the 133 ``star_gazer`` badge if so. 134 135 Friendly failures (no GitHub identity, repo not configured, not 136 yet starred, GitHub rate-limited) come back as 200 with 137 ``verified=False`` + an actionable ``reason``.""" 138 params = {"competition_id": competition_id} if competition_id else None 139 resp = self._http.request("POST", "/me/star-gazer/verify", params=params) 140 _raise_for_status(resp) 141 return StarGazerVerifyResponse.model_validate(resp.json())
Verify the caller starred the ctfy GitHub repo and grant the
star_gazer badge if so.
Friendly failures (no GitHub identity, repo not configured, not
yet starred, GitHub rate-limited) come back as 200 with
verified=False + an actionable reason.