sweatstack 0.77.1__tar.gz → 0.80.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/settings.local.json +5 -1
  2. {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/skills/sweatstack-python/fastapi.md +78 -0
  3. {sweatstack-0.77.1 → sweatstack-0.80.0}/AGENTS.md +0 -22
  4. {sweatstack-0.77.1 → sweatstack-0.80.0}/CHANGELOG.md +28 -0
  5. {sweatstack-0.77.1 → sweatstack-0.80.0}/PKG-INFO +2 -3
  6. sweatstack-0.80.0/plans/004_codebase_hygiene.md +204 -0
  7. sweatstack-0.80.0/plans/005_ost_sport_bridge.md +515 -0
  8. {sweatstack-0.77.1 → sweatstack-0.80.0}/pyproject.toml +2 -3
  9. sweatstack-0.80.0/src/sweatstack/__init__.py +26 -0
  10. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/client.py +171 -139
  11. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/__init__.py +11 -0
  12. sweatstack-0.80.0/src/sweatstack/fastapi/access_token_cache.py +287 -0
  13. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/config.py +12 -1
  14. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/dependencies.py +203 -42
  15. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/schemas.py +77 -4
  16. sweatstack-0.80.0/tests/test_access_token_cache.py +577 -0
  17. sweatstack-0.80.0/tests/test_public_surface.py +69 -0
  18. sweatstack-0.80.0/tests/test_sport_ost_compat.py +123 -0
  19. {sweatstack-0.77.1 → sweatstack-0.80.0}/uv.lock +1 -1
  20. sweatstack-0.77.1/src/sweatstack/__init__.py +0 -12
  21. {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
  22. {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/skills/sweatstack-python/client.md +0 -0
  23. {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
  24. {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
  25. {sweatstack-0.77.1 → sweatstack-0.80.0}/.gitignore +0 -0
  26. {sweatstack-0.77.1 → sweatstack-0.80.0}/.python-version +0 -0
  27. {sweatstack-0.77.1 → sweatstack-0.80.0}/CONTRIBUTING.md +0 -0
  28. {sweatstack-0.77.1 → sweatstack-0.80.0}/DEVELOPMENT.md +0 -0
  29. {sweatstack-0.77.1 → sweatstack-0.80.0}/LICENSE +0 -0
  30. {sweatstack-0.77.1 → sweatstack-0.80.0}/Makefile +0 -0
  31. {sweatstack-0.77.1 → sweatstack-0.80.0}/README.md +0 -0
  32. {sweatstack-0.77.1 → sweatstack-0.80.0}/docs/conf.py +0 -0
  33. {sweatstack-0.77.1 → sweatstack-0.80.0}/docs/everything.rst +0 -0
  34. {sweatstack-0.77.1 → sweatstack-0.80.0}/docs/index.rst +0 -0
  35. {sweatstack-0.77.1 → sweatstack-0.80.0}/examples/fastapi_webhooks_example.py +0 -0
  36. {sweatstack-0.77.1 → sweatstack-0.80.0}/examples/send_webhook.py +0 -0
  37. {sweatstack-0.77.1 → sweatstack-0.80.0}/plans/001a_tests.md +0 -0
  38. {sweatstack-0.77.1 → sweatstack-0.80.0}/plans/001b_metadata.md +0 -0
  39. {sweatstack-0.77.1 → sweatstack-0.80.0}/plans/001c_dailies.md +0 -0
  40. {sweatstack-0.77.1 → sweatstack-0.80.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
  41. {sweatstack-0.77.1 → sweatstack-0.80.0}/plans/003_trace_test_linking.md +0 -0
  42. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  43. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/cli.py +0 -0
  44. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/constants.py +0 -0
  45. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/exceptions.py +0 -0
  46. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/models.py +0 -0
  47. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/routes.py +0 -0
  48. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/session.py +0 -0
  49. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/token_stores.py +0 -0
  50. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/webhooks.py +0 -0
  51. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/ipython_init.py +0 -0
  52. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  53. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/openapi_schemas.py +0 -0
  54. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/py.typed +0 -0
  55. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/streamlit.py +0 -0
  56. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/sweatshell.py +0 -0
  57. {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/utils.py +0 -0
  58. {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/__init__.py +0 -0
  59. {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_dailies.py +0 -0
  60. {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_dtype_conversion.py +0 -0
  61. {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_exceptions.py +0 -0
  62. {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_metadata.py +0 -0
  63. {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_teams.py +0 -0
  64. {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_tests.py +0 -0
  65. {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_trace_test_linking.py +0 -0
  66. {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_webhooks.py +0 -0
@@ -23,7 +23,11 @@
23
23
  "mcp__sentry__get_sentry_resource",
24
24
  "Bash(awk *)",
25
25
  "Bash(git show *)",
26
- "Read(//tmp/**)"
26
+ "Read(//tmp/**)",
27
+ "WebFetch(domain:pypi.org)",
28
+ "WebFetch(domain:github.com)",
29
+ "WebFetch(domain:raw.githubusercontent.com)",
30
+ "Bash(uv add *)"
27
31
  ],
28
32
  "deny": []
29
33
  }
@@ -11,6 +11,7 @@ Requires: `pip install sweatstack[fastapi]`
11
11
  - [User Delegation](#user-delegation)
12
12
  - [Webhooks](#webhooks)
13
13
  - [Token Stores](#token-stores)
14
+ - [Access Token Cache](#access-token-cache)
14
15
 
15
16
  ---
16
17
 
@@ -55,6 +56,7 @@ configure(
55
56
  redirect_unauthenticated: bool = True, # True = redirect to login, False = return 401
56
57
  webhook_secret: str = None, # SWEATSTACK_WEBHOOK_SECRET
57
58
  token_store: TokenStore = None, # for webhook user token persistence
59
+ access_token_cache: AccessTokenCache = None, # default: in-process LRU. See "Access Token Cache"
58
60
  )
59
61
  ```
60
62
 
@@ -207,3 +209,79 @@ class RedisTokenStore(TokenStore):
207
209
  ```
208
210
 
209
211
  `StoredTokens` fields: `user_id`, `access_token`, `refresh_token`, `expires_at: datetime`.
212
+
213
+ ## Access Token Cache
214
+
215
+ When a single page-load fans out into many concurrent requests, the cookie's access token may be on the edge of expiry for several of them at once. Without coordination, each request independently calls `/oauth/token` with the same refresh token, producing a burst of duplicate refreshes. The access-token cache collapses this to a single call: peers serialise on a per-session lock and share the result.
216
+
217
+ The default `InMemoryAccessTokenCache` is correct for single-worker FastAPI deployments. It is **LRU-bounded** (10k entries) so memory is capped regardless of session churn, and uses **striped locking** (64 stripes) so eviction never coordinates with in-flight refreshes.
218
+
219
+ ### Tuning
220
+
221
+ ```python
222
+ from sweatstack.fastapi import configure, InMemoryAccessTokenCache
223
+
224
+ configure(
225
+ ...,
226
+ access_token_cache=InMemoryAccessTokenCache(
227
+ max_entries=50_000, # high session-churn deployments
228
+ lock_stripes=128, # very high concurrent refresh counts
229
+ ),
230
+ )
231
+ ```
232
+
233
+ ### Multi-worker deployments
234
+
235
+ For multiple FastAPI workers (uvicorn `--workers > 1`, Gunicorn, etc.) the in-memory cache de-duplicates **within** each worker, not across them. Cross-worker de-duplication requires a shared-state implementation. Implement the `AccessTokenCache` Protocol — `get`, `set`, `invalidate`, `migrate`, `lock` — and pass it to `configure()`.
236
+
237
+ ```python
238
+ from sweatstack.fastapi import AccessTokenCache, CachedAccessToken
239
+ # Sketch: Redis-backed implementation (hash the key, never store the raw refresh token).
240
+ ```
241
+
242
+ The Protocol surface is documented in detail in `sweatstack/fastapi/access_token_cache.py`. The call pattern (`check → lock → recheck → refresh → set/migrate`) is in the `AccessTokenCache` docstring.
243
+
244
+ ### Timeouts and the `RefreshLockTimeout` exception
245
+
246
+ The refresh path has two bounds, both intended as defence-in-depth rather than knobs users normally tune:
247
+
248
+ | Constant | Default | What it bounds |
249
+ |---|---|---|
250
+ | `REFRESH_HTTP_TIMEOUT` | 10s read, 5s connect | The `/oauth/token` HTTP call |
251
+ | `REFRESH_LOCK_TIMEOUT` | 15s | Waiting for a peer's refresh to finish |
252
+
253
+ A waiter that cannot acquire the per-session lock within 15s raises `RefreshLockTimeout` rather than pinning a threadpool worker. In the cookie-based dependency path it surfaces as a 401; in the webhook path it is wrapped as `WebhookTokenRefreshError`. You don't normally catch it; if you want to wire it to metrics:
254
+
255
+ ```python
256
+ from sweatstack.fastapi import RefreshLockTimeout
257
+
258
+ @app.exception_handler(RefreshLockTimeout)
259
+ async def on_refresh_timeout(request, exc):
260
+ metrics.increment("oauth.refresh.lock_timeout")
261
+ raise # let the normal 401 / WebhookTokenRefreshError path run
262
+ ```
263
+
264
+ ### Refresh-token rotation
265
+
266
+ The current SweatStack `/oauth/token` endpoint does not rotate refresh tokens, but the cache is rotation-aware: if the server ever starts returning a new refresh token, it is captured, the cookie / token store is rewritten with it, and the cache installs the result under both the old and new keys (so in-flight peers still arriving with the old refresh token continue to hit).
267
+
268
+ ### Debugging
269
+
270
+ Enable debug logs to see every cache decision keyed by a short SHA-256 fingerprint of the refresh token (never the raw secret):
271
+
272
+ ```python
273
+ import logging
274
+ logging.getLogger("sweatstack.fastapi.dependencies").setLevel(logging.DEBUG)
275
+ ```
276
+
277
+ Typical output for a 5-way concurrent fan-out on an expiring session:
278
+
279
+ ```
280
+ DEBUG access-token refresh completed (session=4f2a91c0)
281
+ DEBUG access-token cache hit after lock (peer refreshed; session=4f2a91c0)
282
+ DEBUG access-token cache hit after lock (peer refreshed; session=4f2a91c0)
283
+ DEBUG access-token cache hit (session=4f2a91c0)
284
+ DEBUG access-token cache hit (session=4f2a91c0)
285
+ ```
286
+
287
+ One refresh, four cache hits.
@@ -214,28 +214,6 @@ Bad (belongs in the commit message, not the changelog):
214
214
  > `BodyConnectAddEmail...`.
215
215
 
216
216
 
217
- ## Known rough edges
218
-
219
- These are conventions we live with but would reconsider in a rewrite.
220
- Don't paper over them in new code; flag them in review.
221
-
222
- - **Singleton functions are injected via `globals()`** at module import.
223
- The "is this method a singleton?" rule is unwritten, which is exactly
224
- why `update_trace` and `delete_trace` are missing from the list while
225
- `update_test` and `delete_test` are present. When you touch an
226
- unregistered public method, register it.
227
- - **`__init__.py` uses `from .client import *`.** Explicit re-exports
228
- would be safer; we haven't done the work.
229
- - **Docstring exception drift.** Most existing docstrings still say
230
- `HTTPStatusError`. Fix as you touch them, but a one-shot sweep is also
231
- welcome.
232
- - **`pyproject.toml` claims `requires-python = ">=3.9"`** while the code
233
- uses `X | Y` union syntax (3.10+). The real floor is 3.10. Bump the
234
- pin when convenient.
235
- - **No CI type-checker or linter.** For a typed library, this is a gap.
236
- Don't let new code make it worse.
237
-
238
-
239
217
  ## When in doubt
240
218
 
241
219
  - Match the nearest existing method in `client.py`. Consistency with the
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.80.0] - 2026-06-12
9
+
10
+ ### Changed
11
+ - Makes the Sport enum forward compatible with OpenSportTaxonomy sports.
12
+
13
+
14
+ ## [0.79.0] - 2026-05-28
15
+
16
+ ### Fixed
17
+ - `sweatstack.fastapi`: a single page load that fans out into many concurrent requests no longer produces a burst of duplicate `/oauth/token` refreshes when the session's access token is on the edge of expiring. Concurrent requests for the same session now serialise on a per-session lock and share the resulting refresh, so an N-way race collapses to a single token endpoint call.
18
+
19
+ ### Added
20
+ - `sweatstack.fastapi.AccessTokenCache` (Protocol) and `InMemoryAccessTokenCache` (default), exposed via `configure(access_token_cache=...)`. The default is correct for single-worker deployments. Multi-worker deployments that want cross-worker de-duplication can plug in a shared-state implementation (e.g. Redis-backed). The default implementation is bounded by an LRU cap (10k entries) and uses striped locking, so memory growth is bounded regardless of session churn.
21
+ - `sweatstack.fastapi`: refresh-token rotation is now handled transparently. If a future `/oauth/token` response returns a new refresh token, the cache installs the result under both the old and new keys (so in-flight peers still hit), and the cookie / token store is rewritten with the new value. Current SweatStack servers do not rotate, so this is a forward-compatibility measure.
22
+ - `sweatstack.fastapi.RefreshLockTimeout`: a waiter that cannot acquire the per-session refresh lock within 15 seconds now raises this rather than blocking a FastAPI threadpool worker indefinitely. The `/oauth/token` call itself also has an explicit 10-second timeout.
23
+ - `sweatstack.fastapi`: debug logs (`sweatstack.fastapi.dependencies` logger) on every cache hit / seed / refresh / failure, keyed by a short SHA-256 fingerprint of the refresh token. Enable with `logging.getLogger("sweatstack.fastapi.dependencies").setLevel(logging.DEBUG)`.
24
+
25
+
26
+ ## [0.78.0] - 2026-05-19
27
+
28
+ ### Added
29
+ - `update_trace()` and `delete_trace()` are now exposed as module-level functions (e.g. `sweatstack.update_trace(...)`), matching the rest of the CRUD surface. They were previously only reachable via a `Client` instance.
30
+ - The package now declares an `__all__`, so `from sweatstack import *` and tooling that inspects the public surface (Sphinx, IDEs, type-checkers) see a well-defined list.
31
+
32
+ ### Changed
33
+ - Minimum Python version is now declared as `>=3.10` (the code already required 3.10+ syntax; the previous `>=3.9` declaration was incorrect).
34
+ - Exception types in docstrings now reference the typed hierarchy introduced in 0.76.0 (`SweatStackAPIError`, `SweatStackNotFoundError`, `SweatStackAuthError`, `SweatStackBadRequestError`) instead of the now-incorrect `HTTPStatusError`.
35
+
8
36
 
9
37
  ## [0.77.1] - 2026-05-19
10
38
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.77.1
3
+ Version: 0.80.0
4
4
  Summary: The official Python client for SweatStack
5
5
  Project-URL: Homepage, https://sweatstack.no
6
6
  Project-URL: Documentation, https://docs.sweatstack.no/getting-started/
@@ -15,13 +15,12 @@ Classifier: Development Status :: 4 - Beta
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
22
21
  Classifier: Programming Language :: Python :: 3.13
23
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
- Requires-Python: >=3.9
23
+ Requires-Python: >=3.10
25
24
  Requires-Dist: email-validator>=2.2.0
26
25
  Requires-Dist: httpx>=0.28.1
27
26
  Requires-Dist: pandas>=2.2.3
@@ -0,0 +1,204 @@
1
+ # Plan: Codebase hygiene follow-ups
2
+
3
+ A backlog of items that came up while landing trace-to-test linking and
4
+ writing AGENTS.md. Two buckets, with an explicit inclusion criterion so the
5
+ list stays principled instead of accumulating taste.
6
+
7
+
8
+ ## Inclusion criterion
9
+
10
+ The **rough edges** section below contains items that meet at least one of:
11
+
12
+ 1. **Inconsistent within the codebase** — same pattern, handled two ways.
13
+ 2. **Contradicts explicit documentation** — code says one thing, docstrings
14
+ or metadata say another.
15
+ 3. **Literal falsehood** — config or metadata that is untrue.
16
+ 4. **Blocks a class of improvements** — a structural absence with broad
17
+ downstream consequence.
18
+
19
+ Items that meet none of those are stylistic preferences or open design
20
+ questions. They go in the **Design questions** section, marked as such, so
21
+ they don't quietly migrate into "things we must fix."
22
+
23
+
24
+ ## Rough edges
25
+
26
+ ### 1. Singleton method registration is hand-curated and inconsistent
27
+
28
+ **Observation.** `client.py` bottom: `_generate_singleton_methods([...])`
29
+ takes a hand-maintained list of method names to expose as module-level
30
+ functions. The list is incomplete: `update_test` and `delete_test` are
31
+ registered, `update_trace` and `delete_trace` are not. There is no written
32
+ rule for which methods should be in the list.
33
+
34
+ **Why fix.** A consumer calling `sweatstack.update_trace(...)` gets
35
+ `AttributeError` even though `Client.update_trace` exists. The surface
36
+ silently drifts from the class.
37
+
38
+ **Approach.** Either:
39
+
40
+ - **(a) Auto-register**: at module load, iterate `Client` methods, skip
41
+ underscore-prefixed names, generate singletons for the rest. Single
42
+ source of truth, no list to forget.
43
+ - **(b) Keep the list, codify the rule**: AGENTS.md says "all public
44
+ methods on `Client` go here", and add a test that asserts the list
45
+ matches the class.
46
+
47
+ (a) is the cleaner fix; (b) is the smaller patch. (a) preferred.
48
+
49
+ **Effort.** Small (a) / tiny (b).
50
+
51
+
52
+ ### 2. Docstring exception drift since 0.76.0
53
+
54
+ **Observation.** The 0.76.0 typed-exception migration replaced
55
+ `httpx.HTTPStatusError` with `SweatStackAPIError` subclasses at the call
56
+ site, but most existing docstrings still say `Raises: HTTPStatusError`.
57
+ The library now documents an exception it does not raise.
58
+
59
+ **Why fix.** The docs lie. A consumer reading `help(client.get_activity)`
60
+ is told to catch `HTTPStatusError`; their `try/except` will never trigger.
61
+
62
+ **Approach.** One-shot sweep of `client.py`. For each `Raises:` block:
63
+
64
+ - If the endpoint has a path-param resource ID or references another
65
+ resource by ID, list `SweatStackNotFoundError` for the 404 case.
66
+ - If it is an app-token-only endpoint (e.g. `*_app_metadata`), list
67
+ `SweatStackAuthError` for the 403 case.
68
+ - Always finish with `SweatStackAPIError` as the catch-all.
69
+
70
+ Roughly 30 methods; mechanical but needs per-endpoint judgement.
71
+
72
+ **Effort.** Medium.
73
+
74
+
75
+ ### 3. `requires-python = ">=3.9"` is a literal falsehood
76
+
77
+ **Observation.** `pyproject.toml` declares `requires-python = ">=3.9"` and
78
+ classifies for 3.9 onwards. The code uses `X | Y` union syntax (PEP 604,
79
+ Python 3.10+) freely throughout `client.py`, `schemas.py`, generated
80
+ schemas, and tests. On 3.9 the package will install and crash at first
81
+ import with a `TypeError`.
82
+
83
+ **Why fix.** The package metadata is wrong. pip/uv will happily install
84
+ into a 3.9 environment based on the declared floor.
85
+
86
+ **Approach.**
87
+
88
+ - Bump `requires-python = ">=3.10"`.
89
+ - Drop the `Programming Language :: Python :: 3.9` classifier.
90
+ - Ship as a minor bump (`0.78.0`) — tightening a Python floor is a
91
+ breaking change for the affected versions, but in this case the
92
+ package was already non-functional on 3.9, so no real user is
93
+ affected.
94
+
95
+ **Effort.** Tiny. Worth bundling with another release rather than as a
96
+ standalone.
97
+
98
+
99
+ ### 4. No CI type-checker or linter
100
+
101
+ **Observation.** The library ships type hints as part of its value
102
+ proposition. There is no `pyright`, `mypy`, or `ruff` config in
103
+ `pyproject.toml` and no GitHub Actions workflow that runs them.
104
+
105
+ **Why fix.** The issues we hit during trace-to-test linking — silent
106
+ type widening on regen, `Enum | str` vs strict-enum inconsistency, the
107
+ `.value` trap on `httpx.params` — are exactly what a type-checker would
108
+ surface. A linter would catch the `from .client import *` and unused
109
+ imports that creep in.
110
+
111
+ **Approach.**
112
+
113
+ - Add `ruff` config to `pyproject.toml`. Start conservative: formatter +
114
+ the `E`, `F`, `I`, `B`, `UP` rule sets. Format the codebase in a single
115
+ commit to keep the diff readable.
116
+ - Add `pyright` config in non-strict mode (so existing code passes), then
117
+ flip individual `reportMissingTypeArgument`-style checks on over time.
118
+ - One GH Actions workflow: install via `uv sync --dev`, run `ruff
119
+ check`, `ruff format --check`, `pyright`, `pytest`. Block PRs on
120
+ failure.
121
+
122
+ **Effort.** Medium for the first pass. The follow-up of tightening
123
+ pyright strictness is open-ended; gate it on individual PRs rather than
124
+ a flag day.
125
+
126
+
127
+ ## Design questions
128
+
129
+ These do **not** meet the inclusion criterion above. They are deliberate
130
+ choices that someone could reasonably make differently. Listed here so we
131
+ can record a decision and stop re-litigating each time.
132
+
133
+ ### A. Should `update_*` methods return more than `None`?
134
+
135
+ **Today.** PUT methods return `None`; the server's `{"message": "..."}`
136
+ body is read but discarded.
137
+
138
+ **For.** Future-proof if the server starts returning the updated resource
139
+ or an `updated_at`. Symmetric with `create_*` (which returns a model).
140
+
141
+ **Against.** PUT-returns-no-body is standard REST. The create/update
142
+ asymmetry tracks a real semantic difference: create produces a new ID;
143
+ update mutates an existing one. The current message body is
144
+ operator-level noise, not data.
145
+
146
+ **Standing decision.** Keep as `None`. Revisit if the server contract
147
+ changes.
148
+
149
+ ### B. `from .client import *` in `__init__.py`
150
+
151
+ **Today.** Wildcard import.
152
+
153
+ **For.** Explicit re-exports are easier to review; surface-area drift
154
+ shows up in diffs; tools (Sphinx, pyright) handle them better.
155
+
156
+ **Against.** Requires syncing a list every time something is added to
157
+ `client.py`. Small library, low cost.
158
+
159
+ **Standing decision.** Keep wildcard. Reconsider if (a) the public
160
+ surface exceeds what fits comfortably in a manual list, or (b) we adopt
161
+ a type-checker that struggles with `__all__`-less wildcards.
162
+
163
+ ### C. `Client.__new__(Client)` in tests
164
+
165
+ **Today.** Tests that need a `Client` instance for a helper method
166
+ construct via `__new__` to skip `__init__`, then set instance attributes
167
+ directly.
168
+
169
+ **For.** A no-I/O `__init__` would let tests construct clients normally
170
+ (`Client(api_key="test")`).
171
+
172
+ **Against.** `Client.__init__` already does no I/O — it just stores
173
+ fields and wraps secrets. The `__new__` pattern in tests signals "this
174
+ test never authenticates" clearly. Two characters longer than `Client()`.
175
+
176
+ **Standing decision.** Keep. The smell is cosmetic.
177
+
178
+ ### D. `_enums_to_strings([x])[0]` for single values
179
+
180
+ **Today.** Helper takes a list. Single-value callers write
181
+ `self._enums_to_strings([sport])[0] if sport else None`.
182
+
183
+ **For.** A companion `_enum_to_string(x)` would read cleaner at the
184
+ single-value call sites.
185
+
186
+ **Against.** Trivially small footprint; helper proliferation has its
187
+ own cost; the wrap-and-index idiom is already understood across the
188
+ file.
189
+
190
+ **Standing decision.** Keep. Add `_enum_to_string` only if a new
191
+ call site appears where the wrap-and-index actively confuses.
192
+
193
+ ### E. `_default_client = Client()` at module import
194
+
195
+ **Today.** A `Client` instance is constructed when `sweatstack.client`
196
+ is imported, so the module-level singleton functions can bind to it.
197
+
198
+ **For.** Lazy construction would defer any cost to first use.
199
+
200
+ **Against.** `Client.__init__` is I/O-free; the cost is microseconds.
201
+ Lazy binding would complicate the `_generate_singleton_methods`
202
+ machinery for no measurable benefit.
203
+
204
+ **Standing decision.** Keep.