sweatstack 0.77.0__tar.gz → 0.77.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/settings.local.json +3 -1
- sweatstack-0.77.1/AGENTS.md +247 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/CHANGELOG.md +6 -0
- sweatstack-0.77.1/DEVELOPMENT.md +105 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/PKG-INFO +1 -1
- {sweatstack-0.77.0 → sweatstack-0.77.1}/pyproject.toml +2 -1
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/openapi_schemas.py +22 -21
- {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_tests.py +4 -4
- {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_trace_test_linking.py +1 -1
- {sweatstack-0.77.0 → sweatstack-0.77.1}/uv.lock +37 -1
- sweatstack-0.77.0/DEVELOPMENT.md +0 -13
- {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/skills/sweatstack-python/SKILL.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/skills/sweatstack-python/client.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/.gitignore +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/.python-version +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/CONTRIBUTING.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/LICENSE +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/Makefile +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/README.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/docs/conf.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/docs/everything.rst +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/docs/index.rst +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/examples/send_webhook.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/plans/001a_tests.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/plans/001b_metadata.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/plans/001c_dailies.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/plans/002_TYPED_EXCEPTIONS.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/plans/003_trace_test_linking.md +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/client.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/exceptions.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/dependencies.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/routes.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/__init__.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_dailies.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_exceptions.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_metadata.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_teams.py +0 -0
- {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_webhooks.py +0 -0
|
@@ -0,0 +1,247 @@
|
|
|
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
|
+
## Guiding principle: mirror the REST API
|
|
9
|
+
|
|
10
|
+
This client is a thin Python projection of the SweatStack REST API. When
|
|
11
|
+
you add or change a surface, the default is to mirror the server:
|
|
12
|
+
|
|
13
|
+
| Server | Python |
|
|
14
|
+
| --------------------------------------- | --------------------------------------- |
|
|
15
|
+
| `GET /api/v1/tests/{id}` | `client.get_test(test_id)` |
|
|
16
|
+
| `POST /api/v1/traces/` | `client.create_trace(...)` |
|
|
17
|
+
| `PUT /api/v1/traces/{id}` (full-replace)| `client.update_trace(trace_id, ...)` |
|
|
18
|
+
| `DELETE /api/v1/dailies/{id}` | `client.delete_daily(daily_id)` |
|
|
19
|
+
| query param `?sport=cycling&sport=running` | `sports=[Sport.cycling, Sport.running]` |
|
|
20
|
+
| JSON body field `test_id` | kwarg `test_id` |
|
|
21
|
+
| OpenAPI enum value `"auto"` | `TraceResolution.auto` |
|
|
22
|
+
|
|
23
|
+
Match the server's field names, enum values, full-replace PUT semantics,
|
|
24
|
+
and pagination shape. Don't redesign the API in Python.
|
|
25
|
+
|
|
26
|
+
**Deliberate deviations** (these are the only ones — don't invent more):
|
|
27
|
+
|
|
28
|
+
- Path-param IDs are positional Python args (`get_test(test_id)`), not part
|
|
29
|
+
of a URL string.
|
|
30
|
+
- Enum-typed params accept `Enum | str` so callers don't need to import the
|
|
31
|
+
enum for one-off use.
|
|
32
|
+
- A `traces=` query parameter renames to `trace_resolution=` on the Python
|
|
33
|
+
side when the wire name would be ambiguous as a kwarg. Document the
|
|
34
|
+
mapping in the docstring.
|
|
35
|
+
- `as_dataframe=True` is a client-side convenience over list endpoints.
|
|
36
|
+
- Convenience composites that wrap multiple calls
|
|
37
|
+
(`get_latest_activity_data`, `get_longitudinal_*`) live alongside the
|
|
38
|
+
literal mirrors. Add new ones sparingly; only when a real workflow is
|
|
39
|
+
awkward through the literal surface.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Project shape
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
src/sweatstack/
|
|
46
|
+
├── openapi_schemas.py # AUTO-GENERATED. Never hand-edit.
|
|
47
|
+
├── schemas.py # Re-exports + Enum._missing_ / display_name helpers.
|
|
48
|
+
├── exceptions.py # Public error contract. No httpx types leak.
|
|
49
|
+
├── client.py # Single Client class + module-level singletons.
|
|
50
|
+
├── utils.py # Dataframe / JWT helpers.
|
|
51
|
+
├── streamlit.py # Streamlit integration (optional extra).
|
|
52
|
+
├── fastapi/ # FastAPI integration (optional extra).
|
|
53
|
+
└── cli.py # Entry points.
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Adding a new Pydantic model from the server takes three steps:
|
|
57
|
+
|
|
58
|
+
1. Regen `openapi_schemas.py` ([DEVELOPMENT.md](DEVELOPMENT.md#regenerating-openapi_schemaspy)).
|
|
59
|
+
2. Re-export it from `schemas.py`.
|
|
60
|
+
3. Import it into `client.py` so `from sweatstack import *` exposes it.
|
|
61
|
+
|
|
62
|
+
Skipping step 3 silently breaks the public surface. Same for new enums.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
## Hard rules
|
|
66
|
+
|
|
67
|
+
- **`uv`, never `pip`.** Every command goes through `uv run` / `uv add`.
|
|
68
|
+
- **Never hand-edit `openapi_schemas.py`.** Regenerate.
|
|
69
|
+
- **`Raises:` references the typed exceptions** from
|
|
70
|
+
`sweatstack.exceptions`. Don't write `HTTPStatusError` in new docstrings.
|
|
71
|
+
- **Public methods belong in `_generate_singleton_methods(...)`** at the
|
|
72
|
+
bottom of `client.py`. Forgetting is silent.
|
|
73
|
+
- **Enum-typed params accept `Enum | str`** and route through
|
|
74
|
+
`_enums_to_strings`. Don't introduce strict-enum-only parameters.
|
|
75
|
+
- **Tests are offline.** No network calls. Use `Client.__new__(Client)` to
|
|
76
|
+
bypass init when you need an instance for a helper method.
|
|
77
|
+
- **`update_*` methods are full-replace.** Document the silent-clear
|
|
78
|
+
footgun in the docstring (see below).
|
|
79
|
+
- **CHANGELOG entries are user-facing**, not dev-facing.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Method shape
|
|
83
|
+
|
|
84
|
+
Every method on `Client` follows this shape. Match it.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
def do_thing(
|
|
88
|
+
self,
|
|
89
|
+
resource_id: str, # path params: positional
|
|
90
|
+
*, # rest: keyword-only
|
|
91
|
+
timestamp: datetime,
|
|
92
|
+
sport: Sport | str | None = None, # enum params: Enum | str
|
|
93
|
+
tags: list[str] | None = None,
|
|
94
|
+
) -> ThingDetails:
|
|
95
|
+
"""One-line summary.
|
|
96
|
+
|
|
97
|
+
Optional paragraph for non-obvious behaviour (full-replace,
|
|
98
|
+
server-side defaults, side effects).
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
ThingDetails: ...
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
SweatStackNotFoundError: If <specific condition>.
|
|
108
|
+
SweatStackAPIError: If the API request fails for any other reason.
|
|
109
|
+
"""
|
|
110
|
+
sport = self._enums_to_strings([sport])[0] if sport else None
|
|
111
|
+
with self._http_client() as client:
|
|
112
|
+
response = client.post(
|
|
113
|
+
url=f"/api/v1/things/{resource_id}",
|
|
114
|
+
json={"timestamp": timestamp.isoformat(), "sport": sport, "tags": tags},
|
|
115
|
+
)
|
|
116
|
+
self._raise_for_status(response)
|
|
117
|
+
return ThingDetails.model_validate(response.json())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Invariants baked into the template:
|
|
121
|
+
|
|
122
|
+
- Path-param IDs positional, everything else keyword-only.
|
|
123
|
+
- Datetimes serialize via `.isoformat()`.
|
|
124
|
+
- Body uses every field explicitly (full-replace contract).
|
|
125
|
+
- Request goes through `self._http_client()` context manager.
|
|
126
|
+
- Response goes through `self._raise_for_status()` before parsing.
|
|
127
|
+
- Return is a validated Pydantic model — never the raw dict.
|
|
128
|
+
- `update_*` methods are typed `-> None`. The server's
|
|
129
|
+
`{"message": "..."}` body carries no useful info.
|
|
130
|
+
|
|
131
|
+
**List endpoints** add a `_get_<resource>_generator()` that yields
|
|
132
|
+
validated objects with internal pagination, plus a `get_<resource>s(...,
|
|
133
|
+
as_dataframe=False)` wrapper. Empty-list DataFrames go through
|
|
134
|
+
`_create_empty_dataframe_from_model(Model, normalize_columns=[...])` so
|
|
135
|
+
column names stay stable. See `get_activities` and `get_tests` for
|
|
136
|
+
exemplars.
|
|
137
|
+
|
|
138
|
+
**Query parameters** are only sent when the caller supplied a non-default
|
|
139
|
+
value. For enum-typed query params with an explicit default, compare
|
|
140
|
+
against the default and skip when equal. Always pass `.value` for enums
|
|
141
|
+
into `httpx.params` — `httpx` calls `str()`, which gives
|
|
142
|
+
`"TraceResolution.linked"`, not `"linked"`.
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## Full-replace `update_*` methods
|
|
146
|
+
|
|
147
|
+
PUT endpoints overwrite every field, including by setting unsent fields
|
|
148
|
+
to `null`. Two rules:
|
|
149
|
+
|
|
150
|
+
1. **Send every field in the body, even when `None`.** Don't omit.
|
|
151
|
+
2. **Document the silent-clear footgun in the docstring** whenever you
|
|
152
|
+
add a new optional field to an existing `update_*` method, *and*
|
|
153
|
+
call it out in the CHANGELOG `### Changed` section. Existing callers
|
|
154
|
+
who don't know about the new field will silently null it on their
|
|
155
|
+
next update.
|
|
156
|
+
|
|
157
|
+
Don't try to soften the contract with "preserve if omitted" sentinels;
|
|
158
|
+
that diverges from how every other field behaves on these methods.
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
## Exceptions
|
|
162
|
+
|
|
163
|
+
The hierarchy in `sweatstack/exceptions.py` is the **public** error
|
|
164
|
+
contract — consumers should never need to import `httpx`.
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
SweatStackError
|
|
168
|
+
├── SweatStackConnectionError # DNS / timeout / no response
|
|
169
|
+
├── SweatStackTokenRefreshError
|
|
170
|
+
└── SweatStackAPIError # got a response with status >= 400
|
|
171
|
+
├── SweatStackAuthError # 401, 403
|
|
172
|
+
├── SweatStackNotFoundError # 404
|
|
173
|
+
├── SweatStackRateLimitError # 429
|
|
174
|
+
├── SweatStackBadRequestError # other 4xx
|
|
175
|
+
└── SweatStackServerError # 5xx
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Docstrings: list specific subclasses for meaningful conditions (e.g. 404
|
|
179
|
+
when a path ID may not exist), then a catch-all `SweatStackAPIError`.
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
## Tests
|
|
183
|
+
|
|
184
|
+
Live in `tests/`. Offline only. The workhorse pattern is schema
|
|
185
|
+
round-tripping:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
restored = TraceDetails.model_validate(original.model_dump())
|
|
189
|
+
assert restored.test_id == "test_123"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Catches missing fields, type drift, and enum-casing changes from regen.
|
|
193
|
+
Exemplars: `tests/test_trace_test_linking.py`, `tests/test_tests.py`,
|
|
194
|
+
`tests/test_dtype_conversion.py`.
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
## CHANGELOG
|
|
198
|
+
|
|
199
|
+
User-facing only. Lead with what changed for the user; skip regen
|
|
200
|
+
output, file renames, refactors. Loud-callout any behaviour change in
|
|
201
|
+
`### Changed`, especially full-replace footguns. SemVer.
|
|
202
|
+
|
|
203
|
+
Good:
|
|
204
|
+
|
|
205
|
+
> ### Changed
|
|
206
|
+
> - `update_trace()` replaces all fields, including `test_id`. Callers
|
|
207
|
+
> that omit `test_id` will clear any existing link.
|
|
208
|
+
|
|
209
|
+
Bad (belongs in the commit message, not the changelog):
|
|
210
|
+
|
|
211
|
+
> ### Changed
|
|
212
|
+
> - Local datetime fields are now `AwareDatetime`. The
|
|
213
|
+
> `BodyExpressAddEmail...` schema is renamed to
|
|
214
|
+
> `BodyConnectAddEmail...`.
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## Known rough edges
|
|
218
|
+
|
|
219
|
+
These are conventions we live with but would reconsider in a rewrite.
|
|
220
|
+
Don't paper over them in new code; flag them in review.
|
|
221
|
+
|
|
222
|
+
- **Singleton functions are injected via `globals()`** at module import.
|
|
223
|
+
The "is this method a singleton?" rule is unwritten, which is exactly
|
|
224
|
+
why `update_trace` and `delete_trace` are missing from the list while
|
|
225
|
+
`update_test` and `delete_test` are present. When you touch an
|
|
226
|
+
unregistered public method, register it.
|
|
227
|
+
- **`__init__.py` uses `from .client import *`.** Explicit re-exports
|
|
228
|
+
would be safer; we haven't done the work.
|
|
229
|
+
- **Docstring exception drift.** Most existing docstrings still say
|
|
230
|
+
`HTTPStatusError`. Fix as you touch them, but a one-shot sweep is also
|
|
231
|
+
welcome.
|
|
232
|
+
- **`pyproject.toml` claims `requires-python = ">=3.9"`** while the code
|
|
233
|
+
uses `X | Y` union syntax (3.10+). The real floor is 3.10. Bump the
|
|
234
|
+
pin when convenient.
|
|
235
|
+
- **No CI type-checker or linter.** For a typed library, this is a gap.
|
|
236
|
+
Don't let new code make it worse.
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
## When in doubt
|
|
240
|
+
|
|
241
|
+
- Match the nearest existing method in `client.py`. Consistency with the
|
|
242
|
+
surroundings beats local cleverness.
|
|
243
|
+
- Reuse the helpers: `_enums_to_strings`, `_get_*_generator`,
|
|
244
|
+
`_normalize_dataframe_column`, `_create_empty_dataframe_from_model`,
|
|
245
|
+
`_set_app_metadata`. Don't reinvent them.
|
|
246
|
+
- Don't add abstractions for hypothetical future flexibility. Three
|
|
247
|
+
similar blocks is the pattern, not a smell.
|
|
@@ -6,6 +6,12 @@ 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.1] - 2026-05-19
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Reverted unrelated OpenAPI schema drift.
|
|
13
|
+
|
|
14
|
+
|
|
9
15
|
## [0.77.0] - 2026-05-19
|
|
10
16
|
|
|
11
17
|
### Added
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
Mechanics for working on the SweatStack Python client locally. For the
|
|
4
|
+
conventions you must follow when changing code, see [AGENTS.md](AGENTS.md).
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Tooling
|
|
8
|
+
|
|
9
|
+
This project uses [`uv`](https://docs.astral.sh/uv/) for everything.
|
|
10
|
+
**Never use `pip` directly.**
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uv sync # install/update dependencies
|
|
14
|
+
uv run pytest # run tests
|
|
15
|
+
uv run python -c "..." # ad-hoc scripts
|
|
16
|
+
uv run generate-response-models # regenerate OpenAPI schemas (see below)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Python ≥ 3.9 is supported; develop against the version pinned in
|
|
20
|
+
`.python-version`.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Running locally
|
|
24
|
+
|
|
25
|
+
For interactive exploration (Jupyter):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uvx --with-editable "path/to/sweatstack-python[jupyterlab]" jupyter lab
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run from a scratch directory so JupyterLab does not litter the repo with
|
|
32
|
+
`Untitled` notebooks.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Running tests
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv run pytest # full suite
|
|
39
|
+
uv run pytest tests/test_<name>.py # one file
|
|
40
|
+
uv run pytest --ignore=tests/test_webhooks.py # skip optional-dep tests
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`tests/test_webhooks.py` requires `fastapi`, which is an optional extra. If
|
|
44
|
+
you have not installed it, ignore that file or `uv sync --extra fastapi`.
|
|
45
|
+
|
|
46
|
+
All tests are offline — no test should make a network call. See
|
|
47
|
+
[AGENTS.md → Testing](AGENTS.md#testing) for how to write new ones.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Regenerating `openapi_schemas.py`
|
|
51
|
+
|
|
52
|
+
`src/sweatstack/openapi_schemas.py` is **fully machine-generated** from the
|
|
53
|
+
backend's OpenAPI document by `datamodel-code-generator`. Never hand-edit it.
|
|
54
|
+
|
|
55
|
+
### Procedure
|
|
56
|
+
|
|
57
|
+
1. Start the SweatStack backend so it serves `http://localhost:8080/openapi.json`.
|
|
58
|
+
2. Run:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv run generate-response-models
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
3. Review the diff carefully. The file is regenerated as a whole, so the
|
|
65
|
+
diff will often include **unrelated upstream changes** since the last
|
|
66
|
+
regeneration (field renames, type tightening, new endpoints).
|
|
67
|
+
|
|
68
|
+
4. If the diff contains changes outside the feature you are working on,
|
|
69
|
+
**split them into a separate commit** (`chore: regenerate openapi
|
|
70
|
+
schemas`) that lands before the feature commit. Keep the feature
|
|
71
|
+
commit minimal so reviewers can read it.
|
|
72
|
+
|
|
73
|
+
### Staging only part of the regenerated file
|
|
74
|
+
|
|
75
|
+
When the regen drift is too large to include in a feature commit but you
|
|
76
|
+
do not want to lose it, stage only the relevant hunks:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cp src/sweatstack/openapi_schemas.py /tmp/openapi.full.py
|
|
80
|
+
git checkout HEAD -- src/sweatstack/openapi_schemas.py
|
|
81
|
+
# hand-apply only the lines relevant to your feature
|
|
82
|
+
git add src/sweatstack/openapi_schemas.py
|
|
83
|
+
cp /tmp/openapi.full.py src/sweatstack/openapi_schemas.py # restore working tree
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The working tree now has the full regen (unstaged), and the index has the
|
|
87
|
+
minimal feature delta. Commit, then handle the remainder separately.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## Releasing
|
|
91
|
+
|
|
92
|
+
1. Bump `version` in `pyproject.toml` (SemVer).
|
|
93
|
+
2. Add a CHANGELOG entry — see [AGENTS.md → CHANGELOG](AGENTS.md#changelog).
|
|
94
|
+
3. `make build` and `make publish` (twine to PyPI). The `Makefile` has
|
|
95
|
+
the canonical commands.
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## Docs
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
make docs
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Renders Sphinx to `docs/_build/markdown/`. Most public classes are
|
|
105
|
+
documented automatically via `autoclass` directives in `docs/everything.rst`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sweatstack"
|
|
3
|
-
version = "0.77.
|
|
3
|
+
version = "0.77.1"
|
|
4
4
|
description = "The official Python client for SweatStack"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -64,6 +64,7 @@ build-backend = "hatchling.build"
|
|
|
64
64
|
dev = [
|
|
65
65
|
"datamodel-code-generator[ruff]>=0.54.1",
|
|
66
66
|
"myst-parser>=4.0.1",
|
|
67
|
+
"pytest>=9.0.3",
|
|
67
68
|
"sphinx>=8.2.3",
|
|
68
69
|
"sphinx-markdown-builder>=0.6.8",
|
|
69
70
|
"streamlit>=1.42.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# generated by datamodel-codegen:
|
|
2
2
|
# filename: openapi.json
|
|
3
|
-
# timestamp: 2026-
|
|
3
|
+
# timestamp: 2026-04-27T08:28:54+00:00
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -13,6 +13,7 @@ from pydantic import (
|
|
|
13
13
|
AnyUrl,
|
|
14
14
|
AwareDatetime,
|
|
15
15
|
BaseModel,
|
|
16
|
+
NaiveDatetime,
|
|
16
17
|
EmailStr,
|
|
17
18
|
Field,
|
|
18
19
|
RootModel,
|
|
@@ -25,7 +26,7 @@ from pydantic import (
|
|
|
25
26
|
|
|
26
27
|
class Activity(BaseModel):
|
|
27
28
|
id: str = Field(..., title='Id')
|
|
28
|
-
start_date_local:
|
|
29
|
+
start_date_local: NaiveDatetime | None = Field(None, title='Start Date Local')
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class ActivityUpdate(BaseModel):
|
|
@@ -1115,8 +1116,8 @@ class TestSummary(BaseModel):
|
|
|
1115
1116
|
end: AwareDatetime | None = Field(None, title='End')
|
|
1116
1117
|
results: TestResults | None = None
|
|
1117
1118
|
app_metadata: dict[str, Any] | None = Field(None, title='App Metadata')
|
|
1118
|
-
start_local:
|
|
1119
|
-
end_local:
|
|
1119
|
+
start_local: NaiveDatetime = Field(..., title='Start Local')
|
|
1120
|
+
end_local: NaiveDatetime | None = Field(..., title='End Local')
|
|
1120
1121
|
|
|
1121
1122
|
|
|
1122
1123
|
class TestUpdate(BaseModel):
|
|
@@ -1177,7 +1178,7 @@ class UserInfoResponse(BaseModel):
|
|
|
1177
1178
|
given_name: str | None = Field(None, title='Given Name')
|
|
1178
1179
|
family_name: str | None = Field(None, title='Family Name')
|
|
1179
1180
|
email: str | None = Field(None, title='Email')
|
|
1180
|
-
registered_at: AwareDatetime = Field(..., title='Registered At')
|
|
1181
|
+
registered_at: AwareDatetime | NaiveDatetime = Field(..., title='Registered At')
|
|
1181
1182
|
name: str = Field(..., title='Name')
|
|
1182
1183
|
|
|
1183
1184
|
|
|
@@ -1186,7 +1187,7 @@ class UserResponse(BaseModel):
|
|
|
1186
1187
|
first_name: str | None = Field(..., title='First Name')
|
|
1187
1188
|
last_name: str | None = Field(..., title='Last Name')
|
|
1188
1189
|
admin: bool = Field(..., title='Admin')
|
|
1189
|
-
registered_at: AwareDatetime = Field(..., title='Registered At')
|
|
1190
|
+
registered_at: AwareDatetime | NaiveDatetime = Field(..., title='Registered At')
|
|
1190
1191
|
display_name: str = Field(..., title='Display Name')
|
|
1191
1192
|
is_managed: bool = Field(..., title='Is Managed')
|
|
1192
1193
|
|
|
@@ -1299,12 +1300,6 @@ class BodyAuthorizeOauthAuthorizePost(BaseModel):
|
|
|
1299
1300
|
code_challenge_method: str | None = Field(None, title='Code Challenge Method')
|
|
1300
1301
|
|
|
1301
1302
|
|
|
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
|
-
|
|
1308
1303
|
class BodyCreateCheckoutSessionApiV1PaymentStripeCheckoutPost(BaseModel):
|
|
1309
1304
|
subscription_plan: SubscriptionPlan
|
|
1310
1305
|
|
|
@@ -1315,6 +1310,12 @@ class BodyCreateDailyDataDailiesPost(BaseModel):
|
|
|
1315
1310
|
value: float = Field(..., title='Value')
|
|
1316
1311
|
|
|
1317
1312
|
|
|
1313
|
+
class BodyExpressAddEmailPostExpressAddEmailPost(BaseModel):
|
|
1314
|
+
email: EmailStr = Field(..., title='Email')
|
|
1315
|
+
state: str | None = Field(None, title='State')
|
|
1316
|
+
user_flow: UserFlow | None = 'session'
|
|
1317
|
+
|
|
1318
|
+
|
|
1318
1319
|
class BodyGenerateApiKeyPartialsGenerateApiKeyPost(BaseModel):
|
|
1319
1320
|
scopes: list[Scope] = Field(..., title='Scopes')
|
|
1320
1321
|
|
|
@@ -1386,8 +1387,8 @@ class Lap(BaseModel):
|
|
|
1386
1387
|
start: AwareDatetime = Field(..., title='Start')
|
|
1387
1388
|
end: AwareDatetime = Field(..., title='End')
|
|
1388
1389
|
duration: timedelta = Field(..., title='Duration')
|
|
1389
|
-
start_local:
|
|
1390
|
-
end_local:
|
|
1390
|
+
start_local: NaiveDatetime = Field(..., title='Start Local')
|
|
1391
|
+
end_local: NaiveDatetime = Field(..., title='End Local')
|
|
1391
1392
|
|
|
1392
1393
|
|
|
1393
1394
|
class RampValueInput(BaseModel):
|
|
@@ -1568,8 +1569,8 @@ class ActivityDetails(BaseModel):
|
|
|
1568
1569
|
distance: float | None = Field(None, title='Distance')
|
|
1569
1570
|
devices: list[str] | None = Field(None, title='Devices')
|
|
1570
1571
|
duration: timedelta = Field(..., title='Duration')
|
|
1571
|
-
start_local:
|
|
1572
|
-
end_local:
|
|
1572
|
+
start_local: NaiveDatetime = Field(..., title='Start Local')
|
|
1573
|
+
end_local: NaiveDatetime = Field(..., title='End Local')
|
|
1573
1574
|
|
|
1574
1575
|
|
|
1575
1576
|
class ActivitySummary(BaseModel):
|
|
@@ -1585,8 +1586,8 @@ class ActivitySummary(BaseModel):
|
|
|
1585
1586
|
traces: list[TraceDetails] | None = Field(None, title='Traces')
|
|
1586
1587
|
app_metadata: dict[str, Any] | None = Field(None, title='App Metadata')
|
|
1587
1588
|
duration: timedelta = Field(..., title='Duration')
|
|
1588
|
-
start_local:
|
|
1589
|
-
end_local:
|
|
1589
|
+
start_local: NaiveDatetime = Field(..., title='Start Local')
|
|
1590
|
+
end_local: NaiveDatetime = Field(..., title='End Local')
|
|
1590
1591
|
|
|
1591
1592
|
|
|
1592
1593
|
class TestDetails(BaseModel):
|
|
@@ -1601,8 +1602,8 @@ class TestDetails(BaseModel):
|
|
|
1601
1602
|
app_metadata: dict[str, Any] | None = Field(None, title='App Metadata')
|
|
1602
1603
|
traces: list[TraceDetails] | None = Field(None, title='Traces')
|
|
1603
1604
|
activities: list[ActivitySummary] | None = Field(None, title='Activities')
|
|
1604
|
-
start_local:
|
|
1605
|
-
end_local:
|
|
1605
|
+
start_local: NaiveDatetime = Field(..., title='Start Local')
|
|
1606
|
+
end_local: NaiveDatetime | None = Field(..., title='End Local')
|
|
1606
1607
|
|
|
1607
1608
|
|
|
1608
1609
|
class TraceDetails(BaseModel):
|
|
@@ -1621,7 +1622,7 @@ class TraceDetails(BaseModel):
|
|
|
1621
1622
|
activity: ActivitySummary | None = None
|
|
1622
1623
|
sport: Sport | None = None
|
|
1623
1624
|
app_metadata: dict[str, Any] | None = Field(None, title='App Metadata')
|
|
1624
|
-
timestamp_local:
|
|
1625
|
+
timestamp_local: NaiveDatetime = Field(..., title='Timestamp Local')
|
|
1625
1626
|
|
|
1626
1627
|
|
|
1627
1628
|
RepeatInput.model_rebuild()
|
|
@@ -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),
|
|
43
|
+
end_local=datetime(2026, 3, 15, 12, 0),
|
|
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),
|
|
156
|
+
end_local=datetime(2026, 1, i + 1, 12, 0),
|
|
157
157
|
)
|
|
158
158
|
for i in range(3)
|
|
159
159
|
]
|
|
@@ -19,7 +19,7 @@ def _trace_details(**overrides) -> TraceDetails:
|
|
|
19
19
|
base = dict(
|
|
20
20
|
id="trace_001",
|
|
21
21
|
timestamp=datetime(2026, 3, 15, 9, 30, tzinfo=timezone.utc),
|
|
22
|
-
timestamp_local=datetime(2026, 3, 15, 10, 30
|
|
22
|
+
timestamp_local=datetime(2026, 3, 15, 10, 30),
|
|
23
23
|
)
|
|
24
24
|
base.update(overrides)
|
|
25
25
|
return TraceDetails(**base)
|
|
@@ -893,6 +893,15 @@ wheels = [
|
|
|
893
893
|
{ url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" },
|
|
894
894
|
]
|
|
895
895
|
|
|
896
|
+
[[package]]
|
|
897
|
+
name = "iniconfig"
|
|
898
|
+
version = "2.3.0"
|
|
899
|
+
source = { registry = "https://pypi.org/simple" }
|
|
900
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
901
|
+
wheels = [
|
|
902
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
903
|
+
]
|
|
904
|
+
|
|
896
905
|
[[package]]
|
|
897
906
|
name = "ipykernel"
|
|
898
907
|
version = "6.29.5"
|
|
@@ -1680,6 +1689,15 @@ dependencies = [
|
|
|
1680
1689
|
[package.metadata]
|
|
1681
1690
|
requires-dist = [{ name = "sweatstack", extras = ["streamlit"], editable = "." }]
|
|
1682
1691
|
|
|
1692
|
+
[[package]]
|
|
1693
|
+
name = "pluggy"
|
|
1694
|
+
version = "1.6.0"
|
|
1695
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1696
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
1697
|
+
wheels = [
|
|
1698
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
1699
|
+
]
|
|
1700
|
+
|
|
1683
1701
|
[[package]]
|
|
1684
1702
|
name = "prometheus-client"
|
|
1685
1703
|
version = "0.21.1"
|
|
@@ -1901,6 +1919,22 @@ wheels = [
|
|
|
1901
1919
|
{ url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716, upload-time = "2024-12-31T20:59:42.738Z" },
|
|
1902
1920
|
]
|
|
1903
1921
|
|
|
1922
|
+
[[package]]
|
|
1923
|
+
name = "pytest"
|
|
1924
|
+
version = "9.0.3"
|
|
1925
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1926
|
+
dependencies = [
|
|
1927
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
1928
|
+
{ name = "iniconfig" },
|
|
1929
|
+
{ name = "packaging" },
|
|
1930
|
+
{ name = "pluggy" },
|
|
1931
|
+
{ name = "pygments" },
|
|
1932
|
+
]
|
|
1933
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
|
1934
|
+
wheels = [
|
|
1935
|
+
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
|
1936
|
+
]
|
|
1937
|
+
|
|
1904
1938
|
[[package]]
|
|
1905
1939
|
name = "python-dateutil"
|
|
1906
1940
|
version = "2.9.0.post0"
|
|
@@ -2537,7 +2571,7 @@ wheels = [
|
|
|
2537
2571
|
|
|
2538
2572
|
[[package]]
|
|
2539
2573
|
name = "sweatstack"
|
|
2540
|
-
version = "0.77.
|
|
2574
|
+
version = "0.77.1"
|
|
2541
2575
|
source = { editable = "." }
|
|
2542
2576
|
dependencies = [
|
|
2543
2577
|
{ name = "email-validator" },
|
|
@@ -2566,6 +2600,7 @@ streamlit = [
|
|
|
2566
2600
|
dev = [
|
|
2567
2601
|
{ name = "datamodel-code-generator", extra = ["ruff"] },
|
|
2568
2602
|
{ name = "myst-parser" },
|
|
2603
|
+
{ name = "pytest" },
|
|
2569
2604
|
{ name = "sphinx" },
|
|
2570
2605
|
{ name = "sphinx-markdown-builder" },
|
|
2571
2606
|
{ name = "streamlit" },
|
|
@@ -2592,6 +2627,7 @@ provides-extras = ["streamlit", "jupyter", "fastapi"]
|
|
|
2592
2627
|
dev = [
|
|
2593
2628
|
{ name = "datamodel-code-generator", extras = ["ruff"], specifier = ">=0.54.1" },
|
|
2594
2629
|
{ name = "myst-parser", specifier = ">=4.0.1" },
|
|
2630
|
+
{ name = "pytest", specifier = ">=9.0.3" },
|
|
2595
2631
|
{ name = "sphinx", specifier = ">=8.2.3" },
|
|
2596
2632
|
{ name = "sphinx-markdown-builder", specifier = ">=0.6.8" },
|
|
2597
2633
|
{ name = "streamlit", specifier = ">=1.42.0" },
|
sweatstack-0.77.0/DEVELOPMENT.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Development
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Running locally
|
|
6
|
-
|
|
7
|
-
Point your terminal to some other directory (to not clutter this one with `Untitled` files) and then run:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
uvx --with-editable "path/to/sweatstack-python[jupyterlab]" jupyter lab
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
This will start a JupyterLab instance.
|
|
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
|
{sweatstack-0.77.0 → sweatstack-0.77.1}/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
|
|
File without changes
|
|
File without changes
|