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.
- {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/settings.local.json +5 -1
- {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/skills/sweatstack-python/fastapi.md +78 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/AGENTS.md +0 -22
- {sweatstack-0.77.1 → sweatstack-0.80.0}/CHANGELOG.md +28 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/PKG-INFO +2 -3
- sweatstack-0.80.0/plans/004_codebase_hygiene.md +204 -0
- sweatstack-0.80.0/plans/005_ost_sport_bridge.md +515 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/pyproject.toml +2 -3
- sweatstack-0.80.0/src/sweatstack/__init__.py +26 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/client.py +171 -139
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/__init__.py +11 -0
- sweatstack-0.80.0/src/sweatstack/fastapi/access_token_cache.py +287 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/config.py +12 -1
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/dependencies.py +203 -42
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/schemas.py +77 -4
- sweatstack-0.80.0/tests/test_access_token_cache.py +577 -0
- sweatstack-0.80.0/tests/test_public_surface.py +69 -0
- sweatstack-0.80.0/tests/test_sport_ost_compat.py +123 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/uv.lock +1 -1
- sweatstack-0.77.1/src/sweatstack/__init__.py +0 -12
- {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/skills/sweatstack-python/client.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/.gitignore +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/.python-version +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/CONTRIBUTING.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/LICENSE +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/Makefile +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/README.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/docs/conf.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/docs/everything.rst +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/docs/index.rst +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/examples/send_webhook.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/plans/001a_tests.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/plans/001b_metadata.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/plans/001c_dailies.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/plans/003_trace_test_linking.md +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/exceptions.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/routes.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/__init__.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_dailies.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_exceptions.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_metadata.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_teams.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_tests.py +0 -0
- {sweatstack-0.77.1 → sweatstack-0.80.0}/tests/test_trace_test_linking.py +0 -0
- {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.
|
|
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.
|
|
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.
|