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)
class UsersResource:
 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).

UsersResource(http: ctfy.sdk.base.BaseHttpClient)
23    def __init__(self, http: BaseHttpClient) -> None:
24        self._http = http
def get(self, user_id: str) -> ctfy.server.models.UserInfo:
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.

def solve_trend( self, user_id: str, days: int = 30) -> ctfy.server.models.UserSolveTrend:
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.

def profile_stats( self, user_id: str, days: int = 30) -> ctfy.server.models.ProfileStats:
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.

def results(self, user_id: str) -> ctfy.server.models.ProfileResults:
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.

def agents(self, user_id: str) -> ctfy.server.models.ProfileAgents:
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.

def achievements(self, user_id: str) -> ctfy.server.models.UserAchievements:
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.

def authored_challenges( self, user_id: str, offset: int = 0, limit: int = 50) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.CreditedChallengeRow]:
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)

The published uploads naming this person, and how players received them: finishes across every event, the first finish, reactions.

Only uploads the author asked to be named for, and never one whose event withholds their entry from you. solves / reactions arrive None when withheld — a live silent exercise, or, for reactions, a caller without submissions:read — never 0.

def contributors( self, offset: int = 0, limit: int = 50) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.ContributorRow]:
 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.