ctfy.sdk.resources.users
client.users — public per-user profile reads.
1"""``client.users`` — public per-user profile reads.""" 2 3from __future__ import annotations 4 5from ctfy.sdk._helpers import PagedList, _extract_items, _raise_for_status 6from ctfy.sdk.base import BaseHttpClient 7from ctfy.server.models import ( 8 ContributorRow, 9 CreditedChallengeRow, 10 ProfileAgents, 11 ProfileResults, 12 ProfileStats, 13 UserAchievements, 14 UserInfo, 15 UserSolveTrend, 16) 17 18 19class UsersResource: 20 """Public profile reads for one user (privacy-scrubbed per their settings).""" 21 22 def __init__(self, http: BaseHttpClient) -> None: 23 self._http = http 24 25 def get(self, user_id: str) -> UserInfo: 26 """Public profile for one user. Privacy-scrubbed per the user's 27 ``profile_visibility`` settings for non-owner non-admin viewers.""" 28 resp = self._http.request("GET", f"/users/{user_id}") 29 _raise_for_status(resp) 30 return UserInfo.model_validate(resp.json()) 31 32 def solve_trend(self, user_id: str, days: int = 30) -> UserSolveTrend: 33 """Daily solve count over ``days`` (UTC). Privacy-aware: returns 34 an empty series when the viewer can't see the user's solve trend.""" 35 resp = self._http.request( 36 "GET", 37 f"/users/{user_id}/solve-trend", 38 params={"days": days}, 39 ) 40 _raise_for_status(resp) 41 return UserSolveTrend.model_validate(resp.json()) 42 43 def profile_stats(self, user_id: str, days: int = 30) -> ProfileStats: 44 """Public profile visualizations for one user (activity 45 calendar, per-tag radar, difficulty bar, solve trend). 46 Privacy-scrubbed for non-owner non-admin viewers.""" 47 resp = self._http.request("GET", f"/users/{user_id}/profile-stats", params={"days": days}) 48 _raise_for_status(resp) 49 return ProfileStats.model_validate(resp.json()) 50 51 def results(self, user_id: str) -> ProfileResults: 52 """The profile's 战绩 section: published final results of public 53 events, each at that event's own official placement. Never an 54 anonymous, disqualified, frozen or in-progress sitting; empty with 55 ``shown=False`` when the person turned the section off.""" 56 resp = self._http.request("GET", f"/users/{user_id}/results") 57 _raise_for_status(resp) 58 return ProfileResults.model_validate(resp.json()) 59 60 def agents(self, user_id: str) -> ProfileAgents: 61 """The profile's agents section: the person's own agents, each with 62 its agent-track results (each event's anonymity applied) and its best 63 result on a public suite. Empty with ``shown=False`` when the person 64 turned the section off.""" 65 resp = self._http.request("GET", f"/users/{user_id}/agents") 66 _raise_for_status(resp) 67 return ProfileAgents.model_validate(resp.json()) 68 69 def achievements(self, user_id: str) -> UserAchievements: 70 """The badges a person earned, as anybody else may see them. Empty 71 with ``shown=False`` when the person turned the section off.""" 72 resp = self._http.request("GET", f"/users/{user_id}/achievements") 73 _raise_for_status(resp) 74 return UserAchievements.model_validate(resp.json()) 75 76 def authored_challenges( 77 self, user_id: str, offset: int = 0, limit: int = 50 78 ) -> PagedList[CreditedChallengeRow]: 79 """The published uploads naming this person, and how players received 80 them: finishes across every event, the first finish, reactions. 81 82 Only uploads the author asked to be named for, and never one whose 83 event withholds their entry from you. ``solves`` / ``reactions`` 84 arrive ``None`` when withheld — a live silent exercise, or, for 85 ``reactions``, a caller without ``submissions:read`` — never ``0``. 86 """ 87 resp = self._http.request( 88 "GET", 89 f"/users/{user_id}/authored-challenges", 90 params={"offset": offset, "limit": limit}, 91 ) 92 _raise_for_status(resp) 93 return _extract_items(resp.json(), CreditedChallengeRow) 94 95 def contributors(self, offset: int = 0, limit: int = 50) -> PagedList[ContributorRow]: 96 """Everybody a published upload names, by name — the same rows as 97 :meth:`authored_challenges`, summed per person. A sum over an upload 98 whose numbers are withheld is ``None``, never a partial total.""" 99 resp = self._http.request("GET", "/contributors", params={"offset": offset, "limit": limit}) 100 _raise_for_status(resp) 101 return _extract_items(resp.json(), ContributorRow)
20class UsersResource: 21 """Public profile reads for one user (privacy-scrubbed per their settings).""" 22 23 def __init__(self, http: BaseHttpClient) -> None: 24 self._http = http 25 26 def get(self, user_id: str) -> UserInfo: 27 """Public profile for one user. Privacy-scrubbed per the user's 28 ``profile_visibility`` settings for non-owner non-admin viewers.""" 29 resp = self._http.request("GET", f"/users/{user_id}") 30 _raise_for_status(resp) 31 return UserInfo.model_validate(resp.json()) 32 33 def solve_trend(self, user_id: str, days: int = 30) -> UserSolveTrend: 34 """Daily solve count over ``days`` (UTC). Privacy-aware: returns 35 an empty series when the viewer can't see the user's solve trend.""" 36 resp = self._http.request( 37 "GET", 38 f"/users/{user_id}/solve-trend", 39 params={"days": days}, 40 ) 41 _raise_for_status(resp) 42 return UserSolveTrend.model_validate(resp.json()) 43 44 def profile_stats(self, user_id: str, days: int = 30) -> ProfileStats: 45 """Public profile visualizations for one user (activity 46 calendar, per-tag radar, difficulty bar, solve trend). 47 Privacy-scrubbed for non-owner non-admin viewers.""" 48 resp = self._http.request("GET", f"/users/{user_id}/profile-stats", params={"days": days}) 49 _raise_for_status(resp) 50 return ProfileStats.model_validate(resp.json()) 51 52 def results(self, user_id: str) -> ProfileResults: 53 """The profile's 战绩 section: published final results of public 54 events, each at that event's own official placement. Never an 55 anonymous, disqualified, frozen or in-progress sitting; empty with 56 ``shown=False`` when the person turned the section off.""" 57 resp = self._http.request("GET", f"/users/{user_id}/results") 58 _raise_for_status(resp) 59 return ProfileResults.model_validate(resp.json()) 60 61 def agents(self, user_id: str) -> ProfileAgents: 62 """The profile's agents section: the person's own agents, each with 63 its agent-track results (each event's anonymity applied) and its best 64 result on a public suite. Empty with ``shown=False`` when the person 65 turned the section off.""" 66 resp = self._http.request("GET", f"/users/{user_id}/agents") 67 _raise_for_status(resp) 68 return ProfileAgents.model_validate(resp.json()) 69 70 def achievements(self, user_id: str) -> UserAchievements: 71 """The badges a person earned, as anybody else may see them. Empty 72 with ``shown=False`` when the person turned the section off.""" 73 resp = self._http.request("GET", f"/users/{user_id}/achievements") 74 _raise_for_status(resp) 75 return UserAchievements.model_validate(resp.json()) 76 77 def authored_challenges( 78 self, user_id: str, offset: int = 0, limit: int = 50 79 ) -> PagedList[CreditedChallengeRow]: 80 """The published uploads naming this person, and how players received 81 them: finishes across every event, the first finish, reactions. 82 83 Only uploads the author asked to be named for, and never one whose 84 event withholds their entry from you. ``solves`` / ``reactions`` 85 arrive ``None`` when withheld — a live silent exercise, or, for 86 ``reactions``, a caller without ``submissions:read`` — never ``0``. 87 """ 88 resp = self._http.request( 89 "GET", 90 f"/users/{user_id}/authored-challenges", 91 params={"offset": offset, "limit": limit}, 92 ) 93 _raise_for_status(resp) 94 return _extract_items(resp.json(), CreditedChallengeRow) 95 96 def contributors(self, offset: int = 0, limit: int = 50) -> PagedList[ContributorRow]: 97 """Everybody a published upload names, by name — the same rows as 98 :meth:`authored_challenges`, summed per person. A sum over an upload 99 whose numbers are withheld is ``None``, never a partial total.""" 100 resp = self._http.request("GET", "/contributors", params={"offset": offset, "limit": limit}) 101 _raise_for_status(resp) 102 return _extract_items(resp.json(), ContributorRow)
Public profile reads for one user (privacy-scrubbed per their settings).
26 def get(self, user_id: str) -> UserInfo: 27 """Public profile for one user. Privacy-scrubbed per the user's 28 ``profile_visibility`` settings for non-owner non-admin viewers.""" 29 resp = self._http.request("GET", f"/users/{user_id}") 30 _raise_for_status(resp) 31 return UserInfo.model_validate(resp.json())
Public profile for one user. Privacy-scrubbed per the user's
profile_visibility settings for non-owner non-admin viewers.
33 def solve_trend(self, user_id: str, days: int = 30) -> UserSolveTrend: 34 """Daily solve count over ``days`` (UTC). Privacy-aware: returns 35 an empty series when the viewer can't see the user's solve trend.""" 36 resp = self._http.request( 37 "GET", 38 f"/users/{user_id}/solve-trend", 39 params={"days": days}, 40 ) 41 _raise_for_status(resp) 42 return UserSolveTrend.model_validate(resp.json())
Daily solve count over days (UTC). Privacy-aware: returns
an empty series when the viewer can't see the user's solve trend.
44 def profile_stats(self, user_id: str, days: int = 30) -> ProfileStats: 45 """Public profile visualizations for one user (activity 46 calendar, per-tag radar, difficulty bar, solve trend). 47 Privacy-scrubbed for non-owner non-admin viewers.""" 48 resp = self._http.request("GET", f"/users/{user_id}/profile-stats", params={"days": days}) 49 _raise_for_status(resp) 50 return ProfileStats.model_validate(resp.json())
Public profile visualizations for one user (activity calendar, per-tag radar, difficulty bar, solve trend). Privacy-scrubbed for non-owner non-admin viewers.
52 def results(self, user_id: str) -> ProfileResults: 53 """The profile's 战绩 section: published final results of public 54 events, each at that event's own official placement. Never an 55 anonymous, disqualified, frozen or in-progress sitting; empty with 56 ``shown=False`` when the person turned the section off.""" 57 resp = self._http.request("GET", f"/users/{user_id}/results") 58 _raise_for_status(resp) 59 return ProfileResults.model_validate(resp.json())
The profile's 战绩 section: published final results of public
events, each at that event's own official placement. Never an
anonymous, disqualified, frozen or in-progress sitting; empty with
shown=False when the person turned the section off.
61 def agents(self, user_id: str) -> ProfileAgents: 62 """The profile's agents section: the person's own agents, each with 63 its agent-track results (each event's anonymity applied) and its best 64 result on a public suite. Empty with ``shown=False`` when the person 65 turned the section off.""" 66 resp = self._http.request("GET", f"/users/{user_id}/agents") 67 _raise_for_status(resp) 68 return ProfileAgents.model_validate(resp.json())
The profile's agents section: the person's own agents, each with
its agent-track results (each event's anonymity applied) and its best
result on a public suite. Empty with shown=False when the person
turned the section off.
70 def achievements(self, user_id: str) -> UserAchievements: 71 """The badges a person earned, as anybody else may see them. Empty 72 with ``shown=False`` when the person turned the section off.""" 73 resp = self._http.request("GET", f"/users/{user_id}/achievements") 74 _raise_for_status(resp) 75 return UserAchievements.model_validate(resp.json())
The badges a person earned, as anybody else may see them. Empty
with shown=False when the person turned the section off.
96 def contributors(self, offset: int = 0, limit: int = 50) -> PagedList[ContributorRow]: 97 """Everybody a published upload names, by name — the same rows as 98 :meth:`authored_challenges`, summed per person. A sum over an upload 99 whose numbers are withheld is ``None``, never a partial total.""" 100 resp = self._http.request("GET", "/contributors", params={"offset": offset, "limit": limit}) 101 _raise_for_status(resp) 102 return _extract_items(resp.json(), ContributorRow)
Everybody a published upload names, by name — the same rows as
authored_challenges(), summed per person. A sum over an upload
whose numbers are withheld is None, never a partial total.