sweatstack 0.96.0__tar.gz → 0.98.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.96.0 → sweatstack-0.98.0}/.github/workflows/ci.yml +1 -1
- {sweatstack-0.96.0 → sweatstack-0.98.0}/AGENTS.md +5 -5
- {sweatstack-0.96.0 → sweatstack-0.98.0}/CHANGELOG.md +46 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/DEVELOPMENT.md +1 -1
- {sweatstack-0.96.0 → sweatstack-0.98.0}/PKG-INFO +4 -4
- {sweatstack-0.96.0 → sweatstack-0.98.0}/examples/send_webhook.py +8 -8
- {sweatstack-0.96.0 → sweatstack-0.98.0}/pyproject.toml +6 -5
- {sweatstack-0.96.0 → sweatstack-0.98.0}/skills/sweatstack-python/SKILL.md +3 -2
- {sweatstack-0.96.0 → sweatstack-0.98.0}/skills/sweatstack-python/api.md +2 -1
- {sweatstack-0.96.0 → sweatstack-0.98.0}/skills/sweatstack-python/data-models.md +2 -1
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/_frames.py +15 -7
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/_transport.py +16 -16
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/cli.py +2 -2
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/client.py +96 -23
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/exceptions.py +1 -1
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/fastapi/dependencies.py +4 -4
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/fastapi/routes.py +3 -3
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/schemas.py +15 -6
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/streamlit.py +2 -2
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_account_status.py +3 -3
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_credentials.py +148 -1
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_dailies.py +47 -2
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_exceptions.py +7 -7
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_frames.py +2 -2
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_identity.py +3 -3
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_output.py +22 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_portal.py +3 -3
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_resources.py +1 -1
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_tests.py +5 -5
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_trace_test_linking.py +4 -4
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_transport.py +44 -22
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_user_agent.py +10 -10
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_webhooks.py +10 -10
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_write_timezone_validation.py +2 -2
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/typing/output_types.py +1 -1
- {sweatstack-0.96.0 → sweatstack-0.98.0}/uv.lock +72 -508
- {sweatstack-0.96.0 → sweatstack-0.98.0}/.git-blame-ignore-revs +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/.gitignore +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/.gitleaks.toml +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/.python-version +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/CONTRIBUTING.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/LICENSE +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/Makefile +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/README.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/UPGRADING.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/evals/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/evals/agent/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/evals/agent/agent.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/evals/agent/cases.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/evals/agent/evaluators.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/evals/agent/harness.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/evals/agent/lifecycle.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/evals/agent/run.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/001a_tests.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/001b_metadata.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/001c_dailies.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/003_trace_test_linking.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/004_codebase_hygiene.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/005_ost_sport_bridge.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/006_output_backends.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/007_account_status_userinfo_portal.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/008_sdk_documentation.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/009_road_to_1_0.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/010_credentials_one_source.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/plans/011_user_agent_on_every_request.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/_docs.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/_renames.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/fastapi/access_token_cache.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/_app_metadata.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/_base.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/activities.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/dailies.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/oauth.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/portal.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/profile.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/teams.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/tests.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/traces.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/resources/users.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/conftest.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_access_token_cache.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_authorization_url.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_changelog.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_docs_check.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_docstrings.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_longitudinal_mean_max_after.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_metadata.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_public_hygiene.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_public_surface.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_renames.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_segmentation.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_skill.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_sport_ost.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_streamlit_auth.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_teams.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.98.0}/tests/test_wire_names.py +0 -0
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
strategy:
|
|
13
13
|
fail-fast: false
|
|
14
14
|
matrix:
|
|
15
|
-
python: ["3.
|
|
15
|
+
python: ["3.11", "3.12", "3.13"]
|
|
16
16
|
include:
|
|
17
17
|
# The lock file tracks the newest pandas; this leg keeps the declared
|
|
18
18
|
# lower bound (pyproject `pandas>=2.2.3`) honest.
|
|
@@ -87,7 +87,7 @@ src/sweatstack/
|
|
|
87
87
|
├── _frames.py # output= backends: parquet/models -> pandas/polars/arrow/bytes.
|
|
88
88
|
├── _renames.py # Removed names -> replacement hints. Permanent; see "Breaking changes".
|
|
89
89
|
├── _transport.py # Retry policy and the per-request Session on the client's connection pool.
|
|
90
|
-
├── exceptions.py # Public error contract. No
|
|
90
|
+
├── exceptions.py # Public error contract. No HTTP-library types leak.
|
|
91
91
|
├── client.py # Client: config, auth, delegation, transport (_request), resource attributes,
|
|
92
92
|
│ # and the module-level interface (one shared default client).
|
|
93
93
|
├── resources/ # One module per URL segment: activities.py, traces.py, tests.py, ...
|
|
@@ -137,7 +137,7 @@ it automatically.
|
|
|
137
137
|
- **Enum-typed params accept `Enum | str`** and route through
|
|
138
138
|
`_enums_to_strings`. Don't introduce strict-enum-only parameters.
|
|
139
139
|
- **Open enums stay open.** When the server documents an enum as an open
|
|
140
|
-
set (metrics, scopes, daily measures, status codes, capabilities),
|
|
140
|
+
set (metrics, scopes, daily measures, sources, status codes, capabilities),
|
|
141
141
|
register it with `_open_enum(...)` in `schemas.py` so unknown values
|
|
142
142
|
parse as pseudo-members. Leave closed sets strict.
|
|
143
143
|
- **Never retry a request the server can't safely repeat.** `_transport.RETRYABLE_METHODS`
|
|
@@ -233,8 +233,8 @@ Both kinds take `output` as a keyword-only parameter and carry `@overload` stubs
|
|
|
233
233
|
`Literal` values (clone the neighbours); `tests/typing/output_types.py` pins them.
|
|
234
234
|
|
|
235
235
|
**Query parameters** are only sent when the caller supplied a non-default value. Repeatable
|
|
236
|
-
ones go through `self._wire(value)` (R8). Always pass `.value` for enums into `
|
|
237
|
-
`
|
|
236
|
+
ones go through `self._wire(value)` (R8). Always pass `.value` for enums into `httpx2` params:
|
|
237
|
+
`httpx2` calls `str()`, which gives `"TraceResolution.linked"`, not `"linked"`.
|
|
238
238
|
|
|
239
239
|
|
|
240
240
|
## Full-replace `replace` methods
|
|
@@ -309,7 +309,7 @@ construct fails the suite, not a user's notebook.
|
|
|
309
309
|
## Exceptions
|
|
310
310
|
|
|
311
311
|
The hierarchy in `sweatstack/exceptions.py` is the **public** error
|
|
312
|
-
contract — consumers should never need to import `
|
|
312
|
+
contract — consumers should never need to import `httpx2`.
|
|
313
313
|
|
|
314
314
|
```
|
|
315
315
|
SweatStackError
|
|
@@ -11,6 +11,52 @@ for a coding agent, are in `UPGRADING.md` next to this file and at
|
|
|
11
11
|
https://docs.sweatstack.no/learn/python/upgrading/.
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
## [0.98.0] - 2026-10-30
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `SWEATSTACK_CLIENT_ID` and `SWEATSTACK_CLIENT_SECRET` name the app that issued
|
|
19
|
+
`SWEATSTACK_API_KEY` and `SWEATSTACK_REFRESH_TOKEN`, so a client that takes its tokens from the
|
|
20
|
+
environment renews them with that app. Before, it renewed with the SDK's own client, which
|
|
21
|
+
SweatStack refuses for a token issued to another app: tokens your app hands to a notebook, a
|
|
22
|
+
job or a sandbox stopped working once the access token expired. Set the secret only if your app
|
|
23
|
+
exchanged the sign-in code with it. The variables never apply to tokens saved by
|
|
24
|
+
`authenticate()` or passed to `Client`, and `client_id=` and `client_secret=` given to `Client`
|
|
25
|
+
always win.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- If your environment sets `SWEATSTACK_CLIENT_ID` for the Streamlit or FastAPI helper and also
|
|
30
|
+
holds a `SWEATSTACK_REFRESH_TOKEN` that `authenticate()` issued, a plain `Client()` there now
|
|
31
|
+
renews that token with your app, which SweatStack refuses: the renewal fails once the access
|
|
32
|
+
token expires, with an error that says why. Unset `SWEATSTACK_CLIENT_ID` and
|
|
33
|
+
`SWEATSTACK_CLIENT_SECRET` for that process, or pass them to the helper as `client_id=` and
|
|
34
|
+
`client_secret=` instead.
|
|
35
|
+
- `DailySource` is an open enum, like `Metric` and `DailyMeasure`: a `source` this version does
|
|
36
|
+
not know (on a daily, a marker or a max effort) becomes a `DailySource` with that value instead
|
|
37
|
+
of failing the whole response, so a new source on SweatStack never breaks this version.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Polars output works in the browser (Pyodide, as in JupyterLite). Pyodide's Polars has no
|
|
42
|
+
Parquet reader, so every time series and mean-max call raised an `AttributeError`; the client
|
|
43
|
+
now reads through pyarrow there, and `sweatstack[polars]` installs pyarrow on Pyodide.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## [0.97.0] - 2026-10-22
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- Requires Python 3.11 or later. On Python 3.10, pip and uv keep installing 0.96.
|
|
51
|
+
- The SDK sends its requests with [`httpx2`](https://github.com/pydantic/httpx2), the maintained
|
|
52
|
+
continuation of `httpx`. Calling the SDK is unchanged, and errors are still the `SweatStack*`
|
|
53
|
+
exceptions.
|
|
54
|
+
- HTTPS certificates are checked against the operating system's trust store instead of the
|
|
55
|
+
`certifi` bundle that `httpx` shipped. A root certificate installed on the machine, such as a
|
|
56
|
+
company proxy's, is now trusted. A Linux image without a CA bundle (the `ca-certificates`
|
|
57
|
+
package) can no longer connect; install it, or point `SSL_CERT_FILE` at a bundle.
|
|
58
|
+
|
|
59
|
+
|
|
14
60
|
## [0.96.0] - 2026-10-16
|
|
15
61
|
|
|
16
62
|
### Added
|
|
@@ -17,7 +17,7 @@ uv run python -c "..." # ad-hoc scripts
|
|
|
17
17
|
uv run generate-response-models # regenerate OpenAPI schemas (see below)
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Python ≥ 3.
|
|
20
|
+
Python ≥ 3.11 is supported; develop against the version pinned in
|
|
21
21
|
`.python-version`.
|
|
22
22
|
|
|
23
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sweatstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.98.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/learn/python/
|
|
@@ -16,14 +16,13 @@ Classifier: Development Status :: 4 - Beta
|
|
|
16
16
|
Classifier: Intended Audience :: Developers
|
|
17
17
|
Classifier: License :: OSI Approved :: MIT License
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
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.11
|
|
25
24
|
Requires-Dist: email-validator>=2.2.0
|
|
26
|
-
Requires-Dist:
|
|
25
|
+
Requires-Dist: httpx2>=2.16.0
|
|
27
26
|
Requires-Dist: open-sport-taxonomy[pydantic]>=0.10.0
|
|
28
27
|
Requires-Dist: platformdirs>=4.0.0
|
|
29
28
|
Requires-Dist: pydantic>=2.10.5
|
|
@@ -37,6 +36,7 @@ Requires-Dist: pandas>=2.2.3; extra == 'pandas'
|
|
|
37
36
|
Requires-Dist: pyarrow>=20.0.0; extra == 'pandas'
|
|
38
37
|
Provides-Extra: polars
|
|
39
38
|
Requires-Dist: polars>=1.30; extra == 'polars'
|
|
39
|
+
Requires-Dist: pyarrow>=20.0.0; (sys_platform == 'emscripten') and extra == 'polars'
|
|
40
40
|
Provides-Extra: streamlit
|
|
41
41
|
Requires-Dist: pandas>=2.2.3; extra == 'streamlit'
|
|
42
42
|
Requires-Dist: pyarrow>=20.0.0; extra == 'streamlit'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# /// script
|
|
3
3
|
# dependencies = [
|
|
4
|
-
# "
|
|
4
|
+
# "httpx2",
|
|
5
5
|
# ]
|
|
6
6
|
# ///
|
|
7
7
|
"""
|
|
@@ -32,9 +32,9 @@ import hmac
|
|
|
32
32
|
import json
|
|
33
33
|
import sys
|
|
34
34
|
import time
|
|
35
|
-
from datetime import
|
|
35
|
+
from datetime import UTC, datetime
|
|
36
36
|
|
|
37
|
-
import
|
|
37
|
+
import httpx2
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def create_signature(payload: bytes, secret: str, timestamp: int) -> str:
|
|
@@ -55,14 +55,14 @@ def send_webhook(
|
|
|
55
55
|
resource_id: str,
|
|
56
56
|
secret: str,
|
|
57
57
|
verbose: bool = False,
|
|
58
|
-
) ->
|
|
58
|
+
) -> httpx2.Response:
|
|
59
59
|
"""Send a webhook to the specified URL."""
|
|
60
60
|
# Create payload
|
|
61
61
|
payload = {
|
|
62
62
|
"user_id": user_id,
|
|
63
63
|
"event_type": event_type,
|
|
64
64
|
"resource_id": resource_id,
|
|
65
|
-
"timestamp": datetime.now(
|
|
65
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
payload_bytes = json.dumps(payload).encode()
|
|
@@ -79,7 +79,7 @@ def send_webhook(
|
|
|
79
79
|
print(f"{'=' * 60}\n")
|
|
80
80
|
|
|
81
81
|
# Send request
|
|
82
|
-
response =
|
|
82
|
+
response = httpx2.post(
|
|
83
83
|
url,
|
|
84
84
|
content=payload_bytes,
|
|
85
85
|
headers={
|
|
@@ -176,10 +176,10 @@ Examples:
|
|
|
176
176
|
if args.verbose and response.headers:
|
|
177
177
|
print(f" Response headers: {dict(response.headers)}")
|
|
178
178
|
|
|
179
|
-
except
|
|
179
|
+
except httpx2.ConnectError:
|
|
180
180
|
print(f"✗ Connection failed - is the server running at {args.url}?")
|
|
181
181
|
sys.exit(1)
|
|
182
|
-
except
|
|
182
|
+
except httpx2.TimeoutException:
|
|
183
183
|
print("✗ Request timed out")
|
|
184
184
|
sys.exit(1)
|
|
185
185
|
except Exception as e:
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sweatstack"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.98.0"
|
|
4
4
|
description = "The official Python client for SweatStack"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "Aart Goossens", email = "aart@sweatstack.no" }
|
|
9
9
|
]
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
11
|
keywords = ["sweatstack", "sports", "fitness", "api", "client"]
|
|
12
12
|
classifiers = [
|
|
13
13
|
"Development Status :: 4 - Beta",
|
|
14
14
|
"Intended Audience :: Developers",
|
|
15
15
|
"License :: OSI Approved :: MIT License",
|
|
16
16
|
"Programming Language :: Python :: 3",
|
|
17
|
-
"Programming Language :: Python :: 3.10",
|
|
18
17
|
"Programming Language :: Python :: 3.11",
|
|
19
18
|
"Programming Language :: Python :: 3.12",
|
|
20
19
|
"Programming Language :: Python :: 3.13",
|
|
@@ -24,7 +23,7 @@ classifiers = [
|
|
|
24
23
|
# consumers and anyone who only needs the models. CI's bare-install job keeps it so.
|
|
25
24
|
dependencies = [
|
|
26
25
|
"email-validator>=2.2.0",
|
|
27
|
-
"
|
|
26
|
+
"httpx2>=2.16.0",
|
|
28
27
|
"open-sport-taxonomy[pydantic]>=0.10.0",
|
|
29
28
|
"platformdirs>=4.0.0",
|
|
30
29
|
"pydantic>=2.10.5",
|
|
@@ -54,6 +53,8 @@ polars = [
|
|
|
54
53
|
# >=1.30 verified: reads the server's float16 columns (as Float32 before Float16
|
|
55
54
|
# support landed, as Float16 after; the client casts to Float32 either way).
|
|
56
55
|
"polars>=1.30",
|
|
56
|
+
# Pyodide's Polars has no Parquet reader; the client reads through pyarrow there.
|
|
57
|
+
"pyarrow>=20.0.0; sys_platform == 'emscripten'",
|
|
57
58
|
]
|
|
58
59
|
streamlit = [
|
|
59
60
|
"sweatstack[pandas]",
|
|
@@ -91,7 +92,7 @@ evals = [
|
|
|
91
92
|
|
|
92
93
|
[tool.ruff]
|
|
93
94
|
line-length = 100
|
|
94
|
-
target-version = "
|
|
95
|
+
target-version = "py311"
|
|
95
96
|
extend-exclude = ["src/sweatstack/openapi_schemas.py"]
|
|
96
97
|
|
|
97
98
|
[tool.ruff.lint]
|
|
@@ -88,7 +88,8 @@ pandas (`summary.power.mean`) and structs in Polars (`.unnest("summary")`).
|
|
|
88
88
|
## Authentication
|
|
89
89
|
|
|
90
90
|
- Scripts and notebooks: `client.authenticate()` (browser once, tokens saved on the machine).
|
|
91
|
-
- Headless: set `SWEATSTACK_API_KEY` (and `SWEATSTACK_REFRESH_TOKEN` for refresh
|
|
91
|
+
- Headless: set `SWEATSTACK_API_KEY` (and `SWEATSTACK_REFRESH_TOKEN` for refresh, with
|
|
92
|
+
`SWEATSTACK_CLIENT_ID` and `SWEATSTACK_CLIENT_SECRET` when your own app issued them); no
|
|
92
93
|
`authenticate()` call.
|
|
93
94
|
- Apps where other people sign in: the Streamlit or FastAPI helper, see
|
|
94
95
|
[streamlit.md](streamlit.md) and [fastapi.md](fastapi.md). In your own server, build one
|
|
@@ -98,7 +99,7 @@ pandas (`summary.power.mean`) and structs in Polars (`.unnest("summary")`).
|
|
|
98
99
|
|
|
99
100
|
## Errors
|
|
100
101
|
|
|
101
|
-
Everything derives from `SweatStackError`; never import `
|
|
102
|
+
Everything derives from `SweatStackError`; never import `httpx2`. `SweatStackAPIError` (has
|
|
102
103
|
`status_code`, `url`, `method`, `request_id`, `body`) splits into `SweatStackAuthError` (401,
|
|
103
104
|
403), `SweatStackNotFoundError` (404), `SweatStackRateLimitError` (429, `retry_after`),
|
|
104
105
|
`SweatStackBadRequestError` (other 4xx) and `SweatStackServerError` (5xx). Also
|
|
@@ -124,7 +124,8 @@ GET, PUT and DELETE are retried (connection errors, timeouts, 408, 429, 5xx) up
|
|
|
124
124
|
`max_retries` times; POST never is. Use `max_retries=0` inside web requests.
|
|
125
125
|
|
|
126
126
|
The access and refresh token come from one source: the arguments, else `SWEATSTACK_API_KEY`
|
|
127
|
-
and `SWEATSTACK_REFRESH_TOKEN
|
|
127
|
+
and `SWEATSTACK_REFRESH_TOKEN` (renewed by the app in `SWEATSTACK_CLIENT_ID` and
|
|
128
|
+
`SWEATSTACK_CLIENT_SECRET`, if set), else the tokens `authenticate()` saved on this machine. A
|
|
128
129
|
client given only an access token cannot renew it and raises `SweatStackTokenRefreshError` on
|
|
129
130
|
expiry. In a server, build each user's client from that user's tokens with
|
|
130
131
|
`ambient_credentials=False`: it then never reads or writes the machine's tokens.
|
|
@@ -48,7 +48,8 @@ taxonomy is preserved (`is_standard` is `False`) instead of failing.
|
|
|
48
48
|
`end_local`, `results` (`TestResults`: thresholds `lt1`, `lt2`, `vt1`, `vt2`, `mlss`, `fatmax`
|
|
49
49
|
as `Marker`s, plus `vo2max`, `critical_power`, `w_prime`, ...), `tags`, `created_by`;
|
|
50
50
|
`TestDetails` adds the resolved `traces` and overlapping `activities`.
|
|
51
|
-
- **DailyResponse**: `date`, `value`, `status`, `source
|
|
51
|
+
- **DailyResponse**: `date`, `value`, `status`, `source` (`DailySource`, an open set: `manual`,
|
|
52
|
+
`garmin_connect`, ...; markers and max efforts carry it too).
|
|
52
53
|
- **UserSummary** (`users.list()`): `id`, `first_name`, `last_name`, `display_name`, `scopes`,
|
|
53
54
|
`is_managed`.
|
|
54
55
|
- **UserResponse** (`users.create()`, `retrieve()`, `update()`): `id`, `first_name`,
|
|
@@ -20,9 +20,10 @@ package imports without any of them (enforced by CI's bare-install job).
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import json
|
|
23
|
+
import sys
|
|
23
24
|
import warnings
|
|
24
25
|
from collections.abc import Sequence
|
|
25
|
-
from datetime import
|
|
26
|
+
from datetime import UTC, datetime
|
|
26
27
|
from enum import Enum
|
|
27
28
|
from functools import cache
|
|
28
29
|
from importlib import import_module
|
|
@@ -185,15 +186,22 @@ def parquet_to_pandas(content: bytes) -> pd.DataFrame:
|
|
|
185
186
|
|
|
186
187
|
def parquet_to_polars(content: bytes) -> pl.DataFrame:
|
|
187
188
|
"""Parquet bytes to a Polars DataFrame with the wire dtypes, except
|
|
188
|
-
Float16 -> Float32 (Polars supports few operations on Float16).
|
|
189
|
+
Float16 -> Float32 (Polars supports few operations on Float16).
|
|
190
|
+
|
|
191
|
+
In the browser (Pyodide), Polars is built without its Parquet reader, so the bytes go
|
|
192
|
+
through pyarrow, which Pyodide ships, and ``pl.from_arrow``."""
|
|
189
193
|
pl = require("polars")
|
|
190
194
|
|
|
191
|
-
|
|
195
|
+
if sys.platform == "emscripten":
|
|
196
|
+
df = pl.from_arrow(parquet_to_arrow(content))
|
|
197
|
+
else:
|
|
198
|
+
df = pl.read_parquet(BytesIO(content))
|
|
199
|
+
metadata = pl.read_parquet_metadata(BytesIO(content))
|
|
200
|
+
df = df.select(_index_columns_first(df.columns, metadata.get("pandas")))
|
|
192
201
|
float16 = getattr(pl, "Float16", None) # older Polars already reads float16 as Float32
|
|
193
202
|
if float16 is not None:
|
|
194
203
|
df = df.cast({name: pl.Float32 for name, dtype in df.schema.items() if dtype == float16})
|
|
195
|
-
|
|
196
|
-
return df.select(_index_columns_first(df.columns, metadata.get("pandas")))
|
|
204
|
+
return df
|
|
197
205
|
|
|
198
206
|
|
|
199
207
|
def parquet_to_arrow(content: bytes) -> Any:
|
|
@@ -489,7 +497,7 @@ def _plain_value(value: Any, node: FieldType) -> Any:
|
|
|
489
497
|
if node == "string":
|
|
490
498
|
return value if isinstance(value, str) else str(value) # Sport and other value objects
|
|
491
499
|
if node == "datetime" and isinstance(value, datetime) and value.tzinfo is None:
|
|
492
|
-
return value.replace(tzinfo=
|
|
500
|
+
return value.replace(tzinfo=UTC) # naive value in a UTC column
|
|
493
501
|
if node == "naive_datetime" and isinstance(value, datetime) and value.tzinfo is not None:
|
|
494
|
-
return value.astimezone(
|
|
502
|
+
return value.astimezone(UTC).replace(tzinfo=None)
|
|
495
503
|
return value # date, timedelta, bool, int, float, datetime already matching
|
|
@@ -4,7 +4,7 @@ The policy follows the OpenAI, Anthropic and Stripe clients, with one difference
|
|
|
4
4
|
never retried, because the SweatStack API has no idempotency keys and a retried create could
|
|
5
5
|
create twice.
|
|
6
6
|
|
|
7
|
-
This module is also the only place in the package that calls ``
|
|
7
|
+
This module is also the only place in the package that calls ``httpx2`` to make a request:
|
|
8
8
|
through a client's pool, or through :func:`post` for the token calls the FastAPI and Streamlit
|
|
9
9
|
integrations make without a client. Both put the SDK's ``User-Agent`` on the wire, and
|
|
10
10
|
``tests/test_transport.py`` fails on a request made from anywhere else.
|
|
@@ -17,7 +17,7 @@ from collections.abc import Iterator
|
|
|
17
17
|
from contextlib import contextmanager
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
|
-
import
|
|
20
|
+
import httpx2
|
|
21
21
|
|
|
22
22
|
from .constants import USER_AGENT
|
|
23
23
|
|
|
@@ -42,7 +42,7 @@ def backoff(attempt: int) -> float:
|
|
|
42
42
|
return min(0.5 * 2**attempt * random.uniform(0.75, 1.25), MAX_BACKOFF)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def retry_after(response:
|
|
45
|
+
def retry_after(response: httpx2.Response) -> float | None:
|
|
46
46
|
"""The response's ``Retry-After`` in seconds, if it sent one as a number."""
|
|
47
47
|
try:
|
|
48
48
|
return float(response.headers["retry-after"])
|
|
@@ -55,7 +55,7 @@ def wait_before_retry(
|
|
|
55
55
|
attempt: int,
|
|
56
56
|
max_retries: int,
|
|
57
57
|
waited: float,
|
|
58
|
-
response:
|
|
58
|
+
response: httpx2.Response | None = None,
|
|
59
59
|
) -> float | None:
|
|
60
60
|
"""How long to wait before retrying, or ``None`` to stop.
|
|
61
61
|
|
|
@@ -80,16 +80,16 @@ def wait_before_retry(
|
|
|
80
80
|
return wait
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
def post(url: str, **kwargs: Any) ->
|
|
84
|
-
"""``
|
|
83
|
+
def post(url: str, **kwargs: Any) -> httpx2.Response:
|
|
84
|
+
"""``httpx2.post`` with the SDK's ``User-Agent``; the caller's headers win on a clash.
|
|
85
85
|
|
|
86
86
|
For the one-off token calls the integrations make without a ``Client``. It adds the header
|
|
87
87
|
and nothing else: no retry (a ``POST`` is never retried), no error translation and no
|
|
88
88
|
timeout, so each caller keeps its own.
|
|
89
89
|
"""
|
|
90
|
-
headers =
|
|
90
|
+
headers = httpx2.Headers({"User-Agent": USER_AGENT})
|
|
91
91
|
headers.update(kwargs.pop("headers", None) or {})
|
|
92
|
-
return
|
|
92
|
+
return httpx2.post(url, headers=headers, **kwargs)
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
class Session:
|
|
@@ -100,29 +100,29 @@ class Session:
|
|
|
100
100
|
endpoints must never see it.
|
|
101
101
|
"""
|
|
102
102
|
|
|
103
|
-
def __init__(self, http:
|
|
103
|
+
def __init__(self, http: httpx2.Client, headers: httpx2.Headers, timeout: float) -> None:
|
|
104
104
|
self._http = http
|
|
105
105
|
self.headers = headers
|
|
106
106
|
self._timeout = timeout
|
|
107
107
|
|
|
108
|
-
def request(self, method: str, url: str, **kwargs: Any) ->
|
|
108
|
+
def request(self, method: str, url: str, **kwargs: Any) -> httpx2.Response:
|
|
109
109
|
return self._http.request(method, url, headers=self.headers, **kwargs)
|
|
110
110
|
|
|
111
|
-
def get(self, url: str, **kwargs: Any) ->
|
|
111
|
+
def get(self, url: str, **kwargs: Any) -> httpx2.Response:
|
|
112
112
|
return self.request("GET", url, **kwargs)
|
|
113
113
|
|
|
114
|
-
def post(self, url: str, **kwargs: Any) ->
|
|
114
|
+
def post(self, url: str, **kwargs: Any) -> httpx2.Response:
|
|
115
115
|
return self.request("POST", url, **kwargs)
|
|
116
116
|
|
|
117
|
-
def put(self, url: str, **kwargs: Any) ->
|
|
117
|
+
def put(self, url: str, **kwargs: Any) -> httpx2.Response:
|
|
118
118
|
return self.request("PUT", url, **kwargs)
|
|
119
119
|
|
|
120
|
-
def delete(self, url: str, **kwargs: Any) ->
|
|
120
|
+
def delete(self, url: str, **kwargs: Any) -> httpx2.Response:
|
|
121
121
|
return self.request("DELETE", url, **kwargs)
|
|
122
122
|
|
|
123
123
|
@contextmanager
|
|
124
|
-
def stream(self, method: str, url: str, **kwargs: Any) -> Iterator[
|
|
124
|
+
def stream(self, method: str, url: str, **kwargs: Any) -> Iterator[httpx2.Response]:
|
|
125
125
|
"""A streaming request. No read timeout: a stream may stay quiet for a while."""
|
|
126
|
-
timeout =
|
|
126
|
+
timeout = httpx2.Timeout(self._timeout, read=None)
|
|
127
127
|
with self._http.stream(method, url, headers=self.headers, timeout=timeout, **kwargs) as r:
|
|
128
128
|
yield r
|
|
@@ -2,7 +2,7 @@ import ast
|
|
|
2
2
|
import re
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import httpx2
|
|
6
6
|
from datamodel_code_generator import DataModelType, InputFileType, generate
|
|
7
7
|
|
|
8
8
|
|
|
@@ -95,7 +95,7 @@ def _restore_naive_local_datetimes(path: Path) -> None:
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
def generate_response_models():
|
|
98
|
-
response =
|
|
98
|
+
response = httpx2.get("http://localhost:8080/openapi.json")
|
|
99
99
|
response.raise_for_status()
|
|
100
100
|
output_directory = Path(__file__).parent
|
|
101
101
|
output = Path(output_directory / "openapi_schemas.py")
|