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.
- {sweatstack-0.96.0 → sweatstack-0.97.0}/.github/workflows/ci.yml +1 -1
- {sweatstack-0.96.0 → sweatstack-0.97.0}/AGENTS.md +4 -4
- {sweatstack-0.96.0 → sweatstack-0.97.0}/CHANGELOG.md +14 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/DEVELOPMENT.md +1 -1
- {sweatstack-0.96.0 → sweatstack-0.97.0}/PKG-INFO +3 -4
- {sweatstack-0.96.0 → sweatstack-0.97.0}/examples/send_webhook.py +8 -8
- {sweatstack-0.96.0 → sweatstack-0.97.0}/pyproject.toml +4 -5
- {sweatstack-0.96.0 → sweatstack-0.97.0}/skills/sweatstack-python/SKILL.md +1 -1
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/_frames.py +3 -3
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/_transport.py +16 -16
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/cli.py +2 -2
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/client.py +9 -9
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/exceptions.py +1 -1
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/dependencies.py +4 -4
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/routes.py +3 -3
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/streamlit.py +2 -2
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_account_status.py +3 -3
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_exceptions.py +7 -7
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_frames.py +2 -2
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_identity.py +3 -3
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_portal.py +3 -3
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_resources.py +1 -1
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_tests.py +5 -5
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_trace_test_linking.py +4 -4
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_transport.py +44 -22
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_user_agent.py +10 -10
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_webhooks.py +10 -10
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_write_timezone_validation.py +2 -2
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/typing/output_types.py +1 -1
- {sweatstack-0.96.0 → sweatstack-0.97.0}/uv.lock +70 -508
- {sweatstack-0.96.0 → sweatstack-0.97.0}/.git-blame-ignore-revs +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/.gitignore +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/.gitleaks.toml +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/.python-version +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/CONTRIBUTING.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/LICENSE +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/Makefile +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/README.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/UPGRADING.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/agent.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/cases.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/evaluators.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/harness.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/lifecycle.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/evals/agent/run.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/001a_tests.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/001b_metadata.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/001c_dailies.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/003_trace_test_linking.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/004_codebase_hygiene.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/005_ost_sport_bridge.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/006_output_backends.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/007_account_status_userinfo_portal.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/008_sdk_documentation.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/009_road_to_1_0.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/010_credentials_one_source.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/plans/011_user_agent_on_every_request.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/skills/sweatstack-python/api.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/_docs.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/_renames.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/access_token_cache.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/_app_metadata.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/_base.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/activities.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/dailies.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/oauth.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/portal.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/profile.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/teams.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/tests.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/traces.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/resources/users.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/__init__.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/conftest.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_access_token_cache.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_authorization_url.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_changelog.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_credentials.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_dailies.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_docs_check.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_docstrings.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_longitudinal_mean_max_after.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_metadata.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_output.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_public_hygiene.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_public_surface.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_renames.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_segmentation.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_skill.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_sport_ost.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_streamlit_auth.py +0 -0
- {sweatstack-0.96.0 → sweatstack-0.97.0}/tests/test_teams.py +0 -0
- {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.
|
|
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
|
|
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 `
|
|
237
|
-
`
|
|
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 `
|
|
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.
|
|
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.
|
|
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.
|
|
23
|
+
Requires-Python: >=3.11
|
|
25
24
|
Requires-Dist: email-validator>=2.2.0
|
|
26
|
-
Requires-Dist:
|
|
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
|
-
# "
|
|
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
|
|
35
|
+
from datetime import UTC, datetime
|
|
36
36
|
|
|
37
|
-
import
|
|
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
|
-
) ->
|
|
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(
|
|
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 =
|
|
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
|
|
179
|
+
except httpx2.ConnectError:
|
|
180
180
|
print(f"✗ Connection failed - is the server running at {args.url}?")
|
|
181
181
|
sys.exit(1)
|
|
182
|
-
except
|
|
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.
|
|
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
|
+
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
|
-
"
|
|
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 = "
|
|
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 `
|
|
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
|
|
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=
|
|
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(
|
|
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 ``
|
|
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
|
|
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:
|
|
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:
|
|
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) ->
|
|
84
|
-
"""``
|
|
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 =
|
|
90
|
+
headers = httpx2.Headers({"User-Agent": USER_AGENT})
|
|
91
91
|
headers.update(kwargs.pop("headers", None) or {})
|
|
92
|
-
return
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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[
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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) ->
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
) ->
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
10
|
from typing import Annotated, NoReturn
|
|
11
11
|
from urllib.parse import quote
|
|
12
12
|
|
|
13
|
-
import
|
|
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 =
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
103
|
-
response =
|
|
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
|
|
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
|
|
150
|
-
request =
|
|
151
|
-
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
|
|
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
|
|
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 =
|
|
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
|
|
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=
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
28
|
-
response =
|
|
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
|
|
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()
|