sweatstack 0.89.0__tar.gz → 0.91.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.91.0/.git-blame-ignore-revs +3 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/.gitignore +5 -3
- sweatstack-0.91.0/.gitleaks.toml +9 -0
- sweatstack-0.91.0/AGENTS.md +378 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/CHANGELOG.md +245 -1
- sweatstack-0.91.0/DEVELOPMENT.md +169 -0
- sweatstack-0.91.0/Makefile +19 -0
- sweatstack-0.91.0/PKG-INFO +103 -0
- sweatstack-0.91.0/README.md +59 -0
- sweatstack-0.91.0/evals/__init__.py +1 -0
- sweatstack-0.91.0/evals/agent/__init__.py +5 -0
- sweatstack-0.91.0/evals/agent/agent.py +47 -0
- sweatstack-0.91.0/evals/agent/cases.py +132 -0
- sweatstack-0.91.0/evals/agent/evaluators.py +131 -0
- sweatstack-0.91.0/evals/agent/harness.py +159 -0
- sweatstack-0.91.0/evals/agent/lifecycle.py +64 -0
- sweatstack-0.91.0/evals/agent/run.py +94 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/examples/fastapi_webhooks_example.py +4 -5
- {sweatstack-0.89.0 → sweatstack-0.91.0}/examples/send_webhook.py +9 -7
- {sweatstack-0.89.0 → sweatstack-0.91.0}/plans/002_TYPED_EXCEPTIONS.md +1 -2
- {sweatstack-0.89.0 → sweatstack-0.91.0}/plans/003_trace_test_linking.md +7 -9
- {sweatstack-0.89.0 → sweatstack-0.91.0}/plans/005_ost_sport_bridge.md +11 -25
- {sweatstack-0.89.0 → sweatstack-0.91.0}/plans/006_output_backends.md +20 -25
- {sweatstack-0.89.0 → sweatstack-0.91.0}/plans/007_account_status_userinfo_portal.md +3 -5
- sweatstack-0.91.0/plans/008_sdk_documentation.md +327 -0
- sweatstack-0.91.0/plans/009_road_to_1_0.md +736 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/pyproject.toml +31 -15
- sweatstack-0.91.0/skills/sweatstack-python/SKILL.md +109 -0
- sweatstack-0.91.0/skills/sweatstack-python/api.md +124 -0
- sweatstack-0.91.0/skills/sweatstack-python/data-models.md +65 -0
- {sweatstack-0.89.0/.claude → sweatstack-0.91.0}/skills/sweatstack-python/fastapi.md +9 -8
- {sweatstack-0.89.0/.claude → sweatstack-0.91.0}/skills/sweatstack-python/streamlit.md +9 -9
- sweatstack-0.91.0/src/sweatstack/__init__.py +34 -0
- sweatstack-0.91.0/src/sweatstack/_docs.py +391 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/_frames.py +45 -18
- sweatstack-0.91.0/src/sweatstack/_renames.py +93 -0
- sweatstack-0.91.0/src/sweatstack/_transport.py +109 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/cli.py +23 -12
- sweatstack-0.91.0/src/sweatstack/client.py +1131 -0
- sweatstack-0.91.0/src/sweatstack/constants.py +1 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/exceptions.py +9 -1
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/fastapi/__init__.py +9 -9
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/fastapi/access_token_cache.py +6 -13
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/fastapi/config.py +7 -7
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/fastapi/dependencies.py +13 -20
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/fastapi/routes.py +19 -14
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/fastapi/webhooks.py +0 -1
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/openapi_schemas.py +422 -8
- sweatstack-0.91.0/src/sweatstack/resources/__init__.py +38 -0
- sweatstack-0.91.0/src/sweatstack/resources/_app_metadata.py +247 -0
- sweatstack-0.91.0/src/sweatstack/resources/_base.py +83 -0
- sweatstack-0.91.0/src/sweatstack/resources/activities.py +1001 -0
- sweatstack-0.91.0/src/sweatstack/resources/dailies.py +176 -0
- sweatstack-0.91.0/src/sweatstack/resources/oauth.py +170 -0
- sweatstack-0.91.0/src/sweatstack/resources/portal.py +87 -0
- sweatstack-0.91.0/src/sweatstack/resources/profile.py +106 -0
- sweatstack-0.91.0/src/sweatstack/resources/teams.py +122 -0
- sweatstack-0.91.0/src/sweatstack/resources/tests.py +337 -0
- sweatstack-0.91.0/src/sweatstack/resources/traces.py +314 -0
- sweatstack-0.91.0/src/sweatstack/resources/users.py +199 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/schemas.py +34 -11
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/streamlit.py +80 -65
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/utils.py +13 -5
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_access_token_cache.py +10 -20
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_account_status.py +78 -31
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_dailies.py +0 -2
- sweatstack-0.91.0/tests/test_docs_check.py +140 -0
- sweatstack-0.91.0/tests/test_docstrings.py +71 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_dtype_conversion.py +214 -140
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_exceptions.py +3 -5
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_frames.py +50 -20
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_identity.py +38 -12
- sweatstack-0.91.0/tests/test_longitudinal_mean_max_after.py +147 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_metadata.py +46 -12
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_output.py +98 -60
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_portal.py +19 -9
- sweatstack-0.91.0/tests/test_public_hygiene.py +78 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_public_surface.py +39 -16
- sweatstack-0.91.0/tests/test_renames.py +61 -0
- sweatstack-0.91.0/tests/test_resources.py +263 -0
- sweatstack-0.91.0/tests/test_segmentation.py +92 -0
- sweatstack-0.91.0/tests/test_skill.py +49 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_sport_ost.py +32 -13
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_teams.py +0 -1
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_tests.py +1 -3
- sweatstack-0.91.0/tests/test_transport.py +218 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_webhooks.py +3 -3
- sweatstack-0.91.0/tests/test_wire_names.py +93 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_write_timezone_validation.py +12 -15
- sweatstack-0.91.0/tests/typing/output_types.py +41 -0
- sweatstack-0.91.0/uv.lock +3357 -0
- sweatstack-0.89.0/.claude/settings.local.json +0 -39
- sweatstack-0.89.0/.claude/skills/sweatstack-python/SKILL.md +0 -55
- sweatstack-0.89.0/.claude/skills/sweatstack-python/client.md +0 -500
- sweatstack-0.89.0/.claude/skills/sweatstack-python/data-models.md +0 -77
- sweatstack-0.89.0/AGENTS.md +0 -285
- sweatstack-0.89.0/DEVELOPMENT.md +0 -105
- sweatstack-0.89.0/Makefile +0 -12
- sweatstack-0.89.0/PKG-INFO +0 -146
- sweatstack-0.89.0/README.md +0 -96
- sweatstack-0.89.0/docs/conf.py +0 -26
- sweatstack-0.89.0/docs/everything.rst +0 -265
- sweatstack-0.89.0/docs/index.rst +0 -7
- sweatstack-0.89.0/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -345
- sweatstack-0.89.0/src/sweatstack/__init__.py +0 -26
- sweatstack-0.89.0/src/sweatstack/client.py +0 -3614
- sweatstack-0.89.0/src/sweatstack/constants.py +0 -1
- sweatstack-0.89.0/src/sweatstack/ipython_init.py +0 -8
- sweatstack-0.89.0/src/sweatstack/jupyterlab_oauth2_startup.py +0 -29
- sweatstack-0.89.0/src/sweatstack/sweatshell.py +0 -14
- sweatstack-0.89.0/tests/test_longitudinal_mean_max_after.py +0 -133
- sweatstack-0.89.0/tests/test_segmentation.py +0 -69
- sweatstack-0.89.0/uv.lock +0 -3114
- {sweatstack-0.89.0 → sweatstack-0.91.0}/.github/workflows/ci.yml +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/.python-version +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/CONTRIBUTING.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/LICENSE +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/plans/001a_tests.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/plans/001b_metadata.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/plans/001c_dailies.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/plans/004_codebase_hygiene.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/__init__.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.91.0}/tests/test_trace_test_linking.py +0 -0
|
@@ -40,6 +40,8 @@ dump/
|
|
|
40
40
|
# Playground
|
|
41
41
|
playground/
|
|
42
42
|
|
|
43
|
-
#
|
|
44
|
-
.claude/
|
|
45
|
-
|
|
43
|
+
# Agent tooling (local only; the published skill lives in skills/)
|
|
44
|
+
.claude/
|
|
45
|
+
|
|
46
|
+
# Agent test workspaces and program output: real user data
|
|
47
|
+
evals/agent/runs/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# gitleaks configuration: the default rules, plus known false positives.
|
|
2
|
+
# Run before going public and before each release: `gitleaks git .`
|
|
3
|
+
|
|
4
|
+
[extend]
|
|
5
|
+
useDefault = true
|
|
6
|
+
|
|
7
|
+
[[allowlists]]
|
|
8
|
+
description = "The Fernet key the tests use; it decodes to 'test-only-key-for-unit-tests-32b'"
|
|
9
|
+
regexes = ['''dGVzdC1vbmx5LWtleS1mb3ItdW5pdC10ZXN0cy0zMmI=''']
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Working agreement for agents (and humans) changing this codebase. Read end
|
|
4
|
+
to end before your first change. Mechanics — install, test, regen, release —
|
|
5
|
+
live in [DEVELOPMENT.md](DEVELOPMENT.md).
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Public repository
|
|
9
|
+
|
|
10
|
+
This repository is public, and so is everything in its history. Treat every
|
|
11
|
+
change as published the moment it is committed: code, comments, docstrings,
|
|
12
|
+
tests and fixtures, `plans/`, `skills/`, examples, notebooks, the CHANGELOG,
|
|
13
|
+
commit messages and PR descriptions. Removing something later does not
|
|
14
|
+
unpublish it.
|
|
15
|
+
|
|
16
|
+
Never commit:
|
|
17
|
+
|
|
18
|
+
- **Secrets and credentials**: API keys, tokens, client secrets, session
|
|
19
|
+
keys, `.env` contents. Not even expired or test-account ones.
|
|
20
|
+
- **Real identifiers**: user, app, activity or team IDs, emails, client
|
|
21
|
+
IDs, or truncated prefixes of them. Fixtures and examples use obvious
|
|
22
|
+
fakes: `app_123`, `user_123`, `YOUR_CLIENT_ID`.
|
|
23
|
+
- **Customers and their behaviour**: names of apps, companies or people
|
|
24
|
+
using SweatStack, how many there are, what they call or filter on, or
|
|
25
|
+
anything learned from server logs, traffic or support conversations.
|
|
26
|
+
Write "SDK users" or "downstream apps".
|
|
27
|
+
- **The private server codebase**: its file paths, function names, commit
|
|
28
|
+
hashes, branch names and plan numbers. Describe the server by its public
|
|
29
|
+
contract: the OpenAPI schema and docs.sweatstack.no. "The server now
|
|
30
|
+
returns X" is fine; "see server plan N" is not.
|
|
31
|
+
- **Local environments**: home directories, machine paths, names of
|
|
32
|
+
private sibling repos, personal data in notebook outputs or test data.
|
|
33
|
+
Commit notebooks with outputs cleared.
|
|
34
|
+
|
|
35
|
+
When unsure, leave it out and ask. `tests/test_public_hygiene.py` catches
|
|
36
|
+
the patterns a regex can (real-looking IDs, JWTs, keys, home paths, server
|
|
37
|
+
references, notebook outputs). Names and behaviour of customers need you
|
|
38
|
+
to notice them; there is no list to check against, because the list itself
|
|
39
|
+
would be the leak.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Guiding principle: mirror the REST API
|
|
43
|
+
|
|
44
|
+
This client is a thin Python projection of the SweatStack REST API. Every endpoint group is a
|
|
45
|
+
resource namespace on `Client`, and every name follows from the URL by eight rules. Apply them;
|
|
46
|
+
don't invent names.
|
|
47
|
+
|
|
48
|
+
| # | Rule | Examples |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| R1 | **The first URL segment after `/api/v1/` is the top-level namespace**, a typed `cached_property` on `Client`. | `/activities/...` → `client.activities` |
|
|
51
|
+
| R2 | **Collection verbs follow Stripe:** `list`, `retrieve`, `create`, `delete`. | `GET /tests/{id}` → `tests.retrieve(id)` |
|
|
52
|
+
| R3 | **Name writes by what they do, not by the HTTP verb.** Overwrites every field and clears what you leave out: `replace`. Changes only the fields you pass: `update`. Read the server code to tell which; the verb doesn't say. | `PUT /traces/{id}` → `traces.replace`; `PUT /users/{id}` → `users.update` |
|
|
53
|
+
| R4 | **A single value that is set or cleared** (an upsert, or a blob with no ID) uses `set` and `delete`. | `POST /dailies/{measure}` → `dailies.set` |
|
|
54
|
+
| R5 | **A nested collection is a nested namespace**, parent ID first. | `/portal/sessions` → `portal.sessions.create`; `.../{id}/app-metadata` → `activities.app_metadata.set(id, ...)` |
|
|
55
|
+
| R6 | **A sub-path with a single `GET` is a method named after the noun.** | `GET /activities/{id}/data` → `activities.data(id)` |
|
|
56
|
+
| R7 | **An action endpoint (`POST .../<verb>`) is a method named after the verb.** | `POST /activities/upload` → `activities.upload` |
|
|
57
|
+
| R8 | **Parameter names are the wire names.** A repeatable query parameter takes one value or a list under the same name (normalise with `Resource._wire`). | `?sport=a&sport=b` → `sport=["a", "b"]` or `sport="a"` |
|
|
58
|
+
|
|
59
|
+
Match the server's field names, enum values and write semantics. Don't redesign the API in
|
|
60
|
+
Python.
|
|
61
|
+
|
|
62
|
+
**Deliberate deviations** (these are the only ones; don't invent more):
|
|
63
|
+
|
|
64
|
+
- Path-param IDs are positional Python args (`tests.retrieve(test_id)`); everything else is
|
|
65
|
+
keyword-only.
|
|
66
|
+
- Enum-typed params accept `Enum | str` so callers don't need to import the enum.
|
|
67
|
+
- `limit` and `offset` on list methods are SDK-level: `limit` is how many items the caller gets,
|
|
68
|
+
and `Resource._paginate` pages through the endpoint to get them.
|
|
69
|
+
- A `traces=` query parameter is `trace_resolution=` on `tests.retrieve`, where the wire name
|
|
70
|
+
would be ambiguous. The docstring states the mapping.
|
|
71
|
+
- `name=` on `users.list` and `teams.users` filters client-side (case-insensitive substring of
|
|
72
|
+
the display name) until the server offers the filter; then it becomes the wire parameter
|
|
73
|
+
with the same name and semantics. Don't add other client-side filters.
|
|
74
|
+
- `output=` is a client-side choice of container on the data endpoints. See "Output backends".
|
|
75
|
+
- `activities.longitudinal` groups the three `/activities/longitudinal-*` endpoints.
|
|
76
|
+
- Convenience composites that wrap several calls are a last resort. The 0.91 redesign removed
|
|
77
|
+
them (`get_latest_activity_data` became chaining); add one only when a real workflow is
|
|
78
|
+
awkward through the literal surface.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## Project shape
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
src/sweatstack/
|
|
85
|
+
├── openapi_schemas.py # AUTO-GENERATED. Never hand-edit.
|
|
86
|
+
├── schemas.py # Re-exports (incl. OST Sport/Modifier) + Metric/Scope/DailyMeasure helpers.
|
|
87
|
+
├── _frames.py # output= backends: parquet/models -> pandas/polars/arrow/bytes.
|
|
88
|
+
├── _renames.py # Removed names -> replacement hints. Permanent; see "Breaking changes".
|
|
89
|
+
├── _transport.py # Retry policy and the per-request Session on the client's connection pool.
|
|
90
|
+
├── exceptions.py # Public error contract. No httpx types leak.
|
|
91
|
+
├── client.py # Client: config, auth, delegation, transport (_request), resource attributes,
|
|
92
|
+
│ # and the module-level interface (one shared default client).
|
|
93
|
+
├── resources/ # One module per URL segment: activities.py, traces.py, tests.py, ...
|
|
94
|
+
│ ├── _base.py # Resource, _paginate, _wire, shared parameter types.
|
|
95
|
+
│ └── _app_metadata.py # One app-metadata class per endpoint (Activity/Trace/Test/Profile).
|
|
96
|
+
├── utils.py # Dataframe / JWT helpers.
|
|
97
|
+
├── streamlit.py # Streamlit integration (optional extra).
|
|
98
|
+
├── fastapi/ # FastAPI integration (optional extra).
|
|
99
|
+
└── cli.py # Entry points (schema regeneration).
|
|
100
|
+
|
|
101
|
+
skills/sweatstack-python/ # The published agent skill (npx skills add). Update with the public surface.
|
|
102
|
+
plans/ # Design plans, numbered. Public: see "Public repository".
|
|
103
|
+
tests/typing/ # Static type tests, checked by ty, never run by pytest.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`.claude/` is git-ignored: it is local agent tooling only.
|
|
107
|
+
|
|
108
|
+
Adding a new Pydantic model from the server takes three steps:
|
|
109
|
+
|
|
110
|
+
1. Regen `openapi_schemas.py` ([DEVELOPMENT.md](DEVELOPMENT.md#regenerating-openapi_schemaspy)).
|
|
111
|
+
2. Re-export it from `schemas.py`.
|
|
112
|
+
3. Import it into `client.py` and add it to `__all__` so `from sweatstack import *` exposes it.
|
|
113
|
+
|
|
114
|
+
Skipping step 3 silently breaks the public surface. Same for new enums.
|
|
115
|
+
|
|
116
|
+
Adding a new endpoint: find its namespace by R1, its name by R2–R7, write the method in that
|
|
117
|
+
resource module (template below), and add it to `skills/sweatstack-python/api.md`. A new URL
|
|
118
|
+
segment is a new resource module plus a `cached_property` on `Client`; the module level mirrors
|
|
119
|
+
it automatically.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## Hard rules
|
|
123
|
+
|
|
124
|
+
- **`uv`, never `pip`.** Every command goes through `uv run` / `uv add`.
|
|
125
|
+
- **`make check` before every commit.** Format, lint, types and tests; `make
|
|
126
|
+
publish` runs it too. Don't silence a finding without a comment saying why.
|
|
127
|
+
- **Never hand-edit `openapi_schemas.py`.** Regenerate.
|
|
128
|
+
- **`Raises:` references the typed exceptions** from `sweatstack.exceptions`.
|
|
129
|
+
- **Every public resource method's docstring has an `Endpoint:` line and an `Examples:` block**
|
|
130
|
+
with a complete, runnable snippet. `tests/test_docstrings.py` enforces both.
|
|
131
|
+
- **Public resources are typed `cached_property` attributes of `Client`**, so type checkers and
|
|
132
|
+
the snippet check resolve them statically, and a client built with `Client.__new__` still
|
|
133
|
+
has them. The module level mirrors them automatically.
|
|
134
|
+
- **A breaking change ships with its upgrade path** (see "Breaking changes").
|
|
135
|
+
- **A change to the public surface updates `skills/sweatstack-python/` in the same commit.**
|
|
136
|
+
`tests/test_skill.py` fails when `api.md` misses a method.
|
|
137
|
+
- **Enum-typed params accept `Enum | str`** and route through
|
|
138
|
+
`_enums_to_strings`. Don't introduce strict-enum-only parameters.
|
|
139
|
+
- **Open enums stay open.** When the server documents an enum as an open
|
|
140
|
+
set (metrics, scopes, daily measures, status codes, capabilities),
|
|
141
|
+
register it with `_open_enum(...)` in `schemas.py` so unknown values
|
|
142
|
+
parse as pseudo-members. Leave closed sets strict.
|
|
143
|
+
- **Never retry a request the server can't safely repeat.** `_transport.RETRYABLE_METHODS`
|
|
144
|
+
is `GET`, `PUT` and `DELETE`; a `POST` is sent once (no idempotency keys). Don't add
|
|
145
|
+
per-method retry logic: everything goes through `_request`.
|
|
146
|
+
- **Server-to-server calls pass `auth=False` to `_request`.** Endpoints that authenticate with
|
|
147
|
+
the app's own credentials in the body (Portal sessions, the token exchange) must never
|
|
148
|
+
receive a user's bearer.
|
|
149
|
+
- **`Sport` is the OpenSportTaxonomy type** (`open_sport_taxonomy.Sport`), not a generated enum.
|
|
150
|
+
Construct with `Sport("cycling.road")` or `Sport.parse(value)`; serialise with `str(sport)`. Codegen
|
|
151
|
+
binds the `sport` field to OST's permissive `SportField` (cli.py `_bind_sport_to_ost`), so regen is
|
|
152
|
+
safe and `sport`-typed fields keep decoding to `Sport`.
|
|
153
|
+
- **Tests are offline.** No network calls. Fake the transport at `Client._request` (see
|
|
154
|
+
`tests/test_resources.py`), or use `Client.__new__(Client)` to bypass init.
|
|
155
|
+
- **Frame libraries are optional extras.** Never import pandas, numpy,
|
|
156
|
+
pyarrow or polars at module top level; import inside the function, via
|
|
157
|
+
`_frames.require(...)` where an `ImportError` should name the extra.
|
|
158
|
+
CI's bare-install job fails otherwise.
|
|
159
|
+
- **No frame carries an index**, on any backend. Don't `set_index` in a
|
|
160
|
+
method; the caller does that.
|
|
161
|
+
- **`replace` methods overwrite every field.** Document the silent-clear footgun in the
|
|
162
|
+
docstring (see below).
|
|
163
|
+
- **CHANGELOG entries are user-facing**, not dev-facing.
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Method shape
|
|
167
|
+
|
|
168
|
+
Every resource method follows this shape. Match it.
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
class Things(Resource):
|
|
172
|
+
def create(
|
|
173
|
+
self,
|
|
174
|
+
*, # keyword-only (path IDs come first, positional)
|
|
175
|
+
timestamp: datetime,
|
|
176
|
+
sport: Sport | str | None = None, # enum params: Enum | str
|
|
177
|
+
tags: list[str] | None = None,
|
|
178
|
+
) -> ThingDetails:
|
|
179
|
+
"""One-line summary.
|
|
180
|
+
|
|
181
|
+
Endpoint: ``POST /api/v1/things/``
|
|
182
|
+
|
|
183
|
+
Optional paragraph for non-obvious behaviour (full replace,
|
|
184
|
+
server-side defaults, side effects).
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
ThingDetails: ...
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
SweatStackNotFoundError: If <specific condition>.
|
|
194
|
+
SweatStackAPIError: If the API request fails for any other reason.
|
|
195
|
+
|
|
196
|
+
Examples:
|
|
197
|
+
```python
|
|
198
|
+
from sweatstack import Client
|
|
199
|
+
|
|
200
|
+
client = Client()
|
|
201
|
+
thing = client.things.create(timestamp=datetime.now(timezone.utc))
|
|
202
|
+
```
|
|
203
|
+
"""
|
|
204
|
+
sport = self._client._enums_to_strings([sport])[0] if sport else None
|
|
205
|
+
response = self._client._request(
|
|
206
|
+
"post",
|
|
207
|
+
"/api/v1/things/",
|
|
208
|
+
json={"timestamp": timestamp.isoformat(), "sport": sport, "tags": tags},
|
|
209
|
+
)
|
|
210
|
+
return ThingDetails.model_validate(response.json())
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Invariants baked into the template:
|
|
214
|
+
|
|
215
|
+
- Path-param IDs positional, everything else keyword-only.
|
|
216
|
+
- Datetimes serialize via `.isoformat()`; write timestamps go through `_require_aware`.
|
|
217
|
+
- Every request goes through `self._client._request(...)`, which raises the typed exception
|
|
218
|
+
for an error status. Only streaming endpoints open `_http_client()` themselves.
|
|
219
|
+
- Return is a validated Pydantic model, never the raw dict.
|
|
220
|
+
- `replace` methods are typed `-> None`. The server's `{"message": "..."}` body carries no
|
|
221
|
+
useful info.
|
|
222
|
+
- A class that defines `list` annotates with `builtins.list[...]`: inside the class body,
|
|
223
|
+
`list` is the method.
|
|
224
|
+
|
|
225
|
+
**List endpoints** page with `self._paginate(path, params, Model, page_size=..., limit=...,
|
|
226
|
+
offset=...)` and return `self._client._frame_from_models(models, Model, output, flatten=(...))`.
|
|
227
|
+
Empty lists yield typed empty frames automatically. See `Activities.list`.
|
|
228
|
+
|
|
229
|
+
**Parquet endpoints** return `self._client._read_frame(response.content, output)`. Never call
|
|
230
|
+
`pd.read_parquet` in a method.
|
|
231
|
+
|
|
232
|
+
Both kinds take `output` as a keyword-only parameter and carry `@overload` stubs keyed on the
|
|
233
|
+
`Literal` values (clone the neighbours); `tests/typing/output_types.py` pins them.
|
|
234
|
+
|
|
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 `httpx` params:
|
|
237
|
+
`httpx` calls `str()`, which gives `"TraceResolution.linked"`, not `"linked"`.
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
## Full-replace `replace` methods
|
|
241
|
+
|
|
242
|
+
Some PUT endpoints overwrite every field, setting unsent fields to `null` (traces, tests); their
|
|
243
|
+
methods are named `replace` (R3). Two rules:
|
|
244
|
+
|
|
245
|
+
1. **Send every field in the body, even when `None`.** Don't omit.
|
|
246
|
+
2. **Document the silent-clear footgun in the docstring** whenever you
|
|
247
|
+
add a new optional field to an existing `replace` method, *and*
|
|
248
|
+
call it out in the CHANGELOG `### Changed` section. Existing callers
|
|
249
|
+
who don't know about the new field will silently null it on their
|
|
250
|
+
next call.
|
|
251
|
+
|
|
252
|
+
Don't try to soften the contract with "preserve if omitted" sentinels;
|
|
253
|
+
that diverges from how every other field behaves on these methods. A PUT that leaves unsent
|
|
254
|
+
fields unchanged (users) is an `update` and sends only the fields given.
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
## Breaking changes
|
|
258
|
+
|
|
259
|
+
Until 1.0, breaking changes are allowed when they ship with an upgrade path. Every one needs:
|
|
260
|
+
|
|
261
|
+
1. A `### Upgrading` section in the CHANGELOG entry, with an old-to-new table.
|
|
262
|
+
2. A copy-paste migration prompt for coding agents in the same section, for any change bigger
|
|
263
|
+
than a rename or two.
|
|
264
|
+
3. For a removed method or attribute, an entry in `_renames.REMOVED_IN`, so calling it raises an
|
|
265
|
+
`AttributeError` that names the replacement. Entries are permanent.
|
|
266
|
+
`tests/test_renames.py` checks each one against the client, the module and the CHANGELOG.
|
|
267
|
+
|
|
268
|
+
Removed *parameters* need no entry: Python's `TypeError` names them, and the CHANGELOG explains
|
|
269
|
+
the replacement.
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
## Output backends
|
|
273
|
+
|
|
274
|
+
`_frames.py` turns parquet bytes and lists of models into the container
|
|
275
|
+
the caller asked for. Resolution: per-call `output` > `Client(output=)` >
|
|
276
|
+
`sweatstack.set_output()` > the method's default: models for lists, and
|
|
277
|
+
for parquet the installed frame library (`_frames.installed_frame_output`:
|
|
278
|
+
Polars, then pandas, then Arrow; `ImportError` naming the extras if none).
|
|
279
|
+
A configured default a method cannot produce is skipped, a per-call one
|
|
280
|
+
is a `ValueError`. Never hard-code a frame library as a default.
|
|
281
|
+
|
|
282
|
+
**Where `output` belongs.** On the *data* endpoints only: activities,
|
|
283
|
+
traces, tests, dailies and the time series, the things a user groups,
|
|
284
|
+
filters and plots. Everything about the account, the app, teams, status
|
|
285
|
+
and the Portal (`oauth.userinfo`, `profile.status`, `portal.sessions.create`,
|
|
286
|
+
`whoami`, `users.list`, `teams.list`, `profile.sports`, `profile.tags`, ...) takes no `output` and always returns models; those
|
|
287
|
+
methods never call `_read_frame` or `_frame_from_models`, so a configured
|
|
288
|
+
output cannot reach them. A new data endpoint gets `output`; a new
|
|
289
|
+
control-plane endpoint does not.
|
|
290
|
+
|
|
291
|
+
Dtype policy: pandas gets `convert_to_standard_dtypes` (float64, ns);
|
|
292
|
+
Polars keeps wire dtypes except Float16 -> Float32; Arrow is the wire
|
|
293
|
+
table minus pandas index metadata; bytes is the body.
|
|
294
|
+
|
|
295
|
+
The Polars and Arrow paths for lists derive one type tree from each
|
|
296
|
+
model's **JSON Schema** (`_frames.field_types`) and render it per library;
|
|
297
|
+
values come from the instance.
|
|
298
|
+
**Never add a per-model special case there.** If a model needs one, the
|
|
299
|
+
JSON Schema grammar table in `_field_type` is missing a row: add the
|
|
300
|
+
row and its test. `tests/test_frames.py` turns `FrameSchemaWarning` into
|
|
301
|
+
a failure for every public model, so a regen that introduces an unknown
|
|
302
|
+
construct fails the suite, not a user's notebook.
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
## Exceptions
|
|
306
|
+
|
|
307
|
+
The hierarchy in `sweatstack/exceptions.py` is the **public** error
|
|
308
|
+
contract — consumers should never need to import `httpx`.
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
SweatStackError
|
|
312
|
+
├── SweatStackConnectionError # DNS / timeout / no response
|
|
313
|
+
├── SweatStackTokenRefreshError
|
|
314
|
+
└── SweatStackAPIError # got a response with status >= 400
|
|
315
|
+
├── SweatStackAuthError # 401, 403
|
|
316
|
+
├── SweatStackNotFoundError # 404
|
|
317
|
+
├── SweatStackRateLimitError # 429
|
|
318
|
+
├── SweatStackBadRequestError # other 4xx
|
|
319
|
+
└── SweatStackServerError # 5xx
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Docstrings: list specific subclasses for meaningful conditions (e.g. 404
|
|
323
|
+
when a path ID may not exist), then a catch-all `SweatStackAPIError`.
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
## Tests
|
|
327
|
+
|
|
328
|
+
Live in `tests/`. Offline only. Two workhorse patterns.
|
|
329
|
+
|
|
330
|
+
**Faking the transport** at `Client._request`, the one choke point, to see exactly what goes
|
|
331
|
+
on the wire:
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
with patch.object(client, "_request", return_value=response) as request:
|
|
335
|
+
client.activities.list(sport="cycling")
|
|
336
|
+
assert request.call_args.kwargs["params"]["sport"] == ["cycling"]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Schema round-tripping:**
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
restored = TraceDetails.model_validate(original.model_dump())
|
|
343
|
+
assert restored.test_id == "test_123"
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Catches missing fields, type drift, and enum-casing changes from regen.
|
|
347
|
+
Exemplars: `tests/test_resources.py`, `tests/test_trace_test_linking.py`,
|
|
348
|
+
`tests/test_dtype_conversion.py`.
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
## CHANGELOG
|
|
352
|
+
|
|
353
|
+
User-facing only. Lead with what changed for the user; skip regen
|
|
354
|
+
output, file renames, refactors. Loud-callout any behaviour change in
|
|
355
|
+
`### Changed`, especially full-replace footguns. SemVer.
|
|
356
|
+
|
|
357
|
+
Good:
|
|
358
|
+
|
|
359
|
+
> ### Changed
|
|
360
|
+
> - `traces.replace()` replaces all fields, including `test_id`. Callers
|
|
361
|
+
> that omit `test_id` will clear any existing link.
|
|
362
|
+
|
|
363
|
+
Bad (belongs in the commit message, not the changelog):
|
|
364
|
+
|
|
365
|
+
> ### Changed
|
|
366
|
+
> - Local datetime fields are now `AwareDatetime`. The
|
|
367
|
+
> `BodyExpressAddEmail...` schema is renamed to
|
|
368
|
+
> `BodyConnectAddEmail...`.
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
## When in doubt
|
|
372
|
+
|
|
373
|
+
- Match the nearest existing method in `resources/`. Consistency with the
|
|
374
|
+
surroundings beats local cleverness.
|
|
375
|
+
- Reuse the helpers: `_request`, `_paginate`, `_wire`, `_enums_to_strings`,
|
|
376
|
+
`_read_frame`, `_frame_from_models`, `_RecordAppMetadata`. Don't reinvent them.
|
|
377
|
+
- Don't add abstractions for hypothetical future flexibility. Three
|
|
378
|
+
similar blocks is the pattern, not a smell.
|
|
@@ -6,6 +6,250 @@ 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.91.0] - 2026-10-08
|
|
10
|
+
|
|
11
|
+
One namespace per API resource: `client.activities.list()` instead of `client.get_activities()`.
|
|
12
|
+
Every name now follows from the URL (`/api/v1/activities/...` is `client.activities`), so the
|
|
13
|
+
REST API reference doubles as the SDK's map. This is a breaking release with a mechanical
|
|
14
|
+
upgrade; see **Upgrading** below, or hand the migration prompt to your coding agent. Calling a
|
|
15
|
+
removed name raises an `AttributeError` that names its replacement.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **BREAKING: methods moved to resource namespaces**: `activities`, `traces`, `tests`, `dailies`,
|
|
20
|
+
`profile`, `users`, `teams`, `portal` and `oauth`, on `Client` and at module level
|
|
21
|
+
(`sweatstack.activities.list()`). The full table is under **Upgrading**.
|
|
22
|
+
- **BREAKING: `traces.replace()` and `tests.replace()`** are the old `update_trace()` and
|
|
23
|
+
`update_test()`. The new name says what they do: every field you leave out is cleared,
|
|
24
|
+
including a trace's `test_id`.
|
|
25
|
+
- **BREAKING: `sport=` replaces `sports=`** on every filter, as on the server (which deprecated
|
|
26
|
+
`sports`). It takes one sport or a list: `sport="cycling"` or `sport=["cycling", "running"]`.
|
|
27
|
+
`tags=` and `metrics=` also take a single value now.
|
|
28
|
+
- **BREAKING: `activities.latest()`** calls the API's `/activities/latest`: it takes only
|
|
29
|
+
`sport=`, and returns `None` when there is no activity (it raised `StopIteration`).
|
|
30
|
+
- **BREAKING: keyword-only arguments** where they were positional: `metric=` on
|
|
31
|
+
`activities.mean_max()` and `activities.awd()`, `segmentation_on=` and `metrics=` on
|
|
32
|
+
`activities.data()`, `first_name=` and `last_name=` on `users.create()`, `only_root=` on
|
|
33
|
+
`profile.sports()`, `scopes=` on `teams.authorize()`, and every argument of
|
|
34
|
+
`oauth.authorization_url()` and `oauth.exchange_code()`.
|
|
35
|
+
- **BREAKING: longitudinal `date=` / `window_days=`** (deprecated since 0.70) are gone; use
|
|
36
|
+
`start=` and `end=`, typed as `date` objects (`start=date(2026, 1, 1)`).
|
|
37
|
+
`sport=` is a required argument of all three longitudinal methods; the API rejected a
|
|
38
|
+
request without one.
|
|
39
|
+
- **BREAKING: `activities.upload()`** returns the processing status of each file
|
|
40
|
+
(`list[SourceResponse]`) instead of a raw dict, and no longer takes `sport=`: the server never
|
|
41
|
+
read it. A CSV carries its sport in a `sport` column (an Open Sport Taxonomy code such as
|
|
42
|
+
`cycling.road`).
|
|
43
|
+
- **BREAKING: `oauth.exchange_code()` no longer takes `redirect_uri=`**; the token endpoint never
|
|
44
|
+
read it.
|
|
45
|
+
- **BREAKING: `users.retrieve(user_id)`** is the server's `GET /users/{id}`, which only finds
|
|
46
|
+
users you manage. Find anyone else with `users.list(name=...)`, which returns every user whose
|
|
47
|
+
name contains the text, ignoring case.
|
|
48
|
+
- **BREAKING: `switch_user()` and `switch_back()` are removed.** They changed which user a shared
|
|
49
|
+
client acted as, invisibly to everything else holding it. `client.delegated_client(user)` does
|
|
50
|
+
the same job and returns a new client. It takes a user ID or a `UserSummary`, not a
|
|
51
|
+
name: `switch_user("Carla")` becomes a lookup with `users.list(name="Carla")` first.
|
|
52
|
+
- `StreamlitAuth.select_activity()` takes `sport=` instead of `sports=`.
|
|
53
|
+
- Clients from `delegated_client()` and `principal_client()` keep the app's `client_id` and
|
|
54
|
+
`client_secret`, so `portal.sessions.create()` works on them.
|
|
55
|
+
|
|
56
|
+
- **Automatic retries**: `GET`, `PUT` and `DELETE` requests are retried up to twice after a
|
|
57
|
+
connection error, a timeout, or a 408, 429 or 5xx response, with exponential backoff and the
|
|
58
|
+
server's `Retry-After` (at most 30 s of waiting per call). `POST` is never retried, so a create
|
|
59
|
+
can't happen twice. `Client(max_retries=0)` turns retries off, e.g. inside a web request.
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- `Client(timeout=60.0, max_retries=2)`: the timeout in seconds, and how often to retry.
|
|
64
|
+
Clients from `delegated_client()` and `principal_client()` keep both.
|
|
65
|
+
- One connection pool per client, reused across requests (each request opened a new
|
|
66
|
+
connection). `client.close()` releases it; `with Client() as client:` closes on exit.
|
|
67
|
+
- `users.retrieve()`, `users.update()` (changes only the fields you pass) and `users.delete()`
|
|
68
|
+
for managed users.
|
|
69
|
+
- `users.list(include_managed=, include_shared=, name=)` and `teams.users(team_id, name=)`.
|
|
70
|
+
- `SourceResponse` and `SourceError`, the upload status models.
|
|
71
|
+
- `vo2=` on `traces.create()` and `traces.replace()`; the field was already on `TraceDetails`.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- `activities.watch_backfill_status(auto_reconnect=True)` reconnects after a dropped
|
|
76
|
+
connection; it raised instead.
|
|
77
|
+
- `activities.backfill_status()` and `watch_backfill_status()` return the server's updates. Its
|
|
78
|
+
timestamps carry no UTC offset, which failed validation, and every update was skipped
|
|
79
|
+
silently. A line the client cannot parse is now logged as a warning.
|
|
80
|
+
- `StreamlitAuth.select_user()` stores the selected user's own refresh token. It kept the
|
|
81
|
+
signed-in user's, so after the first token refresh the app silently showed the signed-in
|
|
82
|
+
user's data under the selected user's name.
|
|
83
|
+
- `StreamlitAuth` keeps the app's `client_id` and `client_secret` after switching users.
|
|
84
|
+
- Threads sharing a client refresh an expired token once; they all refreshed, which fails with
|
|
85
|
+
rotating refresh tokens.
|
|
86
|
+
- `activities.watch_backfill_status()` no longer times out on a quiet stream: streams have no
|
|
87
|
+
read timeout.
|
|
88
|
+
- Searching users by name ignores case; `get_user("Carla")` found nobody where `"carla"` worked.
|
|
89
|
+
|
|
90
|
+
### Removed
|
|
91
|
+
|
|
92
|
+
- **BREAKING:** the `sweatlab` and `sweatshell` commands, their example notebook, and the
|
|
93
|
+
`[jupyter]` extra. In any notebook: `uv add "sweatstack[pandas]" jupyterlab`, then
|
|
94
|
+
`sweatstack.authenticate()` in the first cell.
|
|
95
|
+
- **BREAKING:** `client.jwt`; use `client.api_key`.
|
|
96
|
+
|
|
97
|
+
<!-- --8<-- [start:upgrading-0-91] -->
|
|
98
|
+
### Upgrading
|
|
99
|
+
|
|
100
|
+
Removed names raise an `AttributeError` naming the replacement, so running your code points at
|
|
101
|
+
each change. The module-level functions moved the same way: `sweatstack.get_activities()` is
|
|
102
|
+
`sweatstack.activities.list()`.
|
|
103
|
+
|
|
104
|
+
| 0.90 | 0.91 |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `get_activities` | `client.activities.list()` |
|
|
107
|
+
| `get_activity` | `client.activities.retrieve(activity_id)` |
|
|
108
|
+
| `get_latest_activity` | `client.activities.latest()` |
|
|
109
|
+
| `get_activity_data` | `client.activities.data(activity_id)` |
|
|
110
|
+
| `get_activity_mean_max` | `client.activities.mean_max(activity_id, metric=...)` |
|
|
111
|
+
| `get_activity_awd` | `client.activities.awd(activity_id)` |
|
|
112
|
+
| `get_latest_activity_data` | `client.activities.data(client.activities.latest().id)` |
|
|
113
|
+
| `get_latest_activity_mean_max` | `client.activities.mean_max(client.activities.latest().id, metric=...)` |
|
|
114
|
+
| `get_longitudinal_data` | `client.activities.longitudinal.data(...)` |
|
|
115
|
+
| `get_longitudinal_mean_max` | `client.activities.longitudinal.mean_max(...)` |
|
|
116
|
+
| `get_longitudinal_awd` | `client.activities.longitudinal.awd(...)` |
|
|
117
|
+
| `upload` | `client.activities.upload(files)` |
|
|
118
|
+
| `get_backfill_status` | `client.activities.backfill_status()` |
|
|
119
|
+
| `watch_backfill_status` | `client.activities.watch_backfill_status()` |
|
|
120
|
+
| `set_activity_app_metadata` | `client.activities.app_metadata.set(activity_id, data=...)` |
|
|
121
|
+
| `delete_activity_app_metadata` | `client.activities.app_metadata.delete(activity_id)` |
|
|
122
|
+
| `get_traces` | `client.traces.list()` |
|
|
123
|
+
| `create_trace` | `client.traces.create(...)` |
|
|
124
|
+
| `update_trace` | `client.traces.replace(trace_id, ...)` |
|
|
125
|
+
| `delete_trace` | `client.traces.delete(trace_id)` |
|
|
126
|
+
| `set_trace_app_metadata` | `client.traces.app_metadata.set(trace_id, data=...)` |
|
|
127
|
+
| `delete_trace_app_metadata` | `client.traces.app_metadata.delete(trace_id)` |
|
|
128
|
+
| `get_tests` | `client.tests.list()` |
|
|
129
|
+
| `get_test` | `client.tests.retrieve(test_id)` |
|
|
130
|
+
| `create_test` | `client.tests.create(...)` |
|
|
131
|
+
| `update_test` | `client.tests.replace(test_id, ...)` |
|
|
132
|
+
| `delete_test` | `client.tests.delete(test_id)` |
|
|
133
|
+
| `set_test_app_metadata` | `client.tests.app_metadata.set(test_id, data=...)` |
|
|
134
|
+
| `delete_test_app_metadata` | `client.tests.app_metadata.delete(test_id)` |
|
|
135
|
+
| `get_dailies` | `client.dailies.list(measure, start=..., end=...)` |
|
|
136
|
+
| `set_daily` | `client.dailies.set(measure, date=..., value=...)` |
|
|
137
|
+
| `delete_daily` | `client.dailies.delete(measure, date=...)` |
|
|
138
|
+
| `get_profile_status` | `client.profile.status()` |
|
|
139
|
+
| `get_sports` | `client.profile.sports()` |
|
|
140
|
+
| `get_tags` | `client.profile.tags()` |
|
|
141
|
+
| `set_user_app_metadata` | `client.profile.app_metadata.set(data=...)` |
|
|
142
|
+
| `delete_user_app_metadata` | `client.profile.app_metadata.delete()` |
|
|
143
|
+
| `get_users` | `client.users.list()` |
|
|
144
|
+
| `get_user` | `client.users.list(name=...)`: every match, so check the length |
|
|
145
|
+
| `create_user` | `client.users.create(first_name=...)` |
|
|
146
|
+
| `get_teams` | `client.teams.list()` |
|
|
147
|
+
| `get_team_users` | `client.teams.users(team_id)` |
|
|
148
|
+
| `get_team_user` | `client.teams.users(team_id, name=...)`: every match |
|
|
149
|
+
| `get_authorized_teams` | `client.teams.authorized()` |
|
|
150
|
+
| `authorize_team` | `client.teams.authorize(team_id)` |
|
|
151
|
+
| `create_portal_session` | `client.portal.sessions.create(destination)` |
|
|
152
|
+
| `get_userinfo` | `client.oauth.userinfo()` |
|
|
153
|
+
| `get_authorization_url` | `client.oauth.authorization_url(...)` |
|
|
154
|
+
| `exchange_code_for_token` | `client.oauth.exchange_code(...)` |
|
|
155
|
+
| `generate_pkce_params` | `client.oauth.generate_pkce_params()` |
|
|
156
|
+
| `switch_user` | `client.delegated_client(user)`: a new client; this one is left unchanged |
|
|
157
|
+
| `switch_back` | keep the original client, or `client.principal_client()` |
|
|
158
|
+
| `client.jwt` | `client.api_key` |
|
|
159
|
+
|
|
160
|
+
Changes that a rename alone doesn't cover. Most raise a `TypeError` naming the argument; the
|
|
161
|
+
ones marked **silent** don't fail loudly, so check for them.
|
|
162
|
+
|
|
163
|
+
| 0.90 | 0.91 |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `sports=["cycling"]` (every filter, and `StreamlitAuth.select_activity`) | `sport="cycling"` or `sport=["cycling", "running"]` |
|
|
166
|
+
| `get_activity_mean_max(id, "power")`, `get_activity_awd(id, "power")` | `activities.mean_max(id, metric="power")`, `activities.awd(id, metric="power")` |
|
|
167
|
+
| `get_activity_data(id, "power", ["power"])` | `activities.data(id, segmentation_on="power", metrics=["power"])` |
|
|
168
|
+
| `create_user("Carla", "Smith")` | `users.create(first_name="Carla", last_name="Smith")` |
|
|
169
|
+
| `get_sports(True)`, `authorize_team(id, scopes)` | `profile.sports(only_root=True)`, `teams.authorize(id, scopes=scopes)` |
|
|
170
|
+
| Positional arguments to `get_authorization_url()` / `exchange_code_for_token()` | Keywords: `oauth.authorization_url(client_id=..., ...)` |
|
|
171
|
+
| `get_latest_activity(start=, end=, tag=)` | `activities.latest(sport=)`; for the rest, `activities.list(start=..., end=..., tags=..., limit=1)` |
|
|
172
|
+
| `get_latest_activity()` raising `StopIteration` when there is none | **Silent:** `activities.latest()` returns `None`; check before using `.id` |
|
|
173
|
+
| `get_longitudinal_*(date=..., window_days=...)` | `start=` and `end=` |
|
|
174
|
+
| `get_longitudinal_*(...)` without `sport` | `sport=` is required (the API already rejected a request without one) |
|
|
175
|
+
| `upload(files, sport=...)` | `activities.upload(files)`: no `sport=`; a CSV needs a `sport` column |
|
|
176
|
+
| `upload(...)` returning a dict | **Silent:** `activities.upload(...)` returns `list[SourceResponse]`; read `.status` / `.error` |
|
|
177
|
+
| `exchange_code_for_token(..., redirect_uri=...)` | `oauth.exchange_code(...)` without `redirect_uri=` |
|
|
178
|
+
| `get_user("Carla")` returning one `UserSummary` | **Silent:** `users.list(name="Carla")` returns a list, every match; `search_mode=` is gone |
|
|
179
|
+
| `switch_user("Carla")` (a name) | `client.delegated_client(users.list(name="Carla")[0])`: a name is not accepted |
|
|
180
|
+
| `client.jwt` | `client.api_key` |
|
|
181
|
+
| `sweatlab`, `sweatshell`, `pip install "sweatstack[jupyter]"` | `uv add "sweatstack[pandas]" jupyterlab`; `sweatstack.authenticate()` in the first cell |
|
|
182
|
+
|
|
183
|
+
New defaults that change behaviour without changing code: `GET`, `PUT` and `DELETE` requests are
|
|
184
|
+
retried (`Client(max_retries=0)` restores single attempts), and requests time out after 60 s
|
|
185
|
+
(`Client(timeout=...)`).
|
|
186
|
+
|
|
187
|
+
**Migration prompt.** Paste this into your coding agent:
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
Upgrade this codebase to sweatstack 0.91, which moved every method to a resource namespace.
|
|
191
|
+
The full upgrade guide, with both tables, is the "Upgrading" section of the 0.91.0 entry in
|
|
192
|
+
https://github.com/SweatStack/sweatstack-python/blob/main/CHANGELOG.md
|
|
193
|
+
(also at https://docs.sweatstack.no/learn/python/upgrading/). Read it first.
|
|
194
|
+
|
|
195
|
+
Find every use of the sweatstack client (a Client instance, the sweatstack module, and
|
|
196
|
+
StreamlitAuth or FastAPI user clients) and:
|
|
197
|
+
|
|
198
|
+
1. Rename calls per the first table, e.g. get_activities() -> activities.list(),
|
|
199
|
+
get_activity_data(id) -> activities.data(id),
|
|
200
|
+
get_longitudinal_mean_max(...) -> activities.longitudinal.mean_max(...),
|
|
201
|
+
update_trace(id, ...) -> traces.replace(id, ...), get_users() -> users.list().
|
|
202
|
+
2. Apply every row of the second table. In particular:
|
|
203
|
+
- sports= -> sport= (one value or a list).
|
|
204
|
+
- Pass by keyword: metric= (mean_max, awd), segmentation_on= and metrics= (data),
|
|
205
|
+
first_name= and last_name= (users.create), only_root= (profile.sports),
|
|
206
|
+
scopes= (teams.authorize), and all arguments of oauth.authorization_url() and
|
|
207
|
+
oauth.exchange_code().
|
|
208
|
+
- Longitudinal methods: sport= is required, and date=/window_days= become start=/end=
|
|
209
|
+
(datetime.date objects).
|
|
210
|
+
- activities.latest() takes only sport= and returns None when there is no activity:
|
|
211
|
+
handle None.
|
|
212
|
+
- activities.upload() takes no sport= (a CSV needs a sport column) and returns
|
|
213
|
+
list[SourceResponse], not a dict.
|
|
214
|
+
- oauth.exchange_code() takes no redirect_uri=.
|
|
215
|
+
- get_user(x) -> users.list(name=x), which returns a list of every match.
|
|
216
|
+
- switch_user(user) -> athlete = client.delegated_client(user_id_or_summary); use that
|
|
217
|
+
new client for the athlete's calls. It doesn't accept a name: look the user up first.
|
|
218
|
+
switch_back() -> keep using the original client.
|
|
219
|
+
- client.jwt -> client.api_key.
|
|
220
|
+
3. Replace get_latest_activity_data(...) with activities.data(activities.latest().id, ...),
|
|
221
|
+
after checking that latest() returned an activity.
|
|
222
|
+
4. If the project used sweatlab, sweatshell or the [jupyter] extra, depend on
|
|
223
|
+
"sweatstack[pandas]" (or [polars]) and jupyterlab instead.
|
|
224
|
+
|
|
225
|
+
Then run the code and the tests. Fix any AttributeError (its message names the replacement)
|
|
226
|
+
and TypeError (a renamed or keyword-only argument). Do not add compatibility shims.
|
|
227
|
+
```
|
|
228
|
+
<!-- --8<-- [end:upgrading-0-91] -->
|
|
229
|
+
|
|
230
|
+
## [0.90.0] - 2026-09-28
|
|
231
|
+
|
|
232
|
+
### Changed
|
|
233
|
+
|
|
234
|
+
- **BREAKING: mean-max curves are one row per duration**.
|
|
235
|
+
`get_activity_mean_max`, `get_latest_activity_mean_max` and `get_longitudinal_mean_max` return
|
|
236
|
+
`duration`, the metric (W or m/s) and `start` (UTC timestamp of the best effort), plus
|
|
237
|
+
`activity_id`, `sport` and `after` on the longitudinal curve. By default 19 durations from 1 s to
|
|
238
|
+
6 h; the curve can rise again at longer durations and is returned as it is.
|
|
239
|
+
|
|
240
|
+
### Added
|
|
241
|
+
|
|
242
|
+
- `durations=` on all three mean-max methods: `None` for the 19 defaults, `"all"` for the full grid,
|
|
243
|
+
or a list of seconds. Keyword-only.
|
|
244
|
+
|
|
245
|
+
### Removed
|
|
246
|
+
|
|
247
|
+
- **BREAKING:** `segmentation` on `get_activity_mean_max` and `get_latest_activity_mean_max` (it never
|
|
248
|
+
reduced the payload; the server ignores it). It was positional: passing `True` in that slot now
|
|
249
|
+
raises `TypeError`.
|
|
250
|
+
- **BREAKING:** `by` on `get_longitudinal_mean_max`, and its `DeprecationWarning`. Every curve is
|
|
251
|
+
duration-oriented.
|
|
252
|
+
|
|
9
253
|
## [0.89.0] - 2026-09-28
|
|
10
254
|
|
|
11
255
|
Frames on your terms. Every method that returns a collection takes `output=`:
|
|
@@ -89,7 +333,7 @@ Four breaking changes come with it; the upgrade is mechanical, see **Upgrading**
|
|
|
89
333
|
|
|
90
334
|
- **Breaking:** `create_trace`, `update_trace`, `create_test`, and `update_test` now require timezone-aware datetimes for `timestamp`, `start`, and `end`. A naive datetime raises `ValueError` before the request is sent. Attach a zone, e.g. `datetime(..., tzinfo=ZoneInfo("Europe/Amsterdam"))` or `datetime.now(timezone.utc)`; the offset is stored alongside the instant.
|
|
91
335
|
- `start`, `end`, and `timestamp` on responses are absolute UTC instants (ISO 8601 with a `Z` suffix), no longer a fixed per-record local offset. For wall-clock display use the companion `start_local` / `end_local` / `timestamp_local` fields, which are always present. The Streamlit activity selector now labels activities by their local date.
|
|
92
|
-
- Requires a SweatStack server with offset-based timezone handling
|
|
336
|
+
- Requires a SweatStack server with offset-based timezone handling. Against an older server the aware-datetime writes still work, but responses keep the previous fixed-offset `start`/`end`/`timestamp`.
|
|
93
337
|
|
|
94
338
|
### Removed
|
|
95
339
|
|