sweatstack 0.76.2__tar.gz → 0.77.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.76.2 → sweatstack-0.77.0}/CHANGELOG.md +10 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/CONTRIBUTING.md +1 -1
- {sweatstack-0.76.2 → sweatstack-0.77.0}/PKG-INFO +3 -3
- {sweatstack-0.76.2 → sweatstack-0.77.0}/README.md +1 -1
- sweatstack-0.77.0/plans/003_trace_test_linking.md +300 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/pyproject.toml +2 -2
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/client.py +43 -5
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/openapi_schemas.py +28 -22
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/schemas.py +1 -1
- {sweatstack-0.76.2 → sweatstack-0.77.0}/tests/test_tests.py +4 -4
- sweatstack-0.77.0/tests/test_trace_test_linking.py +90 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/uv.lock +1 -1
- {sweatstack-0.76.2 → sweatstack-0.77.0}/.claude/settings.local.json +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/.claude/skills/sweatstack-python/client.md +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/.gitignore +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/.python-version +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/LICENSE +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/Makefile +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/docs/conf.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/docs/everything.rst +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/docs/index.rst +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/examples/send_webhook.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/plans/001a_tests.md +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/plans/001b_metadata.md +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/plans/001c_dailies.md +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/exceptions.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/fastapi/dependencies.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/fastapi/routes.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/tests/__init__.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/tests/test_dailies.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/tests/test_exceptions.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/tests/test_metadata.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/tests/test_teams.py +0 -0
- {sweatstack-0.76.2 → sweatstack-0.77.0}/tests/test_webhooks.py +0 -0
|
@@ -6,6 +6,16 @@ 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.77.0] - 2026-05-19
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Link a trace to a test via the new `test_id` argument on `create_trace()` and `update_trace()`. A linked trace appears in the test's traces regardless of its timestamp.
|
|
13
|
+
- `get_test()` accepts `trace_resolution=TraceResolution.linked` to return only traces explicitly linked to the test. Defaults to `TraceResolution.auto` (unchanged behaviour).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `update_trace()` replaces all fields, including `test_id`. Callers that omit `test_id` will clear any existing link — pass it back in to keep the trace linked.
|
|
17
|
+
|
|
18
|
+
|
|
9
19
|
## [0.76.2] - 2026-04-27
|
|
10
20
|
|
|
11
21
|
### Fixed
|
|
@@ -6,4 +6,4 @@ This repository is open-sourced primarily to make it easier for developers and A
|
|
|
6
6
|
|
|
7
7
|
If you've found a bug or have a question about using the library, feel free to [open an issue](https://github.com/SweatStack/sweatstack-python/issues).
|
|
8
8
|
|
|
9
|
-
For general questions about SweatStack, please refer to the [developer documentation](https://
|
|
9
|
+
For general questions about SweatStack, please refer to the [developer documentation](https://docs.sweatstack.no/getting-started/).
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sweatstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.77.0
|
|
4
4
|
Summary: The official Python client for SweatStack
|
|
5
5
|
Project-URL: Homepage, https://sweatstack.no
|
|
6
|
-
Project-URL: Documentation, https://
|
|
6
|
+
Project-URL: Documentation, https://docs.sweatstack.no/getting-started/
|
|
7
7
|
Project-URL: Repository, https://github.com/SweatStack/sweatstack-python
|
|
8
8
|
Project-URL: Issues, https://github.com/SweatStack/sweatstack-python/issues
|
|
9
9
|
Project-URL: Changelog, https://github.com/SweatStack/sweatstack-python/blob/main/CHANGELOG.md
|
|
@@ -43,4 +43,4 @@ Description-Content-Type: text/markdown
|
|
|
43
43
|
|
|
44
44
|
This is the official Python client library for SweatStack.
|
|
45
45
|
|
|
46
|
-
Documentation can be found [here](https://
|
|
46
|
+
Documentation can be found [here](https://docs.sweatstack.no/getting-started/).
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Plan: Trace ↔ Test linking + `TraceResolution`
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Mirror upstream `sweatstack@93cd588` ("Adds trace to test optional foreign key
|
|
6
|
+
relationship") in the Python client:
|
|
7
|
+
|
|
8
|
+
- Traces gain an optional `test_id` FK that *explicitly* links them to a test
|
|
9
|
+
(independent of timestamp).
|
|
10
|
+
- `GET /api/v1/tests/{test_id}` gains a `traces` query parameter
|
|
11
|
+
(`TraceResolution` enum: `auto` | `linked`) that controls how the returned
|
|
12
|
+
`traces` list is resolved.
|
|
13
|
+
- `POST /api/v1/traces/` and `PUT /api/v1/traces/{trace_id}` may now return
|
|
14
|
+
`404` when a referenced `test_id` does not exist.
|
|
15
|
+
|
|
16
|
+
Server-side behaviour:
|
|
17
|
+
|
|
18
|
+
- `auto` (default): traces whose timestamp falls in the test's window, **plus**
|
|
19
|
+
traces explicitly linked to this test, **minus** traces explicitly linked to a
|
|
20
|
+
*different* test.
|
|
21
|
+
- `linked`: only traces explicitly linked via `test_id`; the time window is
|
|
22
|
+
ignored.
|
|
23
|
+
|
|
24
|
+
The `activities` list is unaffected — always time-overlap matched.
|
|
25
|
+
|
|
26
|
+
## Code Quality Requirements
|
|
27
|
+
|
|
28
|
+
Same bar as plan 001a:
|
|
29
|
+
|
|
30
|
+
- Follow existing patterns exactly (keyword-only params, enum handling,
|
|
31
|
+
`_enums_to_strings`, error surface via `_raise_for_status`, docstring style).
|
|
32
|
+
- Comprehensive type hints. No `Any`. No string-typed enums on the surface.
|
|
33
|
+
- Concise Google-style docstrings (Args / Returns / Raises).
|
|
34
|
+
- No dead code, no TODOs, no half-finished branches.
|
|
35
|
+
|
|
36
|
+
## Step 1: Regenerate `openapi_schemas.py`
|
|
37
|
+
|
|
38
|
+
The **user will run the SweatStack server locally** at
|
|
39
|
+
`http://localhost:8080`. Wait for confirmation that the server is up
|
|
40
|
+
before running the regen, and do not attempt to start the server.
|
|
41
|
+
|
|
42
|
+
Then:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv run generate-response-models
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Expected diff in `src/sweatstack/openapi_schemas.py`:
|
|
49
|
+
|
|
50
|
+
- `TraceDetails`: new `test_id: str | None = Field(None, title='Test Id')`.
|
|
51
|
+
- `TraceCreateOrUpdate`: new `test_id: str | None = Field(None, title='Test Id')`.
|
|
52
|
+
- New top-level enum:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
class TraceResolution(Enum):
|
|
56
|
+
auto = 'auto'
|
|
57
|
+
linked = 'linked'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Caveat — regen pulls more than this feature.** Two intervening upstream
|
|
61
|
+
commits since `openapi_schemas.py` was last regenerated also touch the
|
|
62
|
+
schema/API surface: `feebe56` ("Adds security.txt …") and `3f29c1f`
|
|
63
|
+
("Better handling of tags=null"). The regenerated diff will not be
|
|
64
|
+
confined to this plan's scope.
|
|
65
|
+
|
|
66
|
+
Action: review the full diff. If unrelated changes appear, **split them
|
|
67
|
+
into a separate commit** ahead of the trace-linking commit so the
|
|
68
|
+
trace-linking commit stays reviewable. Do not merge regen-noise as part
|
|
69
|
+
of this feature.
|
|
70
|
+
|
|
71
|
+
Also verify on the running server before committing: open
|
|
72
|
+
`http://localhost:8080/openapi.json`, search for `TraceResolution`,
|
|
73
|
+
confirm wire values are `"auto"` and `"linked"` (lowercase). This guards
|
|
74
|
+
against an upstream `BaseEnum` casing change.
|
|
75
|
+
|
|
76
|
+
## Step 2: Re-export `TraceResolution`
|
|
77
|
+
|
|
78
|
+
Two-step exposure (mirrors `Sport`):
|
|
79
|
+
|
|
80
|
+
1. `src/sweatstack/schemas.py` — add `TraceResolution` to the import from
|
|
81
|
+
`openapi_schemas`.
|
|
82
|
+
2. `src/sweatstack/client.py` — add `TraceResolution` to the import block
|
|
83
|
+
pulling enums/schemas from `.schemas` (≈ line 45, where `Sport`,
|
|
84
|
+
`TraceDetails`, etc. are imported).
|
|
85
|
+
|
|
86
|
+
`__init__.py` is `from .client import *`, so step 2 is what actually makes
|
|
87
|
+
`from sweatstack import TraceResolution` work. Verify with a smoke test
|
|
88
|
+
in the REPL after the change. (This is the step that's easy to skip and
|
|
89
|
+
breaks the public surface.)
|
|
90
|
+
|
|
91
|
+
## Step 3: Update `src/sweatstack/client.py`
|
|
92
|
+
|
|
93
|
+
Import `TraceResolution` from `schemas` in the existing imports block
|
|
94
|
+
(≈ line 45, alongside `TraceDetails`).
|
|
95
|
+
|
|
96
|
+
### 3a. `create_trace` — add `test_id` keyword arg
|
|
97
|
+
|
|
98
|
+
Location: ≈ line 1708.
|
|
99
|
+
|
|
100
|
+
- Add `test_id: str | None = None` as the **last** keyword-only param
|
|
101
|
+
(preserves existing call-site compatibility — every existing caller uses
|
|
102
|
+
keyword args, but appending keeps the visual ordering stable).
|
|
103
|
+
- Add `"test_id": test_id` to the JSON body. Do not omit when `None`; the
|
|
104
|
+
server accepts `null` and other optional fields are already passed as
|
|
105
|
+
`None` (consistent with `lactate`, `rpe`, etc.).
|
|
106
|
+
- Docstring:
|
|
107
|
+
- Add `test_id: Optional ID of a test to explicitly link this trace to.
|
|
108
|
+
If the test does not exist, the API raises 404.` to Args.
|
|
109
|
+
- Add a `Raises:` note that an `HTTPStatusError(404)` is raised when
|
|
110
|
+
`test_id` references a non-existent test (existing `_raise_for_status`
|
|
111
|
+
already surfaces this; just document it).
|
|
112
|
+
|
|
113
|
+
### 3b. `update_trace` — add `test_id` keyword arg
|
|
114
|
+
|
|
115
|
+
Location: ≈ line 1762.
|
|
116
|
+
|
|
117
|
+
Identical treatment to `create_trace`. Same docstring additions.
|
|
118
|
+
|
|
119
|
+
**Behavioural-change callout (required in both docstring and CHANGELOG):**
|
|
120
|
+
`update_trace` is full-replace. Once `test_id` is in the signature with a
|
|
121
|
+
default of `None`, **existing callers that don't pass `test_id` will silently
|
|
122
|
+
unlink any previously linked test** the next time they call
|
|
123
|
+
`update_trace`. This is consistent with how every other optional field on
|
|
124
|
+
`update_trace` already behaves (e.g. `lactate=None` nulls lactate), but
|
|
125
|
+
it's a real, observable change for upgraders.
|
|
126
|
+
|
|
127
|
+
Docstring wording suggestion:
|
|
128
|
+
|
|
129
|
+
> Note: This is a full-replace operation. If a trace was previously linked
|
|
130
|
+
> to a test via `test_id`, you must pass that `test_id` again to preserve
|
|
131
|
+
> the link — otherwise it is cleared.
|
|
132
|
+
|
|
133
|
+
Do not try to work around this with "preserve if omitted" sentinels — that
|
|
134
|
+
would diverge from how every other field on this method behaves and rot
|
|
135
|
+
the contract.
|
|
136
|
+
|
|
137
|
+
### 3c. `get_test` — add `trace_resolution` keyword arg
|
|
138
|
+
|
|
139
|
+
Location: ≈ line 1934.
|
|
140
|
+
|
|
141
|
+
Current signature:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
def get_test(self, test_id: str) -> TestDetails:
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
New signature:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
def get_test(
|
|
151
|
+
self,
|
|
152
|
+
test_id: str,
|
|
153
|
+
*,
|
|
154
|
+
trace_resolution: TraceResolution = TraceResolution.auto,
|
|
155
|
+
) -> TestDetails:
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Naming decision: **`trace_resolution`, not `traces`.** The wire name is
|
|
159
|
+
`traces=` (server query param), but on the Python method
|
|
160
|
+
`client.get_test("t1", traces=TraceResolution.linked)` reads as "give me
|
|
161
|
+
the test and traces=X" — ambiguous. `trace_resolution=` is
|
|
162
|
+
self-documenting. Note the wire mapping in the docstring so future
|
|
163
|
+
maintainers know they don't match.
|
|
164
|
+
|
|
165
|
+
Typing decision: **strict `TraceResolution`, not `TraceResolution | str`.**
|
|
166
|
+
This diverges from the `Sport | str` pattern used elsewhere in the
|
|
167
|
+
client. The deviation is deliberate: the enum has exactly two values, it
|
|
168
|
+
is a brand-new surface (no historical string callers to support), and
|
|
169
|
+
the brief asks for "extremely clean". Document the deviation in a code
|
|
170
|
+
review note when the PR goes up; don't surprise the reviewer.
|
|
171
|
+
|
|
172
|
+
Implementation:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
with self._http_client() as client:
|
|
176
|
+
params = {}
|
|
177
|
+
if trace_resolution is not TraceResolution.auto:
|
|
178
|
+
params["traces"] = trace_resolution.value
|
|
179
|
+
response = client.get(
|
|
180
|
+
url=f"/api/v1/tests/{test_id}",
|
|
181
|
+
params=params,
|
|
182
|
+
)
|
|
183
|
+
self._raise_for_status(response)
|
|
184
|
+
return TestDetails.model_validate(response.json())
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Notes:
|
|
188
|
+
|
|
189
|
+
- Use `.value` (lowercase `"auto"` / `"linked"`) explicitly. `httpx` does
|
|
190
|
+
not unwrap `Enum.value` on its own — it `str()`s the value, which gives
|
|
191
|
+
`"TraceResolution.linked"`. Verified pattern: `_get_tests_generator` and
|
|
192
|
+
every other enum query usage in this file calls `.value`.
|
|
193
|
+
- **Omit the param when at default** — matches the convention used by
|
|
194
|
+
`_get_activities_generator`, `_get_tests_generator`, etc., which only
|
|
195
|
+
add optional query params when the caller supplied a non-default.
|
|
196
|
+
Sending `?traces=auto` would deviate from this pattern.
|
|
197
|
+
- Keep `test_id` positional (existing call sites pass it positionally);
|
|
198
|
+
only `trace_resolution` is keyword-only. Strictly additive.
|
|
199
|
+
- Docstring: explain `auto` vs `linked` in user terms. Copy the
|
|
200
|
+
server-side wording verbatim from `app/routers/api.py` — it is the
|
|
201
|
+
canonical description and survives doc drift.
|
|
202
|
+
|
|
203
|
+
### 3d. No other method changes
|
|
204
|
+
|
|
205
|
+
- `get_traces()` is unchanged. Upstream does not yet expose a `test_id`
|
|
206
|
+
filter on the list endpoint; do not add a client-side filter
|
|
207
|
+
(out of scope, would lie about API support).
|
|
208
|
+
- `_get_traces_generator()` is unchanged. The new `test_id` field rides
|
|
209
|
+
along automatically in `TraceDetails`.
|
|
210
|
+
|
|
211
|
+
## Step 4: Tests
|
|
212
|
+
|
|
213
|
+
Add `tests/test_trace_test_linking.py`. Follow the no-network pattern
|
|
214
|
+
established by the other tests in this repo.
|
|
215
|
+
|
|
216
|
+
Coverage:
|
|
217
|
+
|
|
218
|
+
- **Schema round-trip**: build a `TraceDetails` with `test_id="t_123"`,
|
|
219
|
+
`.model_dump()` → `.model_validate()` round-trip preserves it.
|
|
220
|
+
- **TraceCreateOrUpdate accepts `test_id`**: `TraceCreateOrUpdate(timestamp=...,
|
|
221
|
+
test_id="t_123")` validates; `.model_dump()` includes the field.
|
|
222
|
+
- **`TraceResolution` enum values**: assert `TraceResolution.auto.value ==
|
|
223
|
+
"auto"` and `TraceResolution.linked.value == "linked"` (guards against the
|
|
224
|
+
generator regenerating the enum with different casing on the wire).
|
|
225
|
+
- **Re-export**: `from sweatstack import TraceResolution` resolves, *and*
|
|
226
|
+
`import sweatstack; sweatstack.TraceResolution` resolves (catches the
|
|
227
|
+
client-level import step easy to skip).
|
|
228
|
+
|
|
229
|
+
If the repo already has an httpx-mock based test harness (it currently does
|
|
230
|
+
not, judging by the existing files), add a single integration-style test for
|
|
231
|
+
`get_test(test_id, traces=TraceResolution.linked)` that asserts the request
|
|
232
|
+
URL carries `?traces=linked`. If not, skip — the enum-value test above
|
|
233
|
+
provides the same guarantee without inventing a test infrastructure for this
|
|
234
|
+
plan alone.
|
|
235
|
+
|
|
236
|
+
## Step 5: Examples / docs
|
|
237
|
+
|
|
238
|
+
- Quick scan of `examples/` — if there is an existing "tests" example, append
|
|
239
|
+
a short snippet showing:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
test = client.create_test(sport=Sport.cycling, start=...)
|
|
243
|
+
client.create_trace(timestamp=..., lactate=4.0, test_id=test.id)
|
|
244
|
+
detail = client.get_test(test.id, traces=TraceResolution.linked)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
If none exists, do **not** add a new example file for this alone.
|
|
248
|
+
|
|
249
|
+
- `docs/everything.rst` — `TraceDetails` and `TraceCreateOrUpdate` autoclass
|
|
250
|
+
entries pick up the new field automatically. Add a `.. autoclass::
|
|
251
|
+
sweatstack.openapi_schemas.TraceResolution` entry in the same enums block
|
|
252
|
+
where `Sport` lives (if `Sport` is documented there — check before adding).
|
|
253
|
+
|
|
254
|
+
- `.claude/skills/sweatstack-python/client.md` — update the `create_trace`,
|
|
255
|
+
`update_trace`, and `get_test` signatures, and add `TraceResolution` to
|
|
256
|
+
the list of exported enums.
|
|
257
|
+
|
|
258
|
+
## Step 6: Changelog + version bump
|
|
259
|
+
|
|
260
|
+
- `CHANGELOG.md`: new entry under the next minor version. Must include
|
|
261
|
+
both the additive feature and the behavioural-change callout.
|
|
262
|
+
Wording suggestion:
|
|
263
|
+
|
|
264
|
+
> **Added** — Traces can be explicitly linked to a test via the new
|
|
265
|
+
> `test_id` parameter on `create_trace` and `update_trace`. `get_test`
|
|
266
|
+
> accepts a `trace_resolution` parameter (`TraceResolution.auto` or
|
|
267
|
+
> `TraceResolution.linked`) controlling how the returned traces list is
|
|
268
|
+
> resolved.
|
|
269
|
+
>
|
|
270
|
+
> **Behaviour change** — `update_trace` is full-replace. Callers that
|
|
271
|
+
> previously linked a trace to a test (e.g. via the SweatStack UI or
|
|
272
|
+
> another client) and then call `update_trace` without passing
|
|
273
|
+
> `test_id` will now unlink that trace. Pass the existing `test_id`
|
|
274
|
+
> back in to preserve the link.
|
|
275
|
+
|
|
276
|
+
- `pyproject.toml`: bump `0.76.2 → 0.77.0` (minor — additive feature, no
|
|
277
|
+
breaking change). Confirm the version cadence with the maintainer if
|
|
278
|
+
unsure; this is the only judgement call in the plan.
|
|
279
|
+
|
|
280
|
+
## API Reference
|
|
281
|
+
|
|
282
|
+
| Method | Path | New parameter | Notes |
|
|
283
|
+
|--------|------|---------------|-------|
|
|
284
|
+
| POST | `/api/v1/traces/` | body `test_id?: str` | 404 if test missing |
|
|
285
|
+
| PUT | `/api/v1/traces/{id}` | body `test_id?: str` | full-replace; `null` unlinks; 404 if test missing |
|
|
286
|
+
| GET | `/api/v1/tests/{id}` | query `traces=auto\|linked` | default `auto` |
|
|
287
|
+
|
|
288
|
+
### Resolution semantics (server-side, reference only)
|
|
289
|
+
|
|
290
|
+
- `auto`: `(traces in window AND not claimed by another test) ∪ (traces with test_id == this test)`
|
|
291
|
+
- `linked`: `traces with test_id == this test` (window ignored)
|
|
292
|
+
|
|
293
|
+
## Out of Scope
|
|
294
|
+
|
|
295
|
+
- Listing/filtering traces by `test_id` on `GET /traces/` (upstream does not
|
|
296
|
+
expose this).
|
|
297
|
+
- Bulk attach/detach helpers (`attach_traces_to_test(...)`). Not requested by
|
|
298
|
+
upstream; would be premature abstraction.
|
|
299
|
+
- A `linked_only: bool` convenience on `get_test`. Rejected in favour of the
|
|
300
|
+
enum to keep a single, faithful surface.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sweatstack"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.77.0"
|
|
4
4
|
description = "The official Python client for SweatStack"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -32,7 +32,7 @@ dependencies = [
|
|
|
32
32
|
|
|
33
33
|
[project.urls]
|
|
34
34
|
Homepage = "https://sweatstack.no"
|
|
35
|
-
Documentation = "https://
|
|
35
|
+
Documentation = "https://docs.sweatstack.no/getting-started/"
|
|
36
36
|
Repository = "https://github.com/SweatStack/sweatstack-python"
|
|
37
37
|
Issues = "https://github.com/SweatStack/sweatstack-python/issues"
|
|
38
38
|
Changelog = "https://github.com/SweatStack/sweatstack-python/blob/main/CHANGELOG.md"
|
|
@@ -43,7 +43,7 @@ from .schemas import (
|
|
|
43
43
|
BackfillStatus, DailyMeasure, DailyResponse,
|
|
44
44
|
Marker, Metric, Scope, Sport,
|
|
45
45
|
TeamResponse, TestDetails, TestResults, TestSummary, TokenResponse, TraceDetails,
|
|
46
|
-
UserInfoResponse, UserResponse, UserSummary
|
|
46
|
+
TraceResolution, UserInfoResponse, UserResponse, UserSummary
|
|
47
47
|
)
|
|
48
48
|
from .utils import convert_to_standard_dtypes, decode_jwt_body, make_dataframe_streamlit_compatible
|
|
49
49
|
|
|
@@ -1717,6 +1717,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1717
1717
|
heart_rate: int | None = None,
|
|
1718
1718
|
tags: list[str] | None = None,
|
|
1719
1719
|
sport: Sport | str | None = None,
|
|
1720
|
+
test_id: str | None = None,
|
|
1720
1721
|
) -> TraceDetails:
|
|
1721
1722
|
"""Creates a new trace with the specified parameters.
|
|
1722
1723
|
|
|
@@ -1733,12 +1734,18 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1733
1734
|
heart_rate: Optional heart rate measurement in beats per minute.
|
|
1734
1735
|
tags: Optional list of tags to associate with this trace.
|
|
1735
1736
|
sport: Optional sport to associate with this trace.
|
|
1737
|
+
test_id: Optional ID of a test to explicitly link this trace to.
|
|
1738
|
+
The link is independent of timestamp — a linked trace appears
|
|
1739
|
+
in the test's traces list regardless of whether its timestamp
|
|
1740
|
+
falls inside the test window.
|
|
1736
1741
|
|
|
1737
1742
|
Returns:
|
|
1738
1743
|
TraceDetails: The created trace object with all details.
|
|
1739
1744
|
|
|
1740
1745
|
Raises:
|
|
1741
|
-
|
|
1746
|
+
SweatStackNotFoundError: If ``test_id`` references a test that
|
|
1747
|
+
does not exist.
|
|
1748
|
+
SweatStackAPIError: If the API request fails for any other reason.
|
|
1742
1749
|
"""
|
|
1743
1750
|
sport = self._enums_to_strings([sport])[0] if sport else None
|
|
1744
1751
|
with self._http_client() as client:
|
|
@@ -1754,6 +1761,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1754
1761
|
"heart_rate": heart_rate,
|
|
1755
1762
|
"tags": tags,
|
|
1756
1763
|
"sport": sport,
|
|
1764
|
+
"test_id": test_id,
|
|
1757
1765
|
},
|
|
1758
1766
|
)
|
|
1759
1767
|
self._raise_for_status(response)
|
|
@@ -1772,6 +1780,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1772
1780
|
heart_rate: int | None = None,
|
|
1773
1781
|
tags: list[str] | None = None,
|
|
1774
1782
|
sport: Sport | str | None = None,
|
|
1783
|
+
test_id: str | None = None,
|
|
1775
1784
|
) -> None:
|
|
1776
1785
|
"""Updates a trace by replacing all fields.
|
|
1777
1786
|
|
|
@@ -1779,6 +1788,10 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1779
1788
|
server-side. To modify a single field, first fetch the trace with
|
|
1780
1789
|
``get_traces()``, then pass all fields back.
|
|
1781
1790
|
|
|
1791
|
+
In particular: if the trace was previously linked to a test via
|
|
1792
|
+
``test_id`` and you do not pass ``test_id`` here, the link is cleared.
|
|
1793
|
+
Pass the existing ``test_id`` back in to preserve it.
|
|
1794
|
+
|
|
1782
1795
|
Args:
|
|
1783
1796
|
trace_id: The unique identifier of the trace to update.
|
|
1784
1797
|
timestamp: The date and time when the trace was recorded.
|
|
@@ -1790,9 +1803,13 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1790
1803
|
heart_rate: Optional heart rate measurement in beats per minute.
|
|
1791
1804
|
tags: Optional list of tags to associate with this trace.
|
|
1792
1805
|
sport: Optional sport to associate with this trace.
|
|
1806
|
+
test_id: Optional ID of a test to explicitly link this trace to.
|
|
1807
|
+
Pass ``None`` (or omit) to leave the trace unlinked.
|
|
1793
1808
|
|
|
1794
1809
|
Raises:
|
|
1795
|
-
|
|
1810
|
+
SweatStackNotFoundError: If ``trace_id`` does not exist, or if
|
|
1811
|
+
``test_id`` references a test that does not exist.
|
|
1812
|
+
SweatStackAPIError: If the API request fails for any other reason.
|
|
1796
1813
|
"""
|
|
1797
1814
|
sport = self._enums_to_strings([sport])[0] if sport else None
|
|
1798
1815
|
with self._http_client() as client:
|
|
@@ -1808,6 +1825,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1808
1825
|
"heart_rate": heart_rate,
|
|
1809
1826
|
"tags": tags,
|
|
1810
1827
|
"sport": sport,
|
|
1828
|
+
"test_id": test_id,
|
|
1811
1829
|
},
|
|
1812
1830
|
)
|
|
1813
1831
|
self._raise_for_status(response)
|
|
@@ -1931,11 +1949,29 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1931
1949
|
else:
|
|
1932
1950
|
return tests
|
|
1933
1951
|
|
|
1934
|
-
def get_test(
|
|
1952
|
+
def get_test(
|
|
1953
|
+
self,
|
|
1954
|
+
test_id: str,
|
|
1955
|
+
*,
|
|
1956
|
+
trace_resolution: TraceResolution | str = TraceResolution.auto,
|
|
1957
|
+
) -> TestDetails:
|
|
1935
1958
|
"""Gets details for a specific test by ID.
|
|
1936
1959
|
|
|
1937
1960
|
Args:
|
|
1938
1961
|
test_id: The unique identifier of the test to retrieve.
|
|
1962
|
+
trace_resolution: How traces are matched to this test. Affects only
|
|
1963
|
+
the ``traces`` list on the response; ``activities`` is always
|
|
1964
|
+
time-overlap matched. Accepts a ``TraceResolution`` enum or
|
|
1965
|
+
its string value (``"auto"`` or ``"linked"``).
|
|
1966
|
+
|
|
1967
|
+
- ``"auto"`` (default): traces whose timestamp falls in the
|
|
1968
|
+
test's time range, plus any traces explicitly linked to
|
|
1969
|
+
this test, minus any traces explicitly linked to a
|
|
1970
|
+
different test.
|
|
1971
|
+
- ``"linked"``: only traces explicitly linked to this test
|
|
1972
|
+
via ``test_id``, regardless of timestamp.
|
|
1973
|
+
|
|
1974
|
+
Maps to the ``traces`` query parameter on the wire.
|
|
1939
1975
|
|
|
1940
1976
|
Returns:
|
|
1941
1977
|
TestDetails: The test details including resolved traces and overlapping activities.
|
|
@@ -1943,8 +1979,10 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1943
1979
|
Raises:
|
|
1944
1980
|
HTTPStatusError: If the API request fails.
|
|
1945
1981
|
"""
|
|
1982
|
+
resolution = self._enums_to_strings([trace_resolution])[0]
|
|
1983
|
+
params = {} if resolution == TraceResolution.auto.value else {"traces": resolution}
|
|
1946
1984
|
with self._http_client() as client:
|
|
1947
|
-
response = client.get(url=f"/api/v1/tests/{test_id}")
|
|
1985
|
+
response = client.get(url=f"/api/v1/tests/{test_id}", params=params)
|
|
1948
1986
|
self._raise_for_status(response)
|
|
1949
1987
|
return TestDetails.model_validate(response.json())
|
|
1950
1988
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# generated by datamodel-codegen:
|
|
2
2
|
# filename: openapi.json
|
|
3
|
-
# timestamp: 2026-
|
|
3
|
+
# timestamp: 2026-05-19T10:00:53+00:00
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -13,7 +13,6 @@ from pydantic import (
|
|
|
13
13
|
AnyUrl,
|
|
14
14
|
AwareDatetime,
|
|
15
15
|
BaseModel,
|
|
16
|
-
NaiveDatetime,
|
|
17
16
|
EmailStr,
|
|
18
17
|
Field,
|
|
19
18
|
RootModel,
|
|
@@ -26,7 +25,7 @@ from pydantic import (
|
|
|
26
25
|
|
|
27
26
|
class Activity(BaseModel):
|
|
28
27
|
id: str = Field(..., title='Id')
|
|
29
|
-
start_date_local:
|
|
28
|
+
start_date_local: AwareDatetime | None = Field(None, title='Start Date Local')
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
class ActivityUpdate(BaseModel):
|
|
@@ -1116,8 +1115,8 @@ class TestSummary(BaseModel):
|
|
|
1116
1115
|
end: AwareDatetime | None = Field(None, title='End')
|
|
1117
1116
|
results: TestResults | None = None
|
|
1118
1117
|
app_metadata: dict[str, Any] | None = Field(None, title='App Metadata')
|
|
1119
|
-
start_local:
|
|
1120
|
-
end_local:
|
|
1118
|
+
start_local: AwareDatetime = Field(..., title='Start Local')
|
|
1119
|
+
end_local: AwareDatetime | None = Field(..., title='End Local')
|
|
1121
1120
|
|
|
1122
1121
|
|
|
1123
1122
|
class TestUpdate(BaseModel):
|
|
@@ -1150,6 +1149,7 @@ class TokenResponse(BaseModel):
|
|
|
1150
1149
|
|
|
1151
1150
|
class TraceCreateOrUpdate(BaseModel):
|
|
1152
1151
|
timestamp: AwareDatetime = Field(..., title='Timestamp')
|
|
1152
|
+
test_id: str | None = Field(None, title='Test Id')
|
|
1153
1153
|
lactate: float | None = Field(None, title='Lactate')
|
|
1154
1154
|
rpe: int | None = Field(None, title='Rpe')
|
|
1155
1155
|
notes: str | None = Field(None, title='Notes')
|
|
@@ -1161,6 +1161,11 @@ class TraceCreateOrUpdate(BaseModel):
|
|
|
1161
1161
|
sport: Sport | None = None
|
|
1162
1162
|
|
|
1163
1163
|
|
|
1164
|
+
class TraceResolution(Enum):
|
|
1165
|
+
auto = 'auto'
|
|
1166
|
+
linked = 'linked'
|
|
1167
|
+
|
|
1168
|
+
|
|
1164
1169
|
class UserFlow(Enum):
|
|
1165
1170
|
session = 'session'
|
|
1166
1171
|
signup = 'signup'
|
|
@@ -1172,7 +1177,7 @@ class UserInfoResponse(BaseModel):
|
|
|
1172
1177
|
given_name: str | None = Field(None, title='Given Name')
|
|
1173
1178
|
family_name: str | None = Field(None, title='Family Name')
|
|
1174
1179
|
email: str | None = Field(None, title='Email')
|
|
1175
|
-
registered_at: AwareDatetime
|
|
1180
|
+
registered_at: AwareDatetime = Field(..., title='Registered At')
|
|
1176
1181
|
name: str = Field(..., title='Name')
|
|
1177
1182
|
|
|
1178
1183
|
|
|
@@ -1181,7 +1186,7 @@ class UserResponse(BaseModel):
|
|
|
1181
1186
|
first_name: str | None = Field(..., title='First Name')
|
|
1182
1187
|
last_name: str | None = Field(..., title='Last Name')
|
|
1183
1188
|
admin: bool = Field(..., title='Admin')
|
|
1184
|
-
registered_at: AwareDatetime
|
|
1189
|
+
registered_at: AwareDatetime = Field(..., title='Registered At')
|
|
1185
1190
|
display_name: str = Field(..., title='Display Name')
|
|
1186
1191
|
is_managed: bool = Field(..., title='Is Managed')
|
|
1187
1192
|
|
|
@@ -1294,6 +1299,12 @@ class BodyAuthorizeOauthAuthorizePost(BaseModel):
|
|
|
1294
1299
|
code_challenge_method: str | None = Field(None, title='Code Challenge Method')
|
|
1295
1300
|
|
|
1296
1301
|
|
|
1302
|
+
class BodyConnectAddEmailPostConnectAddEmailPost(BaseModel):
|
|
1303
|
+
email: EmailStr = Field(..., title='Email')
|
|
1304
|
+
state: str | None = Field(None, title='State')
|
|
1305
|
+
user_flow: UserFlow | None = 'session'
|
|
1306
|
+
|
|
1307
|
+
|
|
1297
1308
|
class BodyCreateCheckoutSessionApiV1PaymentStripeCheckoutPost(BaseModel):
|
|
1298
1309
|
subscription_plan: SubscriptionPlan
|
|
1299
1310
|
|
|
@@ -1304,12 +1315,6 @@ class BodyCreateDailyDataDailiesPost(BaseModel):
|
|
|
1304
1315
|
value: float = Field(..., title='Value')
|
|
1305
1316
|
|
|
1306
1317
|
|
|
1307
|
-
class BodyExpressAddEmailPostExpressAddEmailPost(BaseModel):
|
|
1308
|
-
email: EmailStr = Field(..., title='Email')
|
|
1309
|
-
state: str | None = Field(None, title='State')
|
|
1310
|
-
user_flow: UserFlow | None = 'session'
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
1318
|
class BodyGenerateApiKeyPartialsGenerateApiKeyPost(BaseModel):
|
|
1314
1319
|
scopes: list[Scope] = Field(..., title='Scopes')
|
|
1315
1320
|
|
|
@@ -1381,8 +1386,8 @@ class Lap(BaseModel):
|
|
|
1381
1386
|
start: AwareDatetime = Field(..., title='Start')
|
|
1382
1387
|
end: AwareDatetime = Field(..., title='End')
|
|
1383
1388
|
duration: timedelta = Field(..., title='Duration')
|
|
1384
|
-
start_local:
|
|
1385
|
-
end_local:
|
|
1389
|
+
start_local: AwareDatetime = Field(..., title='Start Local')
|
|
1390
|
+
end_local: AwareDatetime = Field(..., title='End Local')
|
|
1386
1391
|
|
|
1387
1392
|
|
|
1388
1393
|
class RampValueInput(BaseModel):
|
|
@@ -1563,8 +1568,8 @@ class ActivityDetails(BaseModel):
|
|
|
1563
1568
|
distance: float | None = Field(None, title='Distance')
|
|
1564
1569
|
devices: list[str] | None = Field(None, title='Devices')
|
|
1565
1570
|
duration: timedelta = Field(..., title='Duration')
|
|
1566
|
-
start_local:
|
|
1567
|
-
end_local:
|
|
1571
|
+
start_local: AwareDatetime = Field(..., title='Start Local')
|
|
1572
|
+
end_local: AwareDatetime = Field(..., title='End Local')
|
|
1568
1573
|
|
|
1569
1574
|
|
|
1570
1575
|
class ActivitySummary(BaseModel):
|
|
@@ -1580,8 +1585,8 @@ class ActivitySummary(BaseModel):
|
|
|
1580
1585
|
traces: list[TraceDetails] | None = Field(None, title='Traces')
|
|
1581
1586
|
app_metadata: dict[str, Any] | None = Field(None, title='App Metadata')
|
|
1582
1587
|
duration: timedelta = Field(..., title='Duration')
|
|
1583
|
-
start_local:
|
|
1584
|
-
end_local:
|
|
1588
|
+
start_local: AwareDatetime = Field(..., title='Start Local')
|
|
1589
|
+
end_local: AwareDatetime = Field(..., title='End Local')
|
|
1585
1590
|
|
|
1586
1591
|
|
|
1587
1592
|
class TestDetails(BaseModel):
|
|
@@ -1596,13 +1601,14 @@ class TestDetails(BaseModel):
|
|
|
1596
1601
|
app_metadata: dict[str, Any] | None = Field(None, title='App Metadata')
|
|
1597
1602
|
traces: list[TraceDetails] | None = Field(None, title='Traces')
|
|
1598
1603
|
activities: list[ActivitySummary] | None = Field(None, title='Activities')
|
|
1599
|
-
start_local:
|
|
1600
|
-
end_local:
|
|
1604
|
+
start_local: AwareDatetime = Field(..., title='Start Local')
|
|
1605
|
+
end_local: AwareDatetime | None = Field(..., title='End Local')
|
|
1601
1606
|
|
|
1602
1607
|
|
|
1603
1608
|
class TraceDetails(BaseModel):
|
|
1604
1609
|
tags: list[str] | None = Field(None, title='Tags')
|
|
1605
1610
|
id: str = Field(..., title='Id')
|
|
1611
|
+
test_id: str | None = Field(None, title='Test Id')
|
|
1606
1612
|
timestamp: AwareDatetime = Field(..., title='Timestamp')
|
|
1607
1613
|
lactate: float | None = Field(None, title='Lactate')
|
|
1608
1614
|
rpe: int | None = Field(None, title='Rpe')
|
|
@@ -1615,7 +1621,7 @@ class TraceDetails(BaseModel):
|
|
|
1615
1621
|
activity: ActivitySummary | None = None
|
|
1616
1622
|
sport: Sport | None = None
|
|
1617
1623
|
app_metadata: dict[str, Any] | None = Field(None, title='App Metadata')
|
|
1618
|
-
timestamp_local:
|
|
1624
|
+
timestamp_local: AwareDatetime = Field(..., title='Timestamp Local')
|
|
1619
1625
|
|
|
1620
1626
|
|
|
1621
1627
|
RepeatInput.model_rebuild()
|
|
@@ -18,7 +18,7 @@ from .openapi_schemas import (
|
|
|
18
18
|
BackfillStatus, DailyMeasure, DailyResponse,
|
|
19
19
|
Marker, Metric, Scope, Sport,
|
|
20
20
|
TeamResponse, TestDetails, TestResults, TestSummary, TokenResponse, TraceDetails,
|
|
21
|
-
UserInfoResponse, UserResponse, UserSummary
|
|
21
|
+
TraceResolution, UserInfoResponse, UserResponse, UserSummary
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
|
|
@@ -39,8 +39,8 @@ def sample_test_summary(sample_results: TestResults) -> TestSummary:
|
|
|
39
39
|
end=datetime(2026, 3, 15, 11, 0, tzinfo=timezone.utc),
|
|
40
40
|
results=sample_results,
|
|
41
41
|
tags=["lab", "cycling"],
|
|
42
|
-
start_local=datetime(2026, 3, 15, 10, 0),
|
|
43
|
-
end_local=datetime(2026, 3, 15, 12, 0),
|
|
42
|
+
start_local=datetime(2026, 3, 15, 10, 0, tzinfo=timezone.utc),
|
|
43
|
+
end_local=datetime(2026, 3, 15, 12, 0, tzinfo=timezone.utc),
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
|
|
@@ -152,8 +152,8 @@ class TestDataFrameConversion:
|
|
|
152
152
|
end=datetime(2026, 1, i + 1, 11, 0, tzinfo=timezone.utc),
|
|
153
153
|
results=sample_results if i == 0 else None,
|
|
154
154
|
tags=[],
|
|
155
|
-
start_local=datetime(2026, 1, i + 1, 10, 0),
|
|
156
|
-
end_local=datetime(2026, 1, i + 1, 12, 0),
|
|
155
|
+
start_local=datetime(2026, 1, i + 1, 10, 0, tzinfo=timezone.utc),
|
|
156
|
+
end_local=datetime(2026, 1, i + 1, 12, 0, tzinfo=timezone.utc),
|
|
157
157
|
)
|
|
158
158
|
for i in range(3)
|
|
159
159
|
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Tests for the trace <-> test linking surface.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
|
|
5
|
+
- ``test_id`` round-trips through ``TraceDetails`` and ``TraceCreateOrUpdate``.
|
|
6
|
+
- ``TraceResolution`` enum wire values are exactly ``"auto"`` / ``"linked"``
|
|
7
|
+
(these are what the server's query parameter accepts).
|
|
8
|
+
- ``TraceResolution`` is exposed on the public package surface.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
import sweatstack
|
|
14
|
+
from sweatstack import TraceDetails, TraceResolution
|
|
15
|
+
from sweatstack.openapi_schemas import TraceCreateOrUpdate
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _trace_details(**overrides) -> TraceDetails:
|
|
19
|
+
base = dict(
|
|
20
|
+
id="trace_001",
|
|
21
|
+
timestamp=datetime(2026, 3, 15, 9, 30, tzinfo=timezone.utc),
|
|
22
|
+
timestamp_local=datetime(2026, 3, 15, 10, 30, tzinfo=timezone.utc),
|
|
23
|
+
)
|
|
24
|
+
base.update(overrides)
|
|
25
|
+
return TraceDetails(**base)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestTestIdField:
|
|
29
|
+
def test_trace_details_round_trip_with_test_id(self):
|
|
30
|
+
original = _trace_details(test_id="test_123", lactate=4.2)
|
|
31
|
+
restored = TraceDetails.model_validate(original.model_dump())
|
|
32
|
+
|
|
33
|
+
assert restored.test_id == "test_123"
|
|
34
|
+
assert restored.lactate == 4.2
|
|
35
|
+
|
|
36
|
+
def test_trace_details_test_id_defaults_to_none(self):
|
|
37
|
+
trace = _trace_details()
|
|
38
|
+
assert trace.test_id is None
|
|
39
|
+
|
|
40
|
+
def test_trace_create_or_update_accepts_test_id(self):
|
|
41
|
+
payload = TraceCreateOrUpdate(
|
|
42
|
+
timestamp=datetime(2026, 3, 15, 9, 30, tzinfo=timezone.utc),
|
|
43
|
+
lactate=4.2,
|
|
44
|
+
test_id="test_123",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
dumped = payload.model_dump()
|
|
48
|
+
assert dumped["test_id"] == "test_123"
|
|
49
|
+
|
|
50
|
+
def test_trace_create_or_update_test_id_defaults_to_none(self):
|
|
51
|
+
payload = TraceCreateOrUpdate(
|
|
52
|
+
timestamp=datetime(2026, 3, 15, 9, 30, tzinfo=timezone.utc),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert payload.test_id is None
|
|
56
|
+
assert payload.model_dump()["test_id"] is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestGetTestTraceResolution:
|
|
60
|
+
"""``get_test`` should accept both the enum and the bare string for
|
|
61
|
+
``trace_resolution`` (mirroring how ``sport``, ``measure`` etc. work
|
|
62
|
+
elsewhere in the client)."""
|
|
63
|
+
|
|
64
|
+
def _build_client(self):
|
|
65
|
+
from sweatstack.client import Client
|
|
66
|
+
|
|
67
|
+
return Client.__new__(Client)
|
|
68
|
+
|
|
69
|
+
def test_enum_input(self):
|
|
70
|
+
client = self._build_client()
|
|
71
|
+
assert client._enums_to_strings([TraceResolution.linked]) == ["linked"]
|
|
72
|
+
|
|
73
|
+
def test_string_input(self):
|
|
74
|
+
client = self._build_client()
|
|
75
|
+
assert client._enums_to_strings(["linked"]) == ["linked"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestTraceResolutionEnum:
|
|
79
|
+
def test_wire_values(self):
|
|
80
|
+
"""The wire values must match the server's query parameter exactly."""
|
|
81
|
+
assert TraceResolution.auto.value == "auto"
|
|
82
|
+
assert TraceResolution.linked.value == "linked"
|
|
83
|
+
|
|
84
|
+
def test_membership(self):
|
|
85
|
+
"""Guards against accidental additions/removals on regen."""
|
|
86
|
+
assert {m.value for m in TraceResolution} == {"auto", "linked"}
|
|
87
|
+
|
|
88
|
+
def test_publicly_exported(self):
|
|
89
|
+
"""Importable from the top-level package, like other enums (e.g. Sport)."""
|
|
90
|
+
assert sweatstack.TraceResolution is TraceResolution
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sweatstack-0.76.2 → sweatstack-0.77.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|