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    ProfileStats,
11    UserInfo,
12    UserSolveTrend,
13)
14
15
16class UsersResource:
17    """Public profile reads for one user (privacy-scrubbed per their settings)."""
18
19    def __init__(self, http: BaseHttpClient) -> None:
20        self._http = http
21
22    def get(self, user_id: str) -> UserInfo:
23        """Public profile for one user. Privacy-scrubbed per the user's
24        ``profile_visibility`` settings for non-owner non-admin viewers."""
25        resp = self._http.request("GET", f"/users/{user_id}")
26        _raise_for_status(resp)
27        return UserInfo.model_validate(resp.json())
28
29    def solve_trend(self, user_id: str, days: int = 30) -> UserSolveTrend:
30        """Daily solve count over ``days`` (UTC). Privacy-aware: returns
31        an empty series when the viewer can't see the user's solve trend."""
32        resp = self._http.request(
33            "GET",
34            f"/users/{user_id}/solve-trend",
35            params={"days": days},
36        )
37        _raise_for_status(resp)
38        return UserSolveTrend.model_validate(resp.json())
39
40    def profile_stats(self, user_id: str, days: int = 30) -> ProfileStats:
41        """Public profile visualizations for one user (activity
42        calendar, per-tag radar, difficulty bar, solve trend).
43        Privacy-scrubbed for non-owner non-admin viewers."""
44        resp = self._http.request("GET", f"/users/{user_id}/profile-stats", params={"days": days})
45        _raise_for_status(resp)
46        return ProfileStats.model_validate(resp.json())
47
48    def authored_challenges(
49        self, user_id: str, offset: int = 0, limit: int = 50
50    ) -> PagedList[CreditedChallengeRow]:
51        """The published uploads naming this person, and how players received
52        them: finishes across every event, the first finish, reactions.
53
54        Only uploads the author asked to be named for, and never one whose
55        event withholds their entry from you. ``solves`` / ``reactions``
56        arrive ``None`` when withheld — a live silent exercise, or, for
57        ``reactions``, a caller without ``submissions:read`` — never ``0``.
58        """
59        resp = self._http.request(
60            "GET",
61            f"/users/{user_id}/authored-challenges",
62            params={"offset": offset, "limit": limit},
63        )
64        _raise_for_status(resp)
65        return _extract_items(resp.json(), CreditedChallengeRow)
66
67    def contributors(self, offset: int = 0, limit: int = 50) -> PagedList[ContributorRow]:
68        """Everybody a published upload names, by name — the same rows as
69        :meth:`authored_challenges`, summed per person. A sum over an upload
70        whose numbers are withheld is ``None``, never a partial total."""
71        resp = self._http.request("GET", "/contributors", params={"offset": offset, "limit": limit})
72        _raise_for_status(resp)
73        return _extract_items(resp.json(), ContributorRow)
class UsersResource:
17class UsersResource:
18    """Public profile reads for one user (privacy-scrubbed per their settings)."""
19
20    def __init__(self, http: BaseHttpClient) -> None:
21        self._http = http
22
23    def get(self, user_id: str) -> UserInfo:
24        """Public profile for one user. Privacy-scrubbed per the user's
25        ``profile_visibility`` settings for non-owner non-admin viewers."""
26        resp = self._http.request("GET", f"/users/{user_id}")
27        _raise_for_status(resp)
28        return UserInfo.model_validate(resp.json())
29
30    def solve_trend(self, user_id: str, days: int = 30) -> UserSolveTrend:
31        """Daily solve count over ``days`` (UTC). Privacy-aware: returns
32        an empty series when the viewer can't see the user's solve trend."""
33        resp = self._http.request(
34            "GET",
35            f"/users/{user_id}/solve-trend",
36            params={"days": days},
37        )
38        _raise_for_status(resp)
39        return UserSolveTrend.model_validate(resp.json())
40
41    def profile_stats(self, user_id: str, days: int = 30) -> ProfileStats:
42        """Public profile visualizations for one user (activity
43        calendar, per-tag radar, difficulty bar, solve trend).
44        Privacy-scrubbed for non-owner non-admin viewers."""
45        resp = self._http.request("GET", f"/users/{user_id}/profile-stats", params={"days": days})
46        _raise_for_status(resp)
47        return ProfileStats.model_validate(resp.json())
48
49    def authored_challenges(
50        self, user_id: str, offset: int = 0, limit: int = 50
51    ) -> PagedList[CreditedChallengeRow]:
52        """The published uploads naming this person, and how players received
53        them: finishes across every event, the first finish, reactions.
54
55        Only uploads the author asked to be named for, and never one whose
56        event withholds their entry from you. ``solves`` / ``reactions``
57        arrive ``None`` when withheld — a live silent exercise, or, for
58        ``reactions``, a caller without ``submissions:read`` — never ``0``.
59        """
60        resp = self._http.request(
61            "GET",
62            f"/users/{user_id}/authored-challenges",
63            params={"offset": offset, "limit": limit},
64        )
65        _raise_for_status(resp)
66        return _extract_items(resp.json(), CreditedChallengeRow)
67
68    def contributors(self, offset: int = 0, limit: int = 50) -> PagedList[ContributorRow]:
69        """Everybody a published upload names, by name — the same rows as
70        :meth:`authored_challenges`, summed per person. A sum over an upload
71        whose numbers are withheld is ``None``, never a partial total."""
72        resp = self._http.request("GET", "/contributors", params={"offset": offset, "limit": limit})
73        _raise_for_status(resp)
74        return _extract_items(resp.json(), ContributorRow)

Public profile reads for one user (privacy-scrubbed per their settings).

UsersResource(http: ctfy.sdk.base.BaseHttpClient)
20    def __init__(self, http: BaseHttpClient) -> None:
21        self._http = http
def get(self, user_id: str) -> ctfy.server.models.UserInfo:
23    def get(self, user_id: str) -> UserInfo:
24        """Public profile for one user. Privacy-scrubbed per the user's
25        ``profile_visibility`` settings for non-owner non-admin viewers."""
26        resp = self._http.request("GET", f"/users/{user_id}")
27        _raise_for_status(resp)
28        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:
30    def solve_trend(self, user_id: str, days: int = 30) -> UserSolveTrend:
31        """Daily solve count over ``days`` (UTC). Privacy-aware: returns
32        an empty series when the viewer can't see the user's solve trend."""
33        resp = self._http.request(
34            "GET",
35            f"/users/{user_id}/solve-trend",
36            params={"days": days},
37        )
38        _raise_for_status(resp)
39        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:
41    def profile_stats(self, user_id: str, days: int = 30) -> ProfileStats:
42        """Public profile visualizations for one user (activity
43        calendar, per-tag radar, difficulty bar, solve trend).
44        Privacy-scrubbed for non-owner non-admin viewers."""
45        resp = self._http.request("GET", f"/users/{user_id}/profile-stats", params={"days": days})
46        _raise_for_status(resp)
47        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 authored_challenges( self, user_id: str, offset: int = 0, limit: int = 50) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.CreditedChallengeRow]:
49    def authored_challenges(
50        self, user_id: str, offset: int = 0, limit: int = 50
51    ) -> PagedList[CreditedChallengeRow]:
52        """The published uploads naming this person, and how players received
53        them: finishes across every event, the first finish, reactions.
54
55        Only uploads the author asked to be named for, and never one whose
56        event withholds their entry from you. ``solves`` / ``reactions``
57        arrive ``None`` when withheld — a live silent exercise, or, for
58        ``reactions``, a caller without ``submissions:read`` — never ``0``.
59        """
60        resp = self._http.request(
61            "GET",
62            f"/users/{user_id}/authored-challenges",
63            params={"offset": offset, "limit": limit},
64        )
65        _raise_for_status(resp)
66        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]:
68    def contributors(self, offset: int = 0, limit: int = 50) -> PagedList[ContributorRow]:
69        """Everybody a published upload names, by name — the same rows as
70        :meth:`authored_challenges`, summed per person. A sum over an upload
71        whose numbers are withheld is ``None``, never a partial total."""
72        resp = self._http.request("GET", "/contributors", params={"offset": offset, "limit": limit})
73        _raise_for_status(resp)
74        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.