sweatstack 0.74.0__tar.gz → 0.76.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.74.0 → sweatstack-0.76.0}/.claude/skills/sweatstack-python/SKILL.md +3 -3
- {sweatstack-0.74.0 → sweatstack-0.76.0}/.claude/skills/sweatstack-python/client.md +52 -1
- {sweatstack-0.74.0 → sweatstack-0.76.0}/CHANGELOG.md +22 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/PKG-INFO +1 -1
- sweatstack-0.76.0/plans/002_TYPED_EXCEPTIONS.md +269 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/pyproject.toml +1 -1
- sweatstack-0.76.0/src/sweatstack/__init__.py +12 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/client.py +133 -67
- sweatstack-0.76.0/src/sweatstack/exceptions.py +67 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/fastapi/routes.py +24 -13
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/openapi_schemas.py +5 -5
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/streamlit.py +8 -4
- sweatstack-0.76.0/tests/test_exceptions.py +312 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/uv.lock +1 -1
- sweatstack-0.74.0/src/sweatstack/__init__.py +0 -1
- {sweatstack-0.74.0 → sweatstack-0.76.0}/.claude/settings.local.json +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/.gitignore +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/.python-version +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/CONTRIBUTING.md +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/LICENSE +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/Makefile +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/README.md +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/docs/conf.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/docs/everything.rst +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/docs/index.rst +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/examples/send_webhook.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/plans/001a_tests.md +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/plans/001b_metadata.md +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/plans/001c_dailies.md +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/fastapi/dependencies.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/tests/__init__.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/tests/test_dailies.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/tests/test_metadata.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/tests/test_teams.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/tests/test_tests.py +0 -0
- {sweatstack-0.74.0 → sweatstack-0.76.0}/tests/test_webhooks.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sweatstack-python
|
|
3
3
|
description: >
|
|
4
|
-
Builds Python applications using the SweatStack client library (
|
|
4
|
+
Builds Python applications using the SweatStack client library (uv add sweatstack).
|
|
5
5
|
Covers authentication, activity and trace data retrieval, pandas DataFrames, Streamlit
|
|
6
6
|
dashboards, FastAPI backends, user delegation, teams, and file uploads. Use when writing
|
|
7
7
|
Python scripts, notebooks, Streamlit apps, or FastAPI services that access SweatStack
|
|
@@ -13,9 +13,9 @@ description: >
|
|
|
13
13
|
|
|
14
14
|
Python client library for the SweatStack sports data platform.
|
|
15
15
|
|
|
16
|
-
**Install:** `
|
|
16
|
+
**Install:** `uv add sweatstack`
|
|
17
17
|
|
|
18
|
-
**Extras:** `
|
|
18
|
+
**Extras:** `uv add sweatstack[streamlit]` · `uv add sweatstack[fastapi]`
|
|
19
19
|
|
|
20
20
|
## Quick Start
|
|
21
21
|
|
|
@@ -163,6 +163,21 @@ trace = client.create_trace(
|
|
|
163
163
|
tags=["test"],
|
|
164
164
|
notes="Lactate threshold test",
|
|
165
165
|
)
|
|
166
|
+
|
|
167
|
+
# Update a trace (full replace — fields not provided are set to null)
|
|
168
|
+
client.update_trace(
|
|
169
|
+
trace.id,
|
|
170
|
+
timestamp=trace.timestamp,
|
|
171
|
+
lactate=2.8, # corrected value
|
|
172
|
+
rpe=trace.rpe, # must re-pass to keep existing values
|
|
173
|
+
heart_rate=trace.heart_rate,
|
|
174
|
+
sport=trace.sport,
|
|
175
|
+
tags=trace.tags,
|
|
176
|
+
notes=trace.notes,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Delete a trace
|
|
180
|
+
client.delete_trace("trace_id")
|
|
166
181
|
```
|
|
167
182
|
|
|
168
183
|
## Tests
|
|
@@ -366,6 +381,42 @@ sweatstack.authenticate()
|
|
|
366
381
|
df = sweatstack.get_latest_activity_data()
|
|
367
382
|
```
|
|
368
383
|
|
|
384
|
+
## Error Handling
|
|
385
|
+
|
|
386
|
+
All errors raised by the library are subclasses of `SweatStackError`. Consumers never need to import `httpx`.
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
from sweatstack import (
|
|
390
|
+
SweatStackError, # base — catch-all
|
|
391
|
+
SweatStackConnectionError, # DNS, timeout, connection refused
|
|
392
|
+
SweatStackTokenRefreshError,# token refresh failed (expired/missing)
|
|
393
|
+
SweatStackAPIError, # HTTP error response (has status_code, url, method, body, request_id)
|
|
394
|
+
SweatStackAuthError, # 401, 403
|
|
395
|
+
SweatStackNotFoundError, # 404
|
|
396
|
+
SweatStackRateLimitError, # 429 (has retry_after)
|
|
397
|
+
SweatStackBadRequestError, # other 4xx
|
|
398
|
+
SweatStackServerError, # 5xx
|
|
399
|
+
)
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Typical usage:
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
try:
|
|
406
|
+
activity = client.get_activity("nonexistent_id")
|
|
407
|
+
except SweatStackNotFoundError:
|
|
408
|
+
print("Activity doesn't exist")
|
|
409
|
+
except SweatStackServerError as e:
|
|
410
|
+
print(f"Server error (transient): {e.status_code}")
|
|
411
|
+
except SweatStackAPIError as e:
|
|
412
|
+
print(f"API error: {e.status_code} {e.body}")
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Hierarchy:
|
|
416
|
+
- `SweatStackConnectionError`, `SweatStackTokenRefreshError`, and `SweatStackAPIError` are all direct children of `SweatStackError`.
|
|
417
|
+
- All HTTP-response errors (`SweatStackAuthError`, `SweatStackNotFoundError`, etc.) are subclasses of `SweatStackAPIError` and carry `status_code`, `url`, `method`, `request_id`, and `body` attributes.
|
|
418
|
+
- `SweatStackRateLimitError` additionally has `retry_after: int | None`.
|
|
419
|
+
|
|
369
420
|
## Gotchas
|
|
370
421
|
|
|
371
422
|
- **Sport enum uses underscores:** `Sport.cycling_road`, not `Sport("road")` or `Sport.cycling.road`. String values use dots: `"cycling.road"`.
|
|
@@ -373,6 +424,6 @@ df = sweatstack.get_latest_activity_data()
|
|
|
373
424
|
- **`sport` (singular) vs `sports` (list):** `get_latest_activity(sport=...)` and `create_trace(sport=...)` take a single sport. All other methods that filter by sport use `sports=[...]` (list). The singular `sport` parameter on longitudinal methods is deprecated.
|
|
374
425
|
- **DataFrames have standard dtypes.** The library converts API-optimized types (Int16, float16) to float64/datetime64[ns] automatically.
|
|
375
426
|
- **`as_dataframe=True`** is available on `get_activities()`, `get_traces()`, `get_tests()`, and `get_dailies()`. Time-series methods (`get_activity_data`, `get_longitudinal_data`, etc.) always return DataFrames.
|
|
376
|
-
- **`update_test()`
|
|
427
|
+
- **`update_test()` and `update_trace()` are full replaces.** Omitted optional fields are set to null. Always re-pass all fields you want to keep.
|
|
377
428
|
- **`summary` fields are optional.** Always null-check: `activity.summary.power.mean if activity.summary and activity.summary.power else None`.
|
|
378
429
|
- **`metrics` on ActivitySummary** lists available data streams, not the data itself. Use to check availability before calling `get_activity_data()`.
|
|
@@ -6,6 +6,28 @@ 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
8
|
|
|
9
|
+
## [0.76.0] - 2026-04-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Typed exception hierarchy (`sweatstack.exceptions`). All API errors now raise specific exception types: `SweatStackAuthError` (401/403), `SweatStackNotFoundError` (404), `SweatStackRateLimitError` (429), `SweatStackBadRequestError` (other 4xx), `SweatStackServerError` (5xx). Transport failures raise `SweatStackConnectionError`.
|
|
13
|
+
- Structured error metadata on all API exceptions: `status_code`, `url`, `method`, `request_id`, `body`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **BREAKING:** All client methods now raise `SweatStackAPIError` subclasses instead of `httpx.HTTPStatusError`. Code that catches `httpx.HTTPStatusError` must switch to catching `SweatStackAPIError` (or a specific subclass).
|
|
17
|
+
- **BREAKING:** `TokenRefreshError` renamed to `SweatStackTokenRefreshError` and moved to `sweatstack.exceptions`. Import path changed from `from sweatstack import TokenRefreshError` to `from sweatstack import SweatStackTokenRefreshError`.
|
|
18
|
+
- **BREAKING:** 422 responses now raise `SweatStackBadRequestError` instead of `ValueError`.
|
|
19
|
+
- Transport errors (DNS, timeouts, connection refused) now raise `SweatStackConnectionError` instead of leaking raw httpx exceptions.
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
- `httpx` is no longer part of the public error surface. Consumers do not need to import `httpx` to handle errors.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## [0.75.0] - 2026-04-23
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Update and delete traces: `update_trace()` and `delete_trace()` methods for full trace lifecycle management.
|
|
29
|
+
|
|
30
|
+
|
|
9
31
|
## [0.74.0] - 2026-04-22
|
|
10
32
|
|
|
11
33
|
### Added
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Typed exception hierarchy for the SweatStack client
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
The `sweatstack` Python client currently surfaces `httpx.HTTPStatusError`
|
|
6
|
+
directly to consumers via `Client._raise_for_status` /
|
|
7
|
+
`Client._print_response_and_raise` (`src/sweatstack/client.py:968-997`).
|
|
8
|
+
|
|
9
|
+
This has two problems:
|
|
10
|
+
|
|
11
|
+
1. **Consumers can't branch on error kind without inspecting status codes.**
|
|
12
|
+
Telling apart "upstream is down" (5xx, transient) from "you sent bad input"
|
|
13
|
+
(4xx, your bug) requires `exc.response.status_code`. That's the leaky
|
|
14
|
+
abstraction a client library exists to absorb.
|
|
15
|
+
2. **`httpx` is an implementation detail.** Any consumer that wants to handle
|
|
16
|
+
errors must `import httpx` to catch `HTTPStatusError`. If the transport
|
|
17
|
+
ever changes, every consumer breaks.
|
|
18
|
+
|
|
19
|
+
## Exception hierarchy
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
SweatStackError # base -- catch-all for anything sweatstack-related
|
|
23
|
+
├── SweatStackConnectionError # transport-level: DNS, timeout, no response
|
|
24
|
+
├── SweatStackTokenRefreshError # client-side auth failure (before any request)
|
|
25
|
+
└── SweatStackAPIError # HTTP response with error status
|
|
26
|
+
├── SweatStackAuthError # 401, 403
|
|
27
|
+
├── SweatStackNotFoundError # 404
|
|
28
|
+
├── SweatStackRateLimitError # 429 -- exposes retry_after
|
|
29
|
+
├── SweatStackBadRequestError # other 4xx
|
|
30
|
+
└── SweatStackServerError # 5xx
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`SweatStackTokenRefreshError` is a direct child of `SweatStackError`, not of
|
|
34
|
+
`SweatStackAPIError`. Token refresh is a client-side operation that happens
|
|
35
|
+
before a request is made -- it has no status code, no URL, no method. Forcing
|
|
36
|
+
it under `SweatStackAPIError` would violate Liskov: every `SweatStackAPIError`
|
|
37
|
+
guarantees `status_code: int`, and a token refresh failure can't provide one.
|
|
38
|
+
|
|
39
|
+
### Attributes on `SweatStackAPIError`
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
class SweatStackAPIError(SweatStackError):
|
|
43
|
+
status_code: int
|
|
44
|
+
url: str
|
|
45
|
+
method: str
|
|
46
|
+
request_id: str | None # from X-Request-ID header, if present
|
|
47
|
+
body: dict | str | None # parsed JSON if possible, else raw text, else None
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`SweatStackRateLimitError` adds `retry_after: int | None` (seconds, from
|
|
51
|
+
`Retry-After` header).
|
|
52
|
+
|
|
53
|
+
`SweatStackConnectionError` and `SweatStackTokenRefreshError` take only a
|
|
54
|
+
`message: str`.
|
|
55
|
+
|
|
56
|
+
### Why these classes
|
|
57
|
+
|
|
58
|
+
| Class | Reason |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `SweatStackServerError` | The most important new class. `except SweatStackServerError` handles "upstream is unhappy" without catching consumer bugs. |
|
|
61
|
+
| `SweatStackBadRequestError` | Separate from server errors: don't retry, surface to developers. Different remediation = different class. |
|
|
62
|
+
| `SweatStackNotFoundError` | 404 is often domain-meaningful ("trace was deleted"), not a bug. Consumers want to handle it as a business case. |
|
|
63
|
+
| `SweatStackRateLimitError` | Reserves the class even if the API isn't rate-limited today. Zero cost, prevents future refactoring. |
|
|
64
|
+
| `SweatStackConnectionError` | Covers `httpx.ConnectError`, `httpx.ReadTimeout`, etc. Currently these leak through as raw httpx types. |
|
|
65
|
+
| `SweatStackAuthError` | Groups 401/403 HTTP responses. |
|
|
66
|
+
| `SweatStackTokenRefreshError` | Client-side auth failure. Sibling of `SweatStackAPIError`, not child -- no HTTP response data to carry. |
|
|
67
|
+
|
|
68
|
+
## Implementation
|
|
69
|
+
|
|
70
|
+
### 1. New module: `src/sweatstack/exceptions.py`
|
|
71
|
+
|
|
72
|
+
Define the full hierarchy. Pure module -- no imports from `client.py`, no
|
|
73
|
+
`httpx`. This is the public error contract.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
class SweatStackError(Exception):
|
|
77
|
+
"""Base exception for all SweatStack errors."""
|
|
78
|
+
|
|
79
|
+
class SweatStackConnectionError(SweatStackError):
|
|
80
|
+
"""Transport-level failure: DNS, connect timeout, read timeout, no response."""
|
|
81
|
+
|
|
82
|
+
class SweatStackTokenRefreshError(SweatStackError):
|
|
83
|
+
"""Token refresh failed (expired, missing, or refresh request failed)."""
|
|
84
|
+
|
|
85
|
+
class SweatStackAPIError(SweatStackError):
|
|
86
|
+
"""HTTP response with an error status code."""
|
|
87
|
+
def __init__(self, *, status_code, url, method, request_id=None, body=None):
|
|
88
|
+
self.status_code = status_code
|
|
89
|
+
self.url = url
|
|
90
|
+
self.method = method
|
|
91
|
+
self.request_id = request_id
|
|
92
|
+
self.body = body
|
|
93
|
+
super().__init__(self._format_message())
|
|
94
|
+
|
|
95
|
+
def _format_message(self):
|
|
96
|
+
msg = f"{self.status_code}: {self.method} {self.url}"
|
|
97
|
+
if self.request_id:
|
|
98
|
+
msg += f" (request_id={self.request_id})"
|
|
99
|
+
if self.body:
|
|
100
|
+
msg += f" — {self.body}"
|
|
101
|
+
return msg
|
|
102
|
+
|
|
103
|
+
def __repr__(self):
|
|
104
|
+
return (
|
|
105
|
+
f"{type(self).__name__}(status_code={self.status_code!r}, "
|
|
106
|
+
f"method={self.method!r}, url={self.url!r})"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
class SweatStackAuthError(SweatStackAPIError):
|
|
110
|
+
"""401 or 403: authentication or authorization failure."""
|
|
111
|
+
|
|
112
|
+
class SweatStackNotFoundError(SweatStackAPIError):
|
|
113
|
+
"""404: resource not found."""
|
|
114
|
+
|
|
115
|
+
class SweatStackRateLimitError(SweatStackAPIError):
|
|
116
|
+
"""429: rate limited."""
|
|
117
|
+
def __init__(self, *, retry_after=None, **kwargs):
|
|
118
|
+
self.retry_after = retry_after
|
|
119
|
+
super().__init__(**kwargs)
|
|
120
|
+
|
|
121
|
+
class SweatStackBadRequestError(SweatStackAPIError):
|
|
122
|
+
"""4xx (not 401, 403, 404, 429): client sent invalid input."""
|
|
123
|
+
|
|
124
|
+
class SweatStackServerError(SweatStackAPIError):
|
|
125
|
+
"""5xx: server-side failure, transient, safe to retry for idempotent ops."""
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 2. Translation in `client.py`
|
|
129
|
+
|
|
130
|
+
Replace `_raise_for_status` and `_print_response_and_raise` with a single
|
|
131
|
+
translation point:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
def _raise_for_status(self, response: httpx.Response) -> None:
|
|
135
|
+
if response.is_success:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
status = response.status_code
|
|
139
|
+
body = self._parse_error_body(response)
|
|
140
|
+
request_id = response.headers.get("x-request-id")
|
|
141
|
+
common = dict(
|
|
142
|
+
status_code=status,
|
|
143
|
+
url=str(response.request.url),
|
|
144
|
+
method=response.request.method,
|
|
145
|
+
request_id=request_id,
|
|
146
|
+
body=body,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if status in (401, 403):
|
|
150
|
+
raise SweatStackAuthError(**common)
|
|
151
|
+
if status == 404:
|
|
152
|
+
raise SweatStackNotFoundError(**common)
|
|
153
|
+
if status == 429:
|
|
154
|
+
retry_after = int(response.headers.get("retry-after", "0")) or None
|
|
155
|
+
raise SweatStackRateLimitError(retry_after=retry_after, **common)
|
|
156
|
+
if 400 <= status < 500:
|
|
157
|
+
raise SweatStackBadRequestError(**common)
|
|
158
|
+
if 500 <= status < 600:
|
|
159
|
+
raise SweatStackServerError(**common)
|
|
160
|
+
# Fallback (e.g. 3xx that wasn't followed) -- shouldn't happen
|
|
161
|
+
raise SweatStackAPIError(**common)
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _parse_error_body(response: httpx.Response) -> dict | str | None:
|
|
165
|
+
try:
|
|
166
|
+
return response.json()
|
|
167
|
+
except Exception:
|
|
168
|
+
text = response.text
|
|
169
|
+
return text if text else None
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Wrap transport-level errors at the HTTP call site:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
try:
|
|
176
|
+
response = self._http_client.request(...)
|
|
177
|
+
except httpx.HTTPError as exc:
|
|
178
|
+
raise SweatStackConnectionError(str(exc)) from exc
|
|
179
|
+
self._raise_for_status(response)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 3. Remove `_print_response_and_raise` and `_add_note`
|
|
183
|
+
|
|
184
|
+
These exist only to annotate `httpx.HTTPStatusError` with response text.
|
|
185
|
+
The new exceptions carry `body` natively, so these methods are dead code.
|
|
186
|
+
|
|
187
|
+
### 4. Clean up the 422 special case
|
|
188
|
+
|
|
189
|
+
The current code raises `ValueError(response.json())` for 422. This should
|
|
190
|
+
raise `SweatStackBadRequestError` like any other 4xx. The parsed validation
|
|
191
|
+
error body lands in `body` where consumers can inspect it.
|
|
192
|
+
|
|
193
|
+
### 5. Clean up the 401/Streamlit special case
|
|
194
|
+
|
|
195
|
+
`_raise_for_status` currently detects Streamlit and adds a hint note on 401.
|
|
196
|
+
After this change, it raises `SweatStackAuthError`. The Streamlit hint (if
|
|
197
|
+
still wanted) moves to the Streamlit integration layer, not the core error
|
|
198
|
+
path.
|
|
199
|
+
|
|
200
|
+
### 6. Move `TokenRefreshError` to `exceptions.py`
|
|
201
|
+
|
|
202
|
+
- Rename to `SweatStackTokenRefreshError`.
|
|
203
|
+
- Parent is `SweatStackError` (not `SweatStackAPIError`).
|
|
204
|
+
- Remove old definition from `client.py`.
|
|
205
|
+
- Import in `client.py` from `exceptions.py`.
|
|
206
|
+
|
|
207
|
+
### 7. Update `__init__.py` exports
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from .client import *
|
|
211
|
+
from .exceptions import (
|
|
212
|
+
SweatStackError,
|
|
213
|
+
SweatStackConnectionError,
|
|
214
|
+
SweatStackTokenRefreshError,
|
|
215
|
+
SweatStackAPIError,
|
|
216
|
+
SweatStackAuthError,
|
|
217
|
+
SweatStackNotFoundError,
|
|
218
|
+
SweatStackRateLimitError,
|
|
219
|
+
SweatStackBadRequestError,
|
|
220
|
+
SweatStackServerError,
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 8. Tests
|
|
225
|
+
|
|
226
|
+
Unit tests covering each status branch (401, 403, 404, 429, 400, 422, 500,
|
|
227
|
+
502, 503, network error). Each test asserts:
|
|
228
|
+
- Correct exception type raised
|
|
229
|
+
- `status_code`, `url`, `method`, `body` populated correctly
|
|
230
|
+
- `request_id` populated when header present
|
|
231
|
+
- `retry_after` populated for 429
|
|
232
|
+
- Inheritance: e.g. `SweatStackServerError` is caught by
|
|
233
|
+
`except SweatStackAPIError` and `except SweatStackError`
|
|
234
|
+
- `__repr__` produces structured output suitable for logging
|
|
235
|
+
|
|
236
|
+
## Backwards incompatibilities
|
|
237
|
+
|
|
238
|
+
Since no external consumers exist yet, these are listed for completeness and
|
|
239
|
+
to inform the changelog for the first stable release.
|
|
240
|
+
|
|
241
|
+
| What changed | Before | After |
|
|
242
|
+
|---|---|---|
|
|
243
|
+
| HTTP error exceptions | `httpx.HTTPStatusError` | `SweatStackAPIError` subclasses |
|
|
244
|
+
| 422 responses | `ValueError(response.json())` | `SweatStackBadRequestError` |
|
|
245
|
+
| Transport errors (DNS, timeout) | Raw `httpx.ConnectError`, `httpx.ReadTimeout`, etc. | `SweatStackConnectionError` |
|
|
246
|
+
| `TokenRefreshError` class name | `TokenRefreshError` | `SweatStackTokenRefreshError` |
|
|
247
|
+
| `TokenRefreshError` base class | `Exception` | `SweatStackError` |
|
|
248
|
+
| `TokenRefreshError` import path | `from sweatstack import TokenRefreshError` or `from sweatstack.client import TokenRefreshError` | `from sweatstack import SweatStackTokenRefreshError` or `from sweatstack.exceptions import SweatStackTokenRefreshError` |
|
|
249
|
+
| 401 + Streamlit hint | Appended as exception note on `httpx.HTTPStatusError` | `SweatStackAuthError` raised; Streamlit hint moves to Streamlit integration layer |
|
|
250
|
+
| `_print_response_and_raise` | Public-ish method on `Client` | Removed |
|
|
251
|
+
| `_add_note` | Helper on `Client` | Removed |
|
|
252
|
+
|
|
253
|
+
## Out of scope
|
|
254
|
+
|
|
255
|
+
- **Built-in retry on 5xx / 429.** Clean follow-up once typed exceptions exist.
|
|
256
|
+
- **Idempotency keys on POST.** Requires API-side support.
|
|
257
|
+
- **Native async client.** Separate effort.
|
|
258
|
+
|
|
259
|
+
## Acceptance criteria
|
|
260
|
+
|
|
261
|
+
- All public client methods that previously raised `httpx.HTTPStatusError`
|
|
262
|
+
raise a `SweatStackAPIError` subclass appropriate to the status code.
|
|
263
|
+
- All transport-level failures raise `SweatStackConnectionError`.
|
|
264
|
+
- `httpx` types never appear in the public API surface.
|
|
265
|
+
- `SweatStackTokenRefreshError` is importable from `sweatstack.exceptions`
|
|
266
|
+
and `sweatstack`.
|
|
267
|
+
- Unit tests cover each status branch and verify exception type, attributes,
|
|
268
|
+
and inheritance.
|
|
269
|
+
- All `SweatStackAPIError` instances have a useful `__repr__` for debugging.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .client import * # noqa: F403
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
SweatStackError,
|
|
4
|
+
SweatStackConnectionError,
|
|
5
|
+
SweatStackTokenRefreshError,
|
|
6
|
+
SweatStackAPIError,
|
|
7
|
+
SweatStackAuthError,
|
|
8
|
+
SweatStackNotFoundError,
|
|
9
|
+
SweatStackRateLimitError,
|
|
10
|
+
SweatStackBadRequestError,
|
|
11
|
+
SweatStackServerError,
|
|
12
|
+
)
|