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