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.
Files changed (114) hide show
  1. {sweatstack-0.95.0 → sweatstack-0.96.0}/CHANGELOG.md +19 -0
  2. {sweatstack-0.95.0 → sweatstack-0.96.0}/PKG-INFO +1 -1
  3. {sweatstack-0.95.0 → sweatstack-0.96.0}/pyproject.toml +1 -1
  4. {sweatstack-0.95.0 → sweatstack-0.96.0}/skills/sweatstack-python/api.md +1 -1
  5. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/_frames.py +0 -3
  6. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/cli.py +5 -12
  7. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/client.py +0 -1
  8. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/routes.py +9 -9
  9. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/openapi_schemas.py +22 -453
  10. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/oauth.py +54 -12
  11. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/streamlit.py +14 -12
  12. sweatstack-0.96.0/tests/test_authorization_url.py +103 -0
  13. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_frames.py +0 -21
  14. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_resources.py +2 -3
  15. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_wire_names.py +1 -0
  16. {sweatstack-0.95.0 → sweatstack-0.96.0}/uv.lock +1 -1
  17. {sweatstack-0.95.0 → sweatstack-0.96.0}/.git-blame-ignore-revs +0 -0
  18. {sweatstack-0.95.0 → sweatstack-0.96.0}/.github/workflows/ci.yml +0 -0
  19. {sweatstack-0.95.0 → sweatstack-0.96.0}/.gitignore +0 -0
  20. {sweatstack-0.95.0 → sweatstack-0.96.0}/.gitleaks.toml +0 -0
  21. {sweatstack-0.95.0 → sweatstack-0.96.0}/.python-version +0 -0
  22. {sweatstack-0.95.0 → sweatstack-0.96.0}/AGENTS.md +0 -0
  23. {sweatstack-0.95.0 → sweatstack-0.96.0}/CONTRIBUTING.md +0 -0
  24. {sweatstack-0.95.0 → sweatstack-0.96.0}/DEVELOPMENT.md +0 -0
  25. {sweatstack-0.95.0 → sweatstack-0.96.0}/LICENSE +0 -0
  26. {sweatstack-0.95.0 → sweatstack-0.96.0}/Makefile +0 -0
  27. {sweatstack-0.95.0 → sweatstack-0.96.0}/README.md +0 -0
  28. {sweatstack-0.95.0 → sweatstack-0.96.0}/UPGRADING.md +0 -0
  29. {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/__init__.py +0 -0
  30. {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/__init__.py +0 -0
  31. {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/agent.py +0 -0
  32. {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/cases.py +0 -0
  33. {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/evaluators.py +0 -0
  34. {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/harness.py +0 -0
  35. {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/lifecycle.py +0 -0
  36. {sweatstack-0.95.0 → sweatstack-0.96.0}/evals/agent/run.py +0 -0
  37. {sweatstack-0.95.0 → sweatstack-0.96.0}/examples/fastapi_webhooks_example.py +0 -0
  38. {sweatstack-0.95.0 → sweatstack-0.96.0}/examples/send_webhook.py +0 -0
  39. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/001a_tests.md +0 -0
  40. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/001b_metadata.md +0 -0
  41. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/001c_dailies.md +0 -0
  42. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
  43. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/003_trace_test_linking.md +0 -0
  44. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/004_codebase_hygiene.md +0 -0
  45. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/005_ost_sport_bridge.md +0 -0
  46. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/006_output_backends.md +0 -0
  47. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/007_account_status_userinfo_portal.md +0 -0
  48. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/008_sdk_documentation.md +0 -0
  49. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/009_road_to_1_0.md +0 -0
  50. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/010_credentials_one_source.md +0 -0
  51. {sweatstack-0.95.0 → sweatstack-0.96.0}/plans/011_user_agent_on_every_request.md +0 -0
  52. {sweatstack-0.95.0 → sweatstack-0.96.0}/skills/sweatstack-python/SKILL.md +0 -0
  53. {sweatstack-0.95.0 → sweatstack-0.96.0}/skills/sweatstack-python/data-models.md +0 -0
  54. {sweatstack-0.95.0 → sweatstack-0.96.0}/skills/sweatstack-python/fastapi.md +0 -0
  55. {sweatstack-0.95.0 → sweatstack-0.96.0}/skills/sweatstack-python/streamlit.md +0 -0
  56. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/__init__.py +0 -0
  57. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/_docs.py +0 -0
  58. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/_renames.py +0 -0
  59. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/_transport.py +0 -0
  60. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/constants.py +0 -0
  61. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/exceptions.py +0 -0
  62. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/__init__.py +0 -0
  63. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/access_token_cache.py +0 -0
  64. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/config.py +0 -0
  65. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/dependencies.py +0 -0
  66. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/models.py +0 -0
  67. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/session.py +0 -0
  68. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/token_stores.py +0 -0
  69. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/fastapi/webhooks.py +0 -0
  70. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/py.typed +0 -0
  71. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/__init__.py +0 -0
  72. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/_app_metadata.py +0 -0
  73. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/_base.py +0 -0
  74. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/activities.py +0 -0
  75. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/dailies.py +0 -0
  76. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/portal.py +0 -0
  77. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/profile.py +0 -0
  78. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/teams.py +0 -0
  79. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/tests.py +0 -0
  80. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/traces.py +0 -0
  81. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/resources/users.py +0 -0
  82. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/schemas.py +0 -0
  83. {sweatstack-0.95.0 → sweatstack-0.96.0}/src/sweatstack/utils.py +0 -0
  84. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/__init__.py +0 -0
  85. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/conftest.py +0 -0
  86. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_access_token_cache.py +0 -0
  87. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_account_status.py +0 -0
  88. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_changelog.py +0 -0
  89. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_credentials.py +0 -0
  90. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_dailies.py +0 -0
  91. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_docs_check.py +0 -0
  92. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_docstrings.py +0 -0
  93. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_dtype_conversion.py +0 -0
  94. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_exceptions.py +0 -0
  95. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_identity.py +0 -0
  96. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_longitudinal_mean_max_after.py +0 -0
  97. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_metadata.py +0 -0
  98. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_output.py +0 -0
  99. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_portal.py +0 -0
  100. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_public_hygiene.py +0 -0
  101. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_public_surface.py +0 -0
  102. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_renames.py +0 -0
  103. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_segmentation.py +0 -0
  104. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_skill.py +0 -0
  105. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_sport_ost.py +0 -0
  106. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_streamlit_auth.py +0 -0
  107. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_teams.py +0 -0
  108. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_tests.py +0 -0
  109. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_trace_test_linking.py +0 -0
  110. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_transport.py +0 -0
  111. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_user_agent.py +0 -0
  112. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_webhooks.py +0 -0
  113. {sweatstack-0.95.0 → sweatstack-0.96.0}/tests/test_write_timezone_validation.py +0 -0
  114. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sweatstack
3
- Version: 0.95.0
3
+ Version: 0.96.0
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/learn/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.95.0"
3
+ version = "0.96.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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``, and let the fields the server
71
- sends without an offset (``registered_at``, ``backfill_loaded_until``) accept either. AST-anchored and idempotent, so it survives regeneration (and replaces the
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
 
@@ -642,7 +642,6 @@ class Client:
642
642
  redirect_uri=f"http://localhost:{port}",
643
643
  code_challenge=code_challenge,
644
644
  scope=DEFAULT_SCOPE,
645
- prompt=None,
646
645
  )
647
646
 
648
647
  webbrowser.open(authorization_url)
@@ -6,7 +6,7 @@ import base64
6
6
  import json
7
7
  import logging
8
8
  import secrets
9
- from urllib.parse import urlencode, urlparse
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
- params = {
166
- "client_id": config.client_id,
167
- "redirect_uri": config.redirect_uri,
168
- "scope": " ".join(config.scopes),
169
- "state": state,
170
- "prompt": "none",
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)