sweatstack 0.95.0__tar.gz → 0.96.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sweatstack-0.95.0 → sweatstack-0.96.0}/CHANGELOG.md +19 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/PKG-INFO +1 -1
- {sweatstack-0.95.0 → sweatstack-0.96.0}/pyproject.toml +1 -1
- {sweatstack-0.95.0 → sweatstack-0.96.0}/skills/sweatstack-python/api.md +1 -1
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/_frames.py +0 -3
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/cli.py +5 -12
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/client.py +0 -1
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/routes.py +9 -9
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/openapi_schemas.py +22 -453
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/oauth.py +54 -12
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/streamlit.py +14 -12
- sweatstack-0.96.0/tests/test_authorization_url.py +103 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_frames.py +0 -21
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_resources.py +2 -3
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_wire_names.py +1 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/uv.lock +1 -1
- {sweatstack-0.95.0 → sweatstack-0.96.0}/.git-blame-ignore-revs +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/.github/workflows/ci.yml +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/.gitignore +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/.gitleaks.toml +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/.python-version +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/AGENTS.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/CONTRIBUTING.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/LICENSE +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/Makefile +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/README.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/UPGRADING.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/__init__.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/__init__.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/agent.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/cases.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/evaluators.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/harness.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/lifecycle.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/run.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/examples/send_webhook.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/001a_tests.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/001b_metadata.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/001c_dailies.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/003_trace_test_linking.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/004_codebase_hygiene.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/005_ost_sport_bridge.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/006_output_backends.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/007_account_status_userinfo_portal.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/008_sdk_documentation.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/009_road_to_1_0.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/010_credentials_one_source.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/011_user_agent_on_every_request.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/skills/sweatstack-python/SKILL.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/_docs.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/_renames.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/_transport.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/exceptions.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/access_token_cache.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/dependencies.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/__init__.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/_app_metadata.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/_base.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/activities.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/dailies.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/portal.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/profile.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/teams.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/tests.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/traces.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/users.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/__init__.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/conftest.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_access_token_cache.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_account_status.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_changelog.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_credentials.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_dailies.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_docs_check.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_docstrings.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_exceptions.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_identity.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_longitudinal_mean_max_after.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_metadata.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_output.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_portal.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_public_hygiene.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_public_surface.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_renames.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_segmentation.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_skill.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_sport_ost.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_streamlit_auth.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_teams.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_tests.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_trace_test_linking.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_transport.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_user_agent.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_webhooks.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_write_timezone_validation.py +0 -0
- {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/typing/output_types.py +0 -0
|
@@ -11,6 +11,25 @@ for a coding agent, are in `UPGRADING.md` next to this file and at
|
|
|
11
11
|
https://docs.sweatstack.no/learn/python/upgrading/.
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
## [0.96.0] - 2026-10-16
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `client.oauth.authorization_url()` takes `nonce`, `integration` and `email_auth_mode`, the
|
|
19
|
+
authorization parameters it was missing. `integration="garmin_connect"` sends the user straight
|
|
20
|
+
to Garmin, skipping the sign-in chooser.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- No sign-in link sends `prompt` any more: `client.oauth.authorization_url()` defaults to
|
|
25
|
+
`prompt=None` instead of `"none"`, and the FastAPI plugin and the Streamlit helper leave it out.
|
|
26
|
+
Users see what they saw before: SweatStack skips the consent screen by itself when the user's
|
|
27
|
+
grant already covers the request.
|
|
28
|
+
- The Streamlit helper separates scopes with spaces, as the other sign-in links do.
|
|
29
|
+
- `StreamlitAuth.get_authorization_url()` raises `ValueError` without a client ID or redirect
|
|
30
|
+
URI, instead of building a link that fails on the server.
|
|
31
|
+
|
|
32
|
+
|
|
14
33
|
## [0.95.0] - 2026-10-13
|
|
15
34
|
|
|
16
35
|
Scopes are categories now. Request what your app uses. Breaking: the default scopes change
|
|
@@ -107,7 +107,7 @@ client.portal.sessions.create("manage-integrations", return_url=None) # user to
|
|
|
107
107
|
|
|
108
108
|
client.oauth.userinfo() # OpenID claims + issue (needs openid; profile and email add the name and email claims)
|
|
109
109
|
verifier, challenge = client.oauth.generate_pkce_params()
|
|
110
|
-
client.oauth.authorization_url(client_id="YOUR_CLIENT_ID", redirect_uri="http://localhost:8000/callback", code_challenge=challenge)
|
|
110
|
+
client.oauth.authorization_url(client_id="YOUR_CLIENT_ID", redirect_uri="http://localhost:8000/callback", code_challenge=challenge) # + scope, state, nonce, integration="garmin_connect", email_auth_mode; leave prompt out
|
|
111
111
|
client.oauth.exchange_code("code", client_id="YOUR_CLIENT_ID", code_verifier=verifier) # persist=True saves on this machine; never in a server
|
|
112
112
|
```
|
|
113
113
|
|
|
@@ -363,7 +363,6 @@ def field_types(model: type[BaseModel]) -> dict[str, FieldType]:
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
|
|
366
|
-
_DATETIME_FORMATS = frozenset({"date-time", "naive-date-time"})
|
|
367
366
|
_STRING_FORMATS = {
|
|
368
367
|
"date-time": "datetime",
|
|
369
368
|
"naive-date-time": "naive_datetime",
|
|
@@ -392,8 +391,6 @@ def _field_type(node: dict, definitions: dict, path: str, stack: tuple[str, ...]
|
|
|
392
391
|
return _field_type(options[0], definitions, path, stack)
|
|
393
392
|
if options and all(option.get("type") in ("integer", "number") for option in options):
|
|
394
393
|
return "float"
|
|
395
|
-
if options and all(option.get("format") in _DATETIME_FORMATS for option in options):
|
|
396
|
-
return "datetime" # aware | naive: naive values are taken as UTC
|
|
397
394
|
return _fallback(path, node)
|
|
398
395
|
|
|
399
396
|
if "enum" in node or "const" in node:
|
|
@@ -58,18 +58,14 @@ def _bind_sport_to_ost(path: Path) -> None:
|
|
|
58
58
|
path.write_text("".join(out))
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
# Fields the server sends without a UTC offset, though the OpenAPI schema says date-time.
|
|
62
|
-
_NAIVE_OR_AWARE = {"registered_at", "backfill_loaded_until"}
|
|
63
|
-
|
|
64
|
-
|
|
65
61
|
def _restore_naive_local_datetimes(path: Path) -> None:
|
|
66
62
|
"""Type local timestamps as ``NaiveDatetime`` rather than ``AwareDatetime``.
|
|
67
63
|
|
|
68
64
|
The API returns *local* timestamps without a timezone, but datamodel-codegen types every
|
|
69
65
|
``date-time`` field as ``AwareDatetime`` -- which rejects a naive value. Retype the local fields
|
|
70
|
-
(those whose name ends in ``_local``) back to ``NaiveDatetime
|
|
71
|
-
|
|
72
|
-
manual fixups this file has needed in the past).
|
|
66
|
+
(those whose name ends in ``_local``) back to ``NaiveDatetime``; every other timestamp the server
|
|
67
|
+
returns is UTC with ``Z``. AST-anchored and idempotent, so it survives regeneration (and replaces
|
|
68
|
+
the manual fixups this file has needed in the past).
|
|
73
69
|
"""
|
|
74
70
|
src = path.read_text()
|
|
75
71
|
tree = ast.parse(src)
|
|
@@ -83,12 +79,9 @@ def _restore_naive_local_datetimes(path: Path) -> None:
|
|
|
83
79
|
annotation = ast.get_source_segment(src, node.annotation)
|
|
84
80
|
if annotation is None:
|
|
85
81
|
continue
|
|
86
|
-
if name.endswith("_local") and "AwareDatetime" in annotation:
|
|
87
|
-
retyped = annotation.replace("AwareDatetime", "NaiveDatetime")
|
|
88
|
-
elif name in _NAIVE_OR_AWARE and "NaiveDatetime" not in annotation:
|
|
89
|
-
retyped = annotation.replace("AwareDatetime", "AwareDatetime | NaiveDatetime", 1)
|
|
90
|
-
else:
|
|
82
|
+
if not (name.endswith("_local") and "AwareDatetime" in annotation):
|
|
91
83
|
continue
|
|
84
|
+
retyped = annotation.replace("AwareDatetime", "NaiveDatetime")
|
|
92
85
|
i = node.lineno - 1
|
|
93
86
|
replace[i] = lines[i].replace(annotation, retyped, 1)
|
|
94
87
|
|
|
@@ -6,7 +6,7 @@ import base64
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
8
|
import secrets
|
|
9
|
-
from urllib.parse import
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
|
|
@@ -15,6 +15,7 @@ from fastapi.responses import RedirectResponse
|
|
|
15
15
|
from .. import _transport
|
|
16
16
|
from ..constants import DEFAULT_URL
|
|
17
17
|
from ..exceptions import SweatStackAPIError, SweatStackConnectionError
|
|
18
|
+
from ..resources.oauth import _build_authorization_url
|
|
18
19
|
from ..utils import decode_jwt_body
|
|
19
20
|
from .config import get_config
|
|
20
21
|
from .dependencies import _extract_expiry
|
|
@@ -162,14 +163,13 @@ def create_router() -> APIRouter:
|
|
|
162
163
|
state = create_state(validated_next)
|
|
163
164
|
|
|
164
165
|
# Build authorization URL
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
auth_url = f"{DEFAULT_URL}/oauth/authorize?{urlencode(params)}"
|
|
166
|
+
auth_url = _build_authorization_url(
|
|
167
|
+
DEFAULT_URL,
|
|
168
|
+
client_id=config.client_id,
|
|
169
|
+
redirect_uri=config.redirect_uri,
|
|
170
|
+
scope=" ".join(config.scopes),
|
|
171
|
+
state=state,
|
|
172
|
+
)
|
|
173
173
|
|
|
174
174
|
# Set state cookie and redirect
|
|
175
175
|
response = RedirectResponse(url=auth_url, status_code=302)
|