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.
Files changed (60) hide show
  1. {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/settings.local.json +3 -1
  2. sweatstack-0.77.1/AGENTS.md +247 -0
  3. {sweatstack-0.77.0 → sweatstack-0.77.1}/CHANGELOG.md +6 -0
  4. sweatstack-0.77.1/DEVELOPMENT.md +105 -0
  5. {sweatstack-0.77.0 → sweatstack-0.77.1}/PKG-INFO +1 -1
  6. {sweatstack-0.77.0 → sweatstack-0.77.1}/pyproject.toml +2 -1
  7. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/openapi_schemas.py +22 -21
  8. {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_tests.py +4 -4
  9. {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_trace_test_linking.py +1 -1
  10. {sweatstack-0.77.0 → sweatstack-0.77.1}/uv.lock +37 -1
  11. sweatstack-0.77.0/DEVELOPMENT.md +0 -13
  12. {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/skills/sweatstack-python/SKILL.md +0 -0
  13. {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/skills/sweatstack-python/client.md +0 -0
  14. {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/skills/sweatstack-python/data-models.md +0 -0
  15. {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/skills/sweatstack-python/fastapi.md +0 -0
  16. {sweatstack-0.77.0 → sweatstack-0.77.1}/.claude/skills/sweatstack-python/streamlit.md +0 -0
  17. {sweatstack-0.77.0 → sweatstack-0.77.1}/.gitignore +0 -0
  18. {sweatstack-0.77.0 → sweatstack-0.77.1}/.python-version +0 -0
  19. {sweatstack-0.77.0 → sweatstack-0.77.1}/CONTRIBUTING.md +0 -0
  20. {sweatstack-0.77.0 → sweatstack-0.77.1}/LICENSE +0 -0
  21. {sweatstack-0.77.0 → sweatstack-0.77.1}/Makefile +0 -0
  22. {sweatstack-0.77.0 → sweatstack-0.77.1}/README.md +0 -0
  23. {sweatstack-0.77.0 → sweatstack-0.77.1}/docs/conf.py +0 -0
  24. {sweatstack-0.77.0 → sweatstack-0.77.1}/docs/everything.rst +0 -0
  25. {sweatstack-0.77.0 → sweatstack-0.77.1}/docs/index.rst +0 -0
  26. {sweatstack-0.77.0 → sweatstack-0.77.1}/examples/fastapi_webhooks_example.py +0 -0
  27. {sweatstack-0.77.0 → sweatstack-0.77.1}/examples/send_webhook.py +0 -0
  28. {sweatstack-0.77.0 → sweatstack-0.77.1}/plans/001a_tests.md +0 -0
  29. {sweatstack-0.77.0 → sweatstack-0.77.1}/plans/001b_metadata.md +0 -0
  30. {sweatstack-0.77.0 → sweatstack-0.77.1}/plans/001c_dailies.md +0 -0
  31. {sweatstack-0.77.0 → sweatstack-0.77.1}/plans/002_TYPED_EXCEPTIONS.md +0 -0
  32. {sweatstack-0.77.0 → sweatstack-0.77.1}/plans/003_trace_test_linking.md +0 -0
  33. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  34. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/__init__.py +0 -0
  35. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/cli.py +0 -0
  36. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/client.py +0 -0
  37. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/constants.py +0 -0
  38. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/exceptions.py +0 -0
  39. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/__init__.py +0 -0
  40. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/config.py +0 -0
  41. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/dependencies.py +0 -0
  42. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/models.py +0 -0
  43. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/routes.py +0 -0
  44. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/session.py +0 -0
  45. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/token_stores.py +0 -0
  46. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/fastapi/webhooks.py +0 -0
  47. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/ipython_init.py +0 -0
  48. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  49. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/py.typed +0 -0
  50. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/schemas.py +0 -0
  51. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/streamlit.py +0 -0
  52. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/sweatshell.py +0 -0
  53. {sweatstack-0.77.0 → sweatstack-0.77.1}/src/sweatstack/utils.py +0 -0
  54. {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/__init__.py +0 -0
  55. {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_dailies.py +0 -0
  56. {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_dtype_conversion.py +0 -0
  57. {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_exceptions.py +0 -0
  58. {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_metadata.py +0 -0
  59. {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_teams.py +0 -0
  60. {sweatstack-0.77.0 → sweatstack-0.77.1}/tests/test_webhooks.py +0 -0
@@ -21,7 +21,9 @@
21
21
  "Bash(test:*)",
22
22
  "Bash(uv run:*)",
23
23
  "mcp__sentry__get_sentry_resource",
24
- "Bash(awk *)"
24
+ "Bash(awk *)",
25
+ "Bash(git show *)",
26
+ "Read(//tmp/**)"
25
27
  ],
26
28
  "deny": []
27
29
  }
@@ -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
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.77.0
3
+ Version: 0.77.1
4
4
  Summary: The official Python client for SweatStack
5
5
  Project-URL: Homepage, https://sweatstack.no
6
6
  Project-URL: Documentation, https://docs.sweatstack.no/getting-started/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.77.0"
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-05-19T10:00:53+00:00
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: AwareDatetime | None = Field(None, title='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: AwareDatetime = Field(..., title='Start Local')
1119
- end_local: AwareDatetime | None = Field(..., title='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: AwareDatetime = Field(..., title='Start Local')
1390
- end_local: AwareDatetime = Field(..., title='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: AwareDatetime = Field(..., title='Start Local')
1572
- end_local: AwareDatetime = Field(..., title='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: AwareDatetime = Field(..., title='Start Local')
1589
- end_local: AwareDatetime = Field(..., title='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: AwareDatetime = Field(..., title='Start Local')
1605
- end_local: AwareDatetime | None = Field(..., title='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: AwareDatetime = Field(..., title='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, tzinfo=timezone.utc),
43
- end_local=datetime(2026, 3, 15, 12, 0, tzinfo=timezone.utc),
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, tzinfo=timezone.utc),
156
- end_local=datetime(2026, 1, i + 1, 12, 0, tzinfo=timezone.utc),
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, tzinfo=timezone.utc),
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.0"
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" },
@@ -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