sweatstack 0.96.0__tar.gz → 0.97.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.96.0 → sweatstack-0.97.0}/.github/workflows/ci.yml +1 -1
  2. {sweatstack-0.96.0 → sweatstack-0.97.0}/AGENTS.md +4 -4
  3. {sweatstack-0.96.0 → sweatstack-0.97.0}/CHANGELOG.md +14 -0
  4. {sweatstack-0.96.0 → sweatstack-0.97.0}/DEVELOPMENT.md +1 -1
  5. {sweatstack-0.96.0 → sweatstack-0.97.0}/PKG-INFO +3 -4
  6. {sweatstack-0.96.0 → sweatstack-0.97.0}/examples/send_webhook.py +8 -8
  7. {sweatstack-0.96.0 → sweatstack-0.97.0}/pyproject.toml +4 -5
  8. {sweatstack-0.96.0 → sweatstack-0.97.0}/skills/sweatstack-python/SKILL.md +1 -1
  9. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/_frames.py +3 -3
  10. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/_transport.py +16 -16
  11. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/cli.py +2 -2
  12. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/client.py +9 -9
  13. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/exceptions.py +1 -1
  14. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/dependencies.py +4 -4
  15. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/routes.py +3 -3
  16. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/streamlit.py +2 -2
  17. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_account_status.py +3 -3
  18. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_exceptions.py +7 -7
  19. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_frames.py +2 -2
  20. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_identity.py +3 -3
  21. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_portal.py +3 -3
  22. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_resources.py +1 -1
  23. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_tests.py +5 -5
  24. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_trace_test_linking.py +4 -4
  25. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_transport.py +44 -22
  26. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_user_agent.py +10 -10
  27. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_webhooks.py +10 -10
  28. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_write_timezone_validation.py +2 -2
  29. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/typing/output_types.py +1 -1
  30. {sweatstack-0.96.0 → sweatstack-0.97.0}/uv.lock +70 -508
  31. {sweatstack-0.96.0 → sweatstack-0.97.0}/.git-blame-ignore-revs +0 -0
  32. {sweatstack-0.96.0 → sweatstack-0.97.0}/.gitignore +0 -0
  33. {sweatstack-0.96.0 → sweatstack-0.97.0}/.gitleaks.toml +0 -0
  34. {sweatstack-0.96.0 → sweatstack-0.97.0}/.python-version +0 -0
  35. {sweatstack-0.96.0 → sweatstack-0.97.0}/CONTRIBUTING.md +0 -0
  36. {sweatstack-0.96.0 → sweatstack-0.97.0}/LICENSE +0 -0
  37. {sweatstack-0.96.0 → sweatstack-0.97.0}/Makefile +0 -0
  38. {sweatstack-0.96.0 → sweatstack-0.97.0}/README.md +0 -0
  39. {sweatstack-0.96.0 → sweatstack-0.97.0}/UPGRADING.md +0 -0
  40. {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/__init__.py +0 -0
  41. {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/__init__.py +0 -0
  42. {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/agent.py +0 -0
  43. {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/cases.py +0 -0
  44. {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/evaluators.py +0 -0
  45. {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/harness.py +0 -0
  46. {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/lifecycle.py +0 -0
  47. {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/run.py +0 -0
  48. {sweatstack-0.96.0 → sweatstack-0.97.0}/examples/fastapi_webhooks_example.py +0 -0
  49. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/001a_tests.md +0 -0
  50. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/001b_metadata.md +0 -0
  51. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/001c_dailies.md +0 -0
  52. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
  53. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/003_trace_test_linking.md +0 -0
  54. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/004_codebase_hygiene.md +0 -0
  55. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/005_ost_sport_bridge.md +0 -0
  56. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/006_output_backends.md +0 -0
  57. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/007_account_status_userinfo_portal.md +0 -0
  58. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/008_sdk_documentation.md +0 -0
  59. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/009_road_to_1_0.md +0 -0
  60. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/010_credentials_one_source.md +0 -0
  61. {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/011_user_agent_on_every_request.md +0 -0
  62. {sweatstack-0.96.0 → sweatstack-0.97.0}/skills/sweatstack-python/api.md +0 -0
  63. {sweatstack-0.96.0 → sweatstack-0.97.0}/skills/sweatstack-python/data-models.md +0 -0
  64. {sweatstack-0.96.0 → sweatstack-0.97.0}/skills/sweatstack-python/fastapi.md +0 -0
  65. {sweatstack-0.96.0 → sweatstack-0.97.0}/skills/sweatstack-python/streamlit.md +0 -0
  66. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/__init__.py +0 -0
  67. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/_docs.py +0 -0
  68. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/_renames.py +0 -0
  69. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/constants.py +0 -0
  70. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/__init__.py +0 -0
  71. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/access_token_cache.py +0 -0
  72. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/config.py +0 -0
  73. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/models.py +0 -0
  74. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/session.py +0 -0
  75. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/token_stores.py +0 -0
  76. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/webhooks.py +0 -0
  77. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/openapi_schemas.py +0 -0
  78. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/py.typed +0 -0
  79. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/__init__.py +0 -0
  80. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/_app_metadata.py +0 -0
  81. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/_base.py +0 -0
  82. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/activities.py +0 -0
  83. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/dailies.py +0 -0
  84. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/oauth.py +0 -0
  85. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/portal.py +0 -0
  86. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/profile.py +0 -0
  87. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/teams.py +0 -0
  88. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/tests.py +0 -0
  89. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/traces.py +0 -0
  90. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/users.py +0 -0
  91. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/schemas.py +0 -0
  92. {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/utils.py +0 -0
  93. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/__init__.py +0 -0
  94. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/conftest.py +0 -0
  95. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_access_token_cache.py +0 -0
  96. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_authorization_url.py +0 -0
  97. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_changelog.py +0 -0
  98. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_credentials.py +0 -0
  99. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_dailies.py +0 -0
  100. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_docs_check.py +0 -0
  101. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_docstrings.py +0 -0
  102. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_dtype_conversion.py +0 -0
  103. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_longitudinal_mean_max_after.py +0 -0
  104. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_metadata.py +0 -0
  105. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_output.py +0 -0
  106. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_public_hygiene.py +0 -0
  107. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_public_surface.py +0 -0
  108. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_renames.py +0 -0
  109. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_segmentation.py +0 -0
  110. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_skill.py +0 -0
  111. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_sport_ost.py +0 -0
  112. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_streamlit_auth.py +0 -0
  113. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_teams.py +0 -0
  114. {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_wire_names.py +0 -0
@@ -12,7 +12,7 @@ jobs:
12
12
  strategy:
13
13
  fail-fast: false
14
14
  matrix:
15
- python: ["3.10", "3.11", "3.12", "3.13"]
15
+ python: ["3.11", "3.12", "3.13"]
16
16
  include:
17
17
  # The lock file tracks the newest pandas; this leg keeps the declared
18
18
  # lower bound (pyproject `pandas>=2.2.3`) honest.
@@ -87,7 +87,7 @@ src/sweatstack/
87
87
  ├── _frames.py # output= backends: parquet/models -> pandas/polars/arrow/bytes.
88
88
  ├── _renames.py # Removed names -> replacement hints. Permanent; see "Breaking changes".
89
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.
90
+ ├── exceptions.py # Public error contract. No HTTP-library types leak.
91
91
  ├── client.py # Client: config, auth, delegation, transport (_request), resource attributes,
92
92
  │ # and the module-level interface (one shared default client).
93
93
  ├── resources/ # One module per URL segment: activities.py, traces.py, tests.py, ...
@@ -233,8 +233,8 @@ Both kinds take `output` as a keyword-only parameter and carry `@overload` stubs
233
233
  `Literal` values (clone the neighbours); `tests/typing/output_types.py` pins them.
234
234
 
235
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"`.
236
+ ones go through `self._wire(value)` (R8). Always pass `.value` for enums into `httpx2` params:
237
+ `httpx2` calls `str()`, which gives `"TraceResolution.linked"`, not `"linked"`.
238
238
 
239
239
 
240
240
  ## Full-replace `replace` methods
@@ -309,7 +309,7 @@ construct fails the suite, not a user's notebook.
309
309
  ## Exceptions
310
310
 
311
311
  The hierarchy in `sweatstack/exceptions.py` is the **public** error
312
- contract — consumers should never need to import `httpx`.
312
+ contract — consumers should never need to import `httpx2`.
313
313
 
314
314
  ```
315
315
  SweatStackError
@@ -11,6 +11,20 @@ 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.97.0] - 2026-10-22
15
+
16
+ ### Changed
17
+
18
+ - Requires Python 3.11 or later. On Python 3.10, pip and uv keep installing 0.96.
19
+ - The SDK sends its requests with [`httpx2`](https://github.com/pydantic/httpx2), the maintained
20
+ continuation of `httpx`. Calling the SDK is unchanged, and errors are still the `SweatStack*`
21
+ exceptions.
22
+ - HTTPS certificates are checked against the operating system's trust store instead of the
23
+ `certifi` bundle that `httpx` shipped. A root certificate installed on the machine, such as a
24
+ company proxy's, is now trusted. A Linux image without a CA bundle (the `ca-certificates`
25
+ package) can no longer connect; install it, or point `SSL_CERT_FILE` at a bundle.
26
+
27
+
14
28
  ## [0.96.0] - 2026-10-16
15
29
 
16
30
  ### Added
@@ -17,7 +17,7 @@ uv run python -c "..." # ad-hoc scripts
17
17
  uv run generate-response-models # regenerate OpenAPI schemas (see below)
18
18
  ```
19
19
 
20
- Python ≥ 3.10 is supported; develop against the version pinned in
20
+ Python ≥ 3.11 is supported; develop against the version pinned in
21
21
  `.python-version`.
22
22
 
23
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sweatstack
3
- Version: 0.96.0
3
+ Version: 0.97.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/
@@ -16,14 +16,13 @@ Classifier: Development Status :: 4 - Beta
16
16
  Classifier: Intended Audience :: Developers
17
17
  Classifier: License :: OSI Approved :: MIT License
18
18
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
22
21
  Classifier: Programming Language :: Python :: 3.13
23
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
- Requires-Python: >=3.10
23
+ Requires-Python: >=3.11
25
24
  Requires-Dist: email-validator>=2.2.0
26
- Requires-Dist: httpx>=0.28.1
25
+ Requires-Dist: httpx2>=2.16.0
27
26
  Requires-Dist: open-sport-taxonomy[pydantic]>=0.10.0
28
27
  Requires-Dist: platformdirs>=4.0.0
29
28
  Requires-Dist: pydantic>=2.10.5
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
3
  # dependencies = [
4
- # "httpx",
4
+ # "httpx2",
5
5
  # ]
6
6
  # ///
7
7
  """
@@ -32,9 +32,9 @@ import hmac
32
32
  import json
33
33
  import sys
34
34
  import time
35
- from datetime import datetime, timezone
35
+ from datetime import UTC, datetime
36
36
 
37
- import httpx
37
+ import httpx2
38
38
 
39
39
 
40
40
  def create_signature(payload: bytes, secret: str, timestamp: int) -> str:
@@ -55,14 +55,14 @@ def send_webhook(
55
55
  resource_id: str,
56
56
  secret: str,
57
57
  verbose: bool = False,
58
- ) -> httpx.Response:
58
+ ) -> httpx2.Response:
59
59
  """Send a webhook to the specified URL."""
60
60
  # Create payload
61
61
  payload = {
62
62
  "user_id": user_id,
63
63
  "event_type": event_type,
64
64
  "resource_id": resource_id,
65
- "timestamp": datetime.now(timezone.utc).isoformat(),
65
+ "timestamp": datetime.now(UTC).isoformat(),
66
66
  }
67
67
 
68
68
  payload_bytes = json.dumps(payload).encode()
@@ -79,7 +79,7 @@ def send_webhook(
79
79
  print(f"{'=' * 60}\n")
80
80
 
81
81
  # Send request
82
- response = httpx.post(
82
+ response = httpx2.post(
83
83
  url,
84
84
  content=payload_bytes,
85
85
  headers={
@@ -176,10 +176,10 @@ Examples:
176
176
  if args.verbose and response.headers:
177
177
  print(f" Response headers: {dict(response.headers)}")
178
178
 
179
- except httpx.ConnectError:
179
+ except httpx2.ConnectError:
180
180
  print(f"✗ Connection failed - is the server running at {args.url}?")
181
181
  sys.exit(1)
182
- except httpx.TimeoutException:
182
+ except httpx2.TimeoutException:
183
183
  print("✗ Request timed out")
184
184
  sys.exit(1)
185
185
  except Exception as e:
@@ -1,20 +1,19 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.96.0"
3
+ version = "0.97.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  authors = [
8
8
  { name = "Aart Goossens", email = "aart@sweatstack.no" }
9
9
  ]
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.11"
11
11
  keywords = ["sweatstack", "sports", "fitness", "api", "client"]
12
12
  classifiers = [
13
13
  "Development Status :: 4 - Beta",
14
14
  "Intended Audience :: Developers",
15
15
  "License :: OSI Approved :: MIT License",
16
16
  "Programming Language :: Python :: 3",
17
- "Programming Language :: Python :: 3.10",
18
17
  "Programming Language :: Python :: 3.11",
19
18
  "Programming Language :: Python :: 3.12",
20
19
  "Programming Language :: Python :: 3.13",
@@ -24,7 +23,7 @@ classifiers = [
24
23
  # consumers and anyone who only needs the models. CI's bare-install job keeps it so.
25
24
  dependencies = [
26
25
  "email-validator>=2.2.0",
27
- "httpx>=0.28.1",
26
+ "httpx2>=2.16.0",
28
27
  "open-sport-taxonomy[pydantic]>=0.10.0",
29
28
  "platformdirs>=4.0.0",
30
29
  "pydantic>=2.10.5",
@@ -91,7 +90,7 @@ evals = [
91
90
 
92
91
  [tool.ruff]
93
92
  line-length = 100
94
- target-version = "py310"
93
+ target-version = "py311"
95
94
  extend-exclude = ["src/sweatstack/openapi_schemas.py"]
96
95
 
97
96
  [tool.ruff.lint]
@@ -98,7 +98,7 @@ pandas (`summary.power.mean`) and structs in Polars (`.unnest("summary")`).
98
98
 
99
99
  ## Errors
100
100
 
101
- Everything derives from `SweatStackError`; never import `httpx`. `SweatStackAPIError` (has
101
+ Everything derives from `SweatStackError`; never import `httpx2`. `SweatStackAPIError` (has
102
102
  `status_code`, `url`, `method`, `request_id`, `body`) splits into `SweatStackAuthError` (401,
103
103
  403), `SweatStackNotFoundError` (404), `SweatStackRateLimitError` (429, `retry_after`),
104
104
  `SweatStackBadRequestError` (other 4xx) and `SweatStackServerError` (5xx). Also
@@ -22,7 +22,7 @@ from __future__ import annotations
22
22
  import json
23
23
  import warnings
24
24
  from collections.abc import Sequence
25
- from datetime import datetime, timezone
25
+ from datetime import UTC, datetime
26
26
  from enum import Enum
27
27
  from functools import cache
28
28
  from importlib import import_module
@@ -489,7 +489,7 @@ def _plain_value(value: Any, node: FieldType) -> Any:
489
489
  if node == "string":
490
490
  return value if isinstance(value, str) else str(value) # Sport and other value objects
491
491
  if node == "datetime" and isinstance(value, datetime) and value.tzinfo is None:
492
- return value.replace(tzinfo=timezone.utc) # naive value in a UTC column
492
+ return value.replace(tzinfo=UTC) # naive value in a UTC column
493
493
  if node == "naive_datetime" and isinstance(value, datetime) and value.tzinfo is not None:
494
- return value.astimezone(timezone.utc).replace(tzinfo=None)
494
+ return value.astimezone(UTC).replace(tzinfo=None)
495
495
  return value # date, timedelta, bool, int, float, datetime already matching
@@ -4,7 +4,7 @@ The policy follows the OpenAI, Anthropic and Stripe clients, with one difference
4
4
  never retried, because the SweatStack API has no idempotency keys and a retried create could
5
5
  create twice.
6
6
 
7
- This module is also the only place in the package that calls ``httpx`` to make a request:
7
+ This module is also the only place in the package that calls ``httpx2`` to make a request:
8
8
  through a client's pool, or through :func:`post` for the token calls the FastAPI and Streamlit
9
9
  integrations make without a client. Both put the SDK's ``User-Agent`` on the wire, and
10
10
  ``tests/test_transport.py`` fails on a request made from anywhere else.
@@ -17,7 +17,7 @@ from collections.abc import Iterator
17
17
  from contextlib import contextmanager
18
18
  from typing import Any
19
19
 
20
- import httpx
20
+ import httpx2
21
21
 
22
22
  from .constants import USER_AGENT
23
23
 
@@ -42,7 +42,7 @@ def backoff(attempt: int) -> float:
42
42
  return min(0.5 * 2**attempt * random.uniform(0.75, 1.25), MAX_BACKOFF)
43
43
 
44
44
 
45
- def retry_after(response: httpx.Response) -> float | None:
45
+ def retry_after(response: httpx2.Response) -> float | None:
46
46
  """The response's ``Retry-After`` in seconds, if it sent one as a number."""
47
47
  try:
48
48
  return float(response.headers["retry-after"])
@@ -55,7 +55,7 @@ def wait_before_retry(
55
55
  attempt: int,
56
56
  max_retries: int,
57
57
  waited: float,
58
- response: httpx.Response | None = None,
58
+ response: httpx2.Response | None = None,
59
59
  ) -> float | None:
60
60
  """How long to wait before retrying, or ``None`` to stop.
61
61
 
@@ -80,16 +80,16 @@ def wait_before_retry(
80
80
  return wait
81
81
 
82
82
 
83
- def post(url: str, **kwargs: Any) -> httpx.Response:
84
- """``httpx.post`` with the SDK's ``User-Agent``; the caller's headers win on a clash.
83
+ def post(url: str, **kwargs: Any) -> httpx2.Response:
84
+ """``httpx2.post`` with the SDK's ``User-Agent``; the caller's headers win on a clash.
85
85
 
86
86
  For the one-off token calls the integrations make without a ``Client``. It adds the header
87
87
  and nothing else: no retry (a ``POST`` is never retried), no error translation and no
88
88
  timeout, so each caller keeps its own.
89
89
  """
90
- headers = httpx.Headers({"User-Agent": USER_AGENT})
90
+ headers = httpx2.Headers({"User-Agent": USER_AGENT})
91
91
  headers.update(kwargs.pop("headers", None) or {})
92
- return httpx.post(url, headers=headers, **kwargs)
92
+ return httpx2.post(url, headers=headers, **kwargs)
93
93
 
94
94
 
95
95
  class Session:
@@ -100,29 +100,29 @@ class Session:
100
100
  endpoints must never see it.
101
101
  """
102
102
 
103
- def __init__(self, http: httpx.Client, headers: httpx.Headers, timeout: float) -> None:
103
+ def __init__(self, http: httpx2.Client, headers: httpx2.Headers, timeout: float) -> None:
104
104
  self._http = http
105
105
  self.headers = headers
106
106
  self._timeout = timeout
107
107
 
108
- def request(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
108
+ def request(self, method: str, url: str, **kwargs: Any) -> httpx2.Response:
109
109
  return self._http.request(method, url, headers=self.headers, **kwargs)
110
110
 
111
- def get(self, url: str, **kwargs: Any) -> httpx.Response:
111
+ def get(self, url: str, **kwargs: Any) -> httpx2.Response:
112
112
  return self.request("GET", url, **kwargs)
113
113
 
114
- def post(self, url: str, **kwargs: Any) -> httpx.Response:
114
+ def post(self, url: str, **kwargs: Any) -> httpx2.Response:
115
115
  return self.request("POST", url, **kwargs)
116
116
 
117
- def put(self, url: str, **kwargs: Any) -> httpx.Response:
117
+ def put(self, url: str, **kwargs: Any) -> httpx2.Response:
118
118
  return self.request("PUT", url, **kwargs)
119
119
 
120
- def delete(self, url: str, **kwargs: Any) -> httpx.Response:
120
+ def delete(self, url: str, **kwargs: Any) -> httpx2.Response:
121
121
  return self.request("DELETE", url, **kwargs)
122
122
 
123
123
  @contextmanager
124
- def stream(self, method: str, url: str, **kwargs: Any) -> Iterator[httpx.Response]:
124
+ def stream(self, method: str, url: str, **kwargs: Any) -> Iterator[httpx2.Response]:
125
125
  """A streaming request. No read timeout: a stream may stay quiet for a while."""
126
- timeout = httpx.Timeout(self._timeout, read=None)
126
+ timeout = httpx2.Timeout(self._timeout, read=None)
127
127
  with self._http.stream(method, url, headers=self.headers, timeout=timeout, **kwargs) as r:
128
128
  yield r
@@ -2,7 +2,7 @@ import ast
2
2
  import re
3
3
  from pathlib import Path
4
4
 
5
- import httpx
5
+ import httpx2
6
6
  from datamodel_code_generator import DataModelType, InputFileType, generate
7
7
 
8
8
 
@@ -95,7 +95,7 @@ def _restore_naive_local_datetimes(path: Path) -> None:
95
95
 
96
96
 
97
97
  def generate_response_models():
98
- response = httpx.get("http://localhost:8080/openapi.json")
98
+ response = httpx2.get("http://localhost:8080/openapi.json")
99
99
  response.raise_for_status()
100
100
  output_directory = Path(__file__).parent
101
101
  output = Path(output_directory / "openapi_schemas.py")
@@ -20,7 +20,7 @@ from pathlib import Path
20
20
  from typing import Any, Literal
21
21
  from urllib.parse import parse_qs, urlparse
22
22
 
23
- import httpx
23
+ import httpx2
24
24
  from platformdirs import user_cache_dir, user_data_dir
25
25
  from pydantic import SecretStr
26
26
 
@@ -875,7 +875,7 @@ class Client:
875
875
  # Transport: the internal API the resources use
876
876
  # -------------------------------------------------------------------------
877
877
 
878
- def _pool(self) -> httpx.Client:
878
+ def _pool(self) -> httpx2.Client:
879
879
  """The client's connection pool, created on first use and kept for its lifetime.
880
880
 
881
881
  Rebuilt if ``url`` changes. Authorization is per request (see ``_http_client``), so the
@@ -885,7 +885,7 @@ class Client:
885
885
  built = self.__dict__.get("_pool_entry")
886
886
  if built is not None and built[0] == url:
887
887
  return built[1]
888
- pool = httpx.Client(
888
+ pool = httpx2.Client(
889
889
  base_url=url,
890
890
  headers={"User-Agent": USER_AGENT},
891
891
  timeout=self.timeout,
@@ -902,7 +902,7 @@ class Client:
902
902
  """A request scope on the client's connection pool, with the right Authorization header.
903
903
 
904
904
  Transport-level errors (DNS, timeouts, connection refused) are re-raised as
905
- SweatStackConnectionError so consumers never see raw httpx types.
905
+ SweatStackConnectionError so consumers never see the HTTP library's types.
906
906
 
907
907
  Args:
908
908
  skip_token_check: Use the raw token without the expiry check (used during
@@ -911,7 +911,7 @@ class Client:
911
911
  For the token exchange, which authenticates with the app's own credentials in
912
912
  the body, so a user's bearer is never sent where it is not needed.
913
913
  """
914
- headers = httpx.Headers({"User-Agent": USER_AGENT})
914
+ headers = httpx2.Headers({"User-Agent": USER_AGENT})
915
915
  if not auth:
916
916
  token = None
917
917
  elif skip_token_check:
@@ -924,7 +924,7 @@ class Client:
924
924
 
925
925
  try:
926
926
  yield _transport.Session(self._pool(), headers, self.timeout)
927
- except httpx.HTTPError as exc:
927
+ except httpx2.HTTPError as exc:
928
928
  raise SweatStackConnectionError(str(exc)) from exc
929
929
 
930
930
  def _request(
@@ -934,7 +934,7 @@ class Client:
934
934
  *,
935
935
  auth: bool = True,
936
936
  **kwargs: Any,
937
- ) -> httpx.Response:
937
+ ) -> httpx2.Response:
938
938
  """Send one request, retrying when safe, and raise the typed exception for an error status.
939
939
 
940
940
  Every resource method that makes a single request goes through here. Retries follow
@@ -981,7 +981,7 @@ class Client:
981
981
  waited += wait
982
982
  attempt += 1
983
983
 
984
- def _raise_for_status(self, response: httpx.Response) -> None:
984
+ def _raise_for_status(self, response: httpx2.Response) -> None:
985
985
  if response.is_success:
986
986
  return
987
987
 
@@ -1008,7 +1008,7 @@ class Client:
1008
1008
  raise SweatStackAPIError(**common)
1009
1009
 
1010
1010
  @staticmethod
1011
- def _parse_error_body(response: httpx.Response) -> dict | str | None:
1011
+ def _parse_error_body(response: httpx2.Response) -> dict | str | None:
1012
1012
  try:
1013
1013
  return response.json()
1014
1014
  except Exception:
@@ -1,7 +1,7 @@
1
1
  """Typed exception hierarchy for the SweatStack client.
2
2
 
3
3
  This module defines all exceptions raised by the SweatStack library.
4
- No httpx types are exposed — this is the public error contract.
4
+ No HTTP-library types are exposed — this is the public error contract.
5
5
  """
6
6
 
7
7
 
@@ -6,11 +6,11 @@ import hashlib
6
6
  import logging
7
7
  import time
8
8
  from dataclasses import dataclass
9
- from datetime import datetime, timezone
9
+ from datetime import UTC, datetime
10
10
  from typing import Annotated, NoReturn
11
11
  from urllib.parse import quote
12
12
 
13
- import httpx
13
+ import httpx2
14
14
  from fastapi import Depends, HTTPException, Request, Response
15
15
 
16
16
  from .. import _transport
@@ -37,7 +37,7 @@ TOKEN_EXPIRY_MARGIN = 5
37
37
  # endpoint is normally sub-second; 10 seconds is generous enough to
38
38
  # absorb a one-off spike but tight enough that a hung server doesn't
39
39
  # pin a threadpool worker for minutes.
40
- REFRESH_HTTP_TIMEOUT = httpx.Timeout(10.0, connect=5.0)
40
+ REFRESH_HTTP_TIMEOUT = httpx2.Timeout(10.0, connect=5.0)
41
41
 
42
42
  # Maximum time to wait for the per-session refresh lock. Set above
43
43
  # ``REFRESH_HTTP_TIMEOUT`` plus a small slack so that under normal
@@ -103,7 +103,7 @@ def _is_token_expiring(token: str) -> bool:
103
103
  def _extract_expiry(access_token: str) -> datetime:
104
104
  """Extract expiry time from JWT access token."""
105
105
  body = decode_jwt_body(access_token)
106
- return datetime.fromtimestamp(body["exp"], tz=timezone.utc)
106
+ return datetime.fromtimestamp(body["exp"], tz=UTC)
107
107
 
108
108
 
109
109
  def _refresh_access_token(
@@ -8,7 +8,7 @@ import logging
8
8
  import secrets
9
9
  from urllib.parse import urlparse
10
10
 
11
- import httpx
11
+ import httpx2
12
12
  from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
13
13
  from fastapi.responses import RedirectResponse
14
14
 
@@ -108,7 +108,7 @@ def _fetch_delegated_token(
108
108
  headers={"Authorization": f"Bearer {principal_tokens.access_token}"},
109
109
  json=body,
110
110
  )
111
- except httpx.HTTPError as exc:
111
+ except httpx2.HTTPError as exc:
112
112
  raise SweatStackConnectionError(str(exc)) from exc
113
113
 
114
114
  if response.status_code == 403:
@@ -222,7 +222,7 @@ def create_router() -> APIRouter:
222
222
  "redirect_uri": config.redirect_uri,
223
223
  },
224
224
  )
225
- except httpx.HTTPError as e:
225
+ except httpx2.HTTPError as e:
226
226
  logger.error("Token exchange error: %s", e)
227
227
  return error_redirect("token_exchange_failed")
228
228
 
@@ -38,7 +38,7 @@ except ImportError:
38
38
  "You can install it with:\n\n"
39
39
  "pip install 'sweatstack[streamlit]'\n\n"
40
40
  ) from None
41
- import httpx
41
+ import httpx2
42
42
  from pydantic import SecretStr
43
43
 
44
44
  from . import _transport
@@ -424,7 +424,7 @@ class StreamlitAuth:
424
424
  "client_secret": self.client_secret,
425
425
  "code": code,
426
426
  }
427
- auth = httpx.BasicAuth(username=self.client_id, password=self.client_secret)
427
+ auth = httpx2.BasicAuth(username=self.client_id, password=self.client_secret)
428
428
  response = _transport.post(
429
429
  f"{DEFAULT_URL}/api/v1/oauth/token",
430
430
  data=token_data,
@@ -8,7 +8,7 @@ temporary: ``action_required`` or ``syncing``, never ``unavailable`` (plan 075).
8
8
 
9
9
  from unittest.mock import MagicMock, patch
10
10
 
11
- import httpx
11
+ import httpx2
12
12
  import polars as pl
13
13
  import pytest
14
14
  from pydantic import ValidationError
@@ -99,8 +99,8 @@ def client():
99
99
 
100
100
 
101
101
  def _http_returning(status_code: int, payload) -> MagicMock:
102
- request = httpx.Request("GET", "https://test.sweatstack.no/api/v1/profile/status")
103
- response = httpx.Response(status_code, json=payload, request=request)
102
+ request = httpx2.Request("GET", "https://test.sweatstack.no/api/v1/profile/status")
103
+ response = httpx2.Response(status_code, json=payload, request=request)
104
104
  http = MagicMock()
105
105
  http.__enter__ = MagicMock(return_value=http)
106
106
  http.__exit__ = MagicMock(return_value=False)
@@ -8,7 +8,7 @@ Covers:
8
8
  - Transport error wrapping
9
9
  """
10
10
 
11
- import httpx
11
+ import httpx2
12
12
  import pytest
13
13
 
14
14
  from sweatstack import Client
@@ -146,9 +146,9 @@ class TestFormatting:
146
146
 
147
147
 
148
148
  def _make_response(status_code, headers=None, json_body=None, text_body=""):
149
- """Create a mock httpx.Response for testing _raise_for_status."""
150
- request = httpx.Request("GET", "https://api.sweatstack.no/api/v1/test")
151
- response = httpx.Response(
149
+ """Create a mock httpx2.Response for testing _raise_for_status."""
150
+ request = httpx2.Request("GET", "https://api.sweatstack.no/api/v1/test")
151
+ response = httpx2.Response(
152
152
  status_code=status_code,
153
153
  request=request,
154
154
  headers=headers or {},
@@ -280,7 +280,7 @@ class TestTransportErrors:
280
280
 
281
281
  with pytest.raises(SweatStackConnectionError) as exc_info:
282
282
  with client._http_client(skip_token_check=True):
283
- raise httpx.ConnectError("Connection refused")
283
+ raise httpx2.ConnectError("Connection refused")
284
284
 
285
285
  assert "Connection refused" in str(exc_info.value)
286
286
 
@@ -293,7 +293,7 @@ class TestTransportErrors:
293
293
 
294
294
  with pytest.raises(SweatStackConnectionError):
295
295
  with client._http_client(skip_token_check=True):
296
- raise httpx.ReadTimeout("Read timed out")
296
+ raise httpx2.ReadTimeout("Read timed out")
297
297
 
298
298
  def test_connection_error_preserves_cause(self):
299
299
  client = Client.__new__(Client)
@@ -302,7 +302,7 @@ class TestTransportErrors:
302
302
  client._client_secret = None
303
303
  client.streamlit_compatible = False
304
304
 
305
- original = httpx.ConnectError("DNS resolution failed")
305
+ original = httpx2.ConnectError("DNS resolution failed")
306
306
  with pytest.raises(SweatStackConnectionError) as exc_info:
307
307
  with client._http_client(skip_token_check=True):
308
308
  raise original
@@ -7,7 +7,7 @@ are the regen guard: any construct the mapper does not know raises here (via
7
7
 
8
8
  import json
9
9
  import warnings
10
- from datetime import date, datetime, timedelta, timezone
10
+ from datetime import UTC, date, datetime, timedelta
11
11
  from inspect import isclass
12
12
 
13
13
  import polars as pl
@@ -138,7 +138,7 @@ class TestModelsToPolars:
138
138
 
139
139
  assert df.shape == (2, len(ActivitySummary.model_fields))
140
140
  assert df["sport"].to_list() == ["cycling.road", "cycling.road"]
141
- assert df["start"].to_list() == [datetime(2025, 6, 1, 8, tzinfo=timezone.utc)] * 2
141
+ assert df["start"].to_list() == [datetime(2025, 6, 1, 8, tzinfo=UTC)] * 2
142
142
  assert df["start_local"].to_list() == [datetime(2025, 6, 1, 10)] * 2
143
143
  assert df["duration"].to_list() == [timedelta(hours=2)] * 2
144
144
  assert df["metrics"].to_list() == [["power", "heart_rate"], ["speed"]]
@@ -4,7 +4,7 @@ import base64
4
4
  import json
5
5
  from unittest.mock import MagicMock, patch
6
6
 
7
- import httpx
7
+ import httpx2
8
8
  import pytest
9
9
  from pydantic import SecretStr
10
10
 
@@ -60,7 +60,7 @@ class TestWhoami:
60
60
 
61
61
  class TestUserinfo:
62
62
  def test_issue_is_exposed(self, client):
63
- request = httpx.Request("GET", "https://test.sweatstack.no/api/v1/oauth/userinfo")
63
+ request = httpx2.Request("GET", "https://test.sweatstack.no/api/v1/oauth/userinfo")
64
64
  payload = {
65
65
  "sub": "01JUSER",
66
66
  "name": "A",
@@ -75,7 +75,7 @@ class TestUserinfo:
75
75
  http = MagicMock()
76
76
  http.__enter__ = MagicMock(return_value=http)
77
77
  http.__exit__ = MagicMock(return_value=False)
78
- http.get.return_value = httpx.Response(200, json=payload, request=request)
78
+ http.get.return_value = httpx2.Response(200, json=payload, request=request)
79
79
  with patch.object(client, "_http_client", return_value=http):
80
80
  user = client.oauth.userinfo()
81
81
  assert isinstance(user, UserInfoResponse)
@@ -2,7 +2,7 @@
2
2
 
3
3
  from unittest.mock import MagicMock, patch
4
4
 
5
- import httpx
5
+ import httpx2
6
6
  import pytest
7
7
 
8
8
  from sweatstack import PortalDestination, PortalSessionResponse
@@ -24,8 +24,8 @@ def app():
24
24
 
25
25
 
26
26
  def _http_returning(status_code: int, payload) -> MagicMock:
27
- request = httpx.Request("POST", "https://test.sweatstack.no/api/v1/portal/sessions")
28
- response = httpx.Response(status_code, json=payload, request=request)
27
+ request = httpx2.Request("POST", "https://test.sweatstack.no/api/v1/portal/sessions")
28
+ response = httpx2.Response(status_code, json=payload, request=request)
29
29
  http = MagicMock()
30
30
  http.__enter__ = MagicMock(return_value=http)
31
31
  http.__exit__ = MagicMock(return_value=False)
@@ -126,7 +126,7 @@ class TestActivities:
126
126
  assert name == "ride.fit" and handle.closed
127
127
 
128
128
  def test_watch_backfill_status_reconnects_after_a_dropped_connection(self, client):
129
- """Regression: auto_reconnect caught httpx errors the transport had already converted."""
129
+ """Regression: auto_reconnect caught transport errors the transport had already converted."""
130
130
  response = MagicMock()
131
131
  response.iter_lines.return_value = ['{"backfill_loaded_until": "2026-01-01T00:00:00Z"}']
132
132
  http = MagicMock()