sweatstack 0.94.0__tar.gz → 0.95.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.94.0 → sweatstack-0.95.0}/CHANGELOG.md +77 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/PKG-INFO +1 -1
- sweatstack-0.95.0/plans/011_user_agent_on_every_request.md +228 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/pyproject.toml +1 -1
- {sweatstack-0.94.0 → sweatstack-0.95.0}/skills/sweatstack-python/api.md +1 -1
- {sweatstack-0.94.0 → sweatstack-0.95.0}/skills/sweatstack-python/data-models.md +1 -1
- {sweatstack-0.94.0 → sweatstack-0.95.0}/skills/sweatstack-python/fastapi.md +1 -1
- {sweatstack-0.94.0 → sweatstack-0.95.0}/skills/sweatstack-python/streamlit.md +2 -2
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/__init__.py +1 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/_transport.py +19 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/client.py +4 -10
- sweatstack-0.95.0/src/sweatstack/constants.py +19 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/fastapi/config.py +3 -2
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/fastapi/dependencies.py +2 -1
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/fastapi/routes.py +3 -2
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/openapi_schemas.py +127 -64
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/oauth.py +8 -6
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/profile.py +2 -2
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/teams.py +5 -3
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/streamlit.py +5 -4
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_account_status.py +5 -5
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_transport.py +69 -0
- sweatstack-0.95.0/tests/test_user_agent.py +131 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/uv.lock +1 -1
- sweatstack-0.94.0/src/sweatstack/constants.py +0 -1
- {sweatstack-0.94.0 → sweatstack-0.95.0}/.git-blame-ignore-revs +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/.github/workflows/ci.yml +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/.gitignore +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/.gitleaks.toml +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/.python-version +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/AGENTS.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/CONTRIBUTING.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/LICENSE +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/Makefile +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/README.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/UPGRADING.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/evals/__init__.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/evals/agent/__init__.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/evals/agent/agent.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/evals/agent/cases.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/evals/agent/evaluators.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/evals/agent/harness.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/evals/agent/lifecycle.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/evals/agent/run.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/examples/send_webhook.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/001a_tests.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/001b_metadata.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/001c_dailies.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/003_trace_test_linking.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/004_codebase_hygiene.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/005_ost_sport_bridge.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/006_output_backends.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/007_account_status_userinfo_portal.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/008_sdk_documentation.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/009_road_to_1_0.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/plans/010_credentials_one_source.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/skills/sweatstack-python/SKILL.md +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/_docs.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/_frames.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/_renames.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/exceptions.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/fastapi/access_token_cache.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/__init__.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/_app_metadata.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/_base.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/activities.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/dailies.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/portal.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/tests.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/traces.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/resources/users.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/__init__.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/conftest.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_access_token_cache.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_changelog.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_credentials.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_dailies.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_docs_check.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_docstrings.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_exceptions.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_frames.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_identity.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_longitudinal_mean_max_after.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_metadata.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_output.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_portal.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_public_hygiene.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_public_surface.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_renames.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_resources.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_segmentation.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_skill.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_sport_ost.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_streamlit_auth.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_teams.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_tests.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_trace_test_linking.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_webhooks.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_wire_names.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/test_write_timezone_validation.py +0 -0
- {sweatstack-0.94.0 → sweatstack-0.95.0}/tests/typing/output_types.py +0 -0
|
@@ -11,6 +11,83 @@ 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.95.0] - 2026-10-13
|
|
15
|
+
|
|
16
|
+
Scopes are categories now. Request what your app uses. Breaking: the default scopes change
|
|
17
|
+
without an error, and the metabolic map and capability names move; see **Upgrading** below, or
|
|
18
|
+
hand the migration prompt to your coding agent.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- The default scope of `Client.login()`, `client.oauth.authorization_url()`, the FastAPI plugin and
|
|
23
|
+
the Streamlit helper is one constant, `sweatstack.constants.DEFAULT_SCOPE`:
|
|
24
|
+
`openid profile email activities:read offline_access`. Add `wellness:read`,
|
|
25
|
+
`reproductive_health:read`, `workouts:write` or `sharing:read` when your app uses them.
|
|
26
|
+
- `client.teams.authorize()` grants `activities:read` by default; only the delegable scopes are
|
|
27
|
+
ever granted.
|
|
28
|
+
- `client.oauth.userinfo()` needs the `openid` scope; the name claims need `profile`, the email
|
|
29
|
+
needs `email`.
|
|
30
|
+
- `client.profile.status()` needs no scope.
|
|
31
|
+
- `Application.default_scopes` and `Application.purposes` replace `capabilities` on
|
|
32
|
+
`client.applications` (the old field is still accepted and translated).
|
|
33
|
+
- The metabolic map responses are the markers themselves: no `inputs`, no `markers` wrapper; the
|
|
34
|
+
VO2 map has `vo2max_relative`.
|
|
35
|
+
- The `dailies` capability and its issue codes are `wellness`, `wellness_not_granted` and
|
|
36
|
+
`wellness_no_source`.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- `sweatstack.__version__` is the installed version.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- Token calls from the FastAPI and Streamlit integrations (login, refresh and delegated tokens)
|
|
45
|
+
now send the `python-sweatstack/<version>` User-Agent, like every other request the SDK
|
|
46
|
+
makes. Before, they identified themselves as plain httpx.
|
|
47
|
+
|
|
48
|
+
<!-- --8<-- [start:upgrading-0-95] -->
|
|
49
|
+
### Upgrading
|
|
50
|
+
|
|
51
|
+
| 0.94 | 0.95 |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `scope="data:read data:write profile offline_access"`, or no `scope=` at all | **Silent:** the default is `DEFAULT_SCOPE` (`openid profile email activities:read offline_access`). Add the categories you use: `wellness`, `reproductive_health`, `workouts` and `sharing`, each as `:read` or `:write` |
|
|
54
|
+
| `Scope.data_read` in `teams.authorize(scopes=...)`, or no `scopes=` | **Silent:** `"activities:read"` is the default, and only the delegable categories are granted; anything else is dropped |
|
|
55
|
+
| `userinfo()` with `profile` | `userinfo()` with `openid profile email`; without `openid` it raises `SweatStackAuthError` |
|
|
56
|
+
| `response.markers.fatmax` on a metabolic map | `response.fatmax` |
|
|
57
|
+
| `response.inputs.body_mass` on a metabolic map | not published; `response.vo2max_relative` on the VO2 map |
|
|
58
|
+
| `status.capabilities["dailies"]` | `status.capabilities["wellness"]` |
|
|
59
|
+
|
|
60
|
+
An app registered before the platform release keeps working with `data:read` and `data:write`
|
|
61
|
+
for six months; see the platform changelog.
|
|
62
|
+
|
|
63
|
+
**Migration prompt.** Paste this into your coding agent:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
Upgrade this codebase to sweatstack 0.95, which replaces the data:read and data:write scope
|
|
67
|
+
bundles with per-category scopes. The upgrade table is the "Upgrading" section of the 0.95.0
|
|
68
|
+
entry in https://github.com/SweatStack/sweatstack-python/blob/main/CHANGELOG.md
|
|
69
|
+
(also at https://docs.sweatstack.no/learn/python/upgrading/). Read it first.
|
|
70
|
+
|
|
71
|
+
Find every place the app requests or grants scopes (Client.login(), oauth.authorization_url(),
|
|
72
|
+
sweatstack.fastapi.configure(scopes=...), StreamlitAuth(scopes=...) and SWEATSTACK_SCOPES,
|
|
73
|
+
teams.authorize(scopes=...)) and:
|
|
74
|
+
|
|
75
|
+
1. Replace data:read and data:write with the categories the app uses: activities, wellness,
|
|
76
|
+
reproductive_health, workouts and sharing, each as :read or :write. Start from
|
|
77
|
+
sweatstack.constants.DEFAULT_SCOPE (openid profile email activities:read offline_access)
|
|
78
|
+
and add to it. Do not keep the old bundles.
|
|
79
|
+
2. Where the app calls userinfo(), request openid, plus profile and email for the claims it
|
|
80
|
+
shows.
|
|
81
|
+
3. On the metabolic maps, read response.<marker> instead of response.markers.<marker>, drop
|
|
82
|
+
any use of response.inputs, and read status.capabilities["wellness"] where the app read
|
|
83
|
+
["dailies"].
|
|
84
|
+
|
|
85
|
+
Then run the code and the tests. A category the app forgot surfaces as SweatStackAuthError
|
|
86
|
+
on the first call that needs it; add that category, not a bundle.
|
|
87
|
+
```
|
|
88
|
+
<!-- --8<-- [end:upgrading-0-95] -->
|
|
89
|
+
|
|
90
|
+
|
|
14
91
|
## [0.94.0] - 2026-10-12
|
|
15
92
|
|
|
16
93
|
A client's tokens come from one source. Upgrade every app that other people sign in to.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Plan 011: Every request the SDK makes carries its User-Agent
|
|
2
|
+
|
|
3
|
+
Requests made through `Client` identify themselves as `python-sweatstack/<version>`. The four
|
|
4
|
+
token calls in the FastAPI and Streamlit integrations do not: each is a bare `httpx.post`, so it
|
|
5
|
+
goes out as `python-httpx/<version>`, the library default. Seen from the server, an integrator's
|
|
6
|
+
logins, refreshes and delegations look like an unknown httpx script, not like the SDK at a known
|
|
7
|
+
version. That makes three things harder than they should be: telling an integrator which SDK
|
|
8
|
+
version they run, tying a token-endpoint failure to an SDK release, and knowing when an old
|
|
9
|
+
release has stopped being used.
|
|
10
|
+
|
|
11
|
+
The fix is one constant, one function and one guard. **Constant:** the header string has one
|
|
12
|
+
home, next to `__version__`. **Function:** every request that does not go through the client's
|
|
13
|
+
pool goes through `_transport.post`, which adds the header. **Guard:** a test fails on any
|
|
14
|
+
request made with `httpx` directly from anywhere else, so the next bare call is caught at
|
|
15
|
+
`make check`, not in the server's logs.
|
|
16
|
+
|
|
17
|
+
Proposed 2026-10-13. Ships in the next release, under *Fixed*, next to the scope work already under
|
|
18
|
+
`[Unreleased]`; no public surface changes.
|
|
19
|
+
|
|
20
|
+
> **Public repository.** This file is published with the code. It describes the SDK by its own
|
|
21
|
+
> source and the server by its public contract. It cites no app, no version in use, no counts
|
|
22
|
+
> and no time window from server logs; those are customer behaviour and stay out (AGENTS.md,
|
|
23
|
+
> "Public repository").
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## As built (2026-10-13)
|
|
27
|
+
|
|
28
|
+
Sections 2 to 5 shipped as designed, with three refinements:
|
|
29
|
+
|
|
30
|
+
- **`__version__` is public at the package root.** `client.py` no longer defines or imports
|
|
31
|
+
it; `sweatstack.__version__` re-exports it from `constants.py`, which is where Python users
|
|
32
|
+
look for it. Nothing in the repository read it from `client.py`. Listed under *Added*.
|
|
33
|
+
- **The guard also catches `from httpx import post`.** D5 named attribute calls only; the test
|
|
34
|
+
flags a `from httpx import ...` of any request-making name too, so the rule cannot be dodged
|
|
35
|
+
by changing the import style.
|
|
36
|
+
- **The callback test runs the real route.** It mounts the router with `instrument`, sets the
|
|
37
|
+
state cookie that `create_state` mints and follows the handler through to the session
|
|
38
|
+
redirect, so a change to the state check or the exchange would surface here.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## 1. The problem, verified
|
|
42
|
+
|
|
43
|
+
### 1.1 What the header is for
|
|
44
|
+
|
|
45
|
+
The server reads `User-Agent` to attribute traffic to an SDK release. The client has sent
|
|
46
|
+
`python-sweatstack/<version>` since the pooled connection landed; every call that goes through
|
|
47
|
+
`Client._request` carries it, including the client's own token exchange and refresh. The value
|
|
48
|
+
comes from `importlib.metadata.version("sweatstack")`, with `"unknown"` as the fallback when
|
|
49
|
+
the package is not installed.
|
|
50
|
+
|
|
51
|
+
### 1.2 Where it is missing
|
|
52
|
+
|
|
53
|
+
On `main` at 0.94.0, four requests bypass the pool. Each is a bare `httpx.post` with no
|
|
54
|
+
`User-Agent`:
|
|
55
|
+
|
|
56
|
+
| Call site | Endpoint | When it runs |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `fastapi/dependencies.py`, `_refresh_access_token` | `POST /api/v1/oauth/token` (`refresh_token`) | Every request whose session token is about to expire |
|
|
59
|
+
| `fastapi/routes.py`, the `/callback` route | `POST /api/v1/oauth/token` (`authorization_code`) | Every login |
|
|
60
|
+
| `fastapi/routes.py`, `_fetch_delegated_token` | `POST /api/v1/oauth/delegated-token` | Switching to a delegated user |
|
|
61
|
+
| `streamlit.py`, `StreamlitAuth._exchange_token` | `POST /api/v1/oauth/token` (`authorization_code`) | Every login |
|
|
62
|
+
|
|
63
|
+
The refresh is by far the most frequent of the four: a long-running app makes one per session
|
|
64
|
+
per token lifetime, which is why token traffic from the integrations is mostly anonymous today.
|
|
65
|
+
|
|
66
|
+
Nothing else is affected. `client.py` builds exactly one `httpx.Client`, the pool, with the
|
|
67
|
+
header set. `cli.py` fetches a local `openapi.json` with `httpx.get`; that is a developer tool
|
|
68
|
+
and stays as it is. No other HTTP library is used anywhere in `src/`.
|
|
69
|
+
|
|
70
|
+
### 1.3 How it got this way
|
|
71
|
+
|
|
72
|
+
Release 0.65.0 had the same defect inside `Client`: a bare `httpx.post` for the token exchange
|
|
73
|
+
next to a pooled client that did carry the header. The pool refactor fixed it for `Client` by
|
|
74
|
+
routing the exchange through `_http_client`. The integrations were never followed up. Plan 010
|
|
75
|
+
noted the Streamlit exchange as "not folded in" and left it for later; this is later.
|
|
76
|
+
|
|
77
|
+
The header string is also written out twice in `client.py`, once for the pool and once in
|
|
78
|
+
`_http_client`. Two copies today, no shared one, and `__version__` lives in `client.py`, which
|
|
79
|
+
is the wrong place for the integrations to import from.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## 2. Design
|
|
83
|
+
|
|
84
|
+
Five decisions. Each names the alternative it rejects.
|
|
85
|
+
|
|
86
|
+
**D1. One constant, one home.** `constants.py` gains
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
__version__ = version("sweatstack")
|
|
93
|
+
except PackageNotFoundError:
|
|
94
|
+
__version__ = "unknown"
|
|
95
|
+
|
|
96
|
+
USER_AGENT = f"python-sweatstack/{__version__}"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
and `client.py` imports both names from there, so `sweatstack.client.__version__` keeps
|
|
100
|
+
working. `constants.py` imports nothing from the package and every call-site module already
|
|
101
|
+
imports it, so there is no cycle to manage. *Rejected:* a new `_version.py` module. One more
|
|
102
|
+
file for one line, and `constants.py` is already the home for values every entry point shares.
|
|
103
|
+
|
|
104
|
+
**D2. One function for requests outside the pool.** `_transport.py` gains
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
def post(url: str, **kwargs: Any) -> httpx.Response:
|
|
108
|
+
"""``httpx.post`` with the SDK's ``User-Agent``; the caller's headers win on a clash."""
|
|
109
|
+
headers = httpx.Headers({"User-Agent": USER_AGENT})
|
|
110
|
+
headers.update(kwargs.pop("headers", None) or {})
|
|
111
|
+
return httpx.post(url, headers=headers, **kwargs)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It adds the header and nothing else. No retry (`POST` is never retried; see
|
|
115
|
+
`RETRYABLE_METHODS`), no error translation, no timeout. The four sites keep their own error
|
|
116
|
+
handling and timeouts exactly as they are. *Rejected:* passing `headers={"User-Agent": ...}` at
|
|
117
|
+
each site. It works, but then the guard has to prove that every multi-line call passes the
|
|
118
|
+
header, which a test cannot do reliably. With a function, the guard is one rule: nobody but
|
|
119
|
+
the transport talks to `httpx`.
|
|
120
|
+
|
|
121
|
+
**D3. The integrations stay stateless.** They do not acquire a `Client` to make the call. They
|
|
122
|
+
are deliberately thin helpers over the token endpoint, with no token pair of their own to
|
|
123
|
+
manage, and a `Client` would bring the refresh lock and credential resolution of plan 010 into
|
|
124
|
+
code paths that must not have them. *Rejected:* `Client(auth=False)._request(...)`; a bigger
|
|
125
|
+
change than the header needs, and it pulls a user-facing object into a server hot path.
|
|
126
|
+
|
|
127
|
+
**D4. The value stays `python-sweatstack/<version>`.** Identical to what the client sends, so
|
|
128
|
+
the server sees one SDK, not two. *Rejected:* appending `python-httpx/<version>` or a runtime
|
|
129
|
+
description, as some SDKs do. Nothing on the server reads it, and it would make the two paths
|
|
130
|
+
disagree.
|
|
131
|
+
|
|
132
|
+
**D5. The guard is an AST test, not a grep.** It parses every module under `src/sweatstack/`
|
|
133
|
+
and fails on a call to `httpx.get`, `post`, `put`, `patch`, `delete`, `request`, `stream`,
|
|
134
|
+
`Client` or `AsyncClient` outside an allow-list of three: `_transport.py` (the function above),
|
|
135
|
+
`client.py` (the pool) and `cli.py` (the developer tool). *Rejected:* the regex in the first
|
|
136
|
+
draft of this plan. A regex matches text, not calls, and cannot see across the lines of a call
|
|
137
|
+
to check what it passes.
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
## 3. Change
|
|
141
|
+
|
|
142
|
+
Seven files, all small. Order matters only for the first two.
|
|
143
|
+
|
|
144
|
+
1. **`constants.py`**: add `__version__` and `USER_AGENT` as in D1.
|
|
145
|
+
2. **`client.py`**: drop the local `__version__` block and the `importlib.metadata` import;
|
|
146
|
+
`from .constants import DEFAULT_SCOPE, DEFAULT_URL, USER_AGENT, __version__`. Replace both
|
|
147
|
+
header literals (the pool and `_http_client`) with `USER_AGENT`.
|
|
148
|
+
3. **`_transport.py`**: add `post` as in D2, importing `USER_AGENT` from `constants`.
|
|
149
|
+
4. **`fastapi/dependencies.py`**: `httpx.post(...)` in `_refresh_access_token` becomes
|
|
150
|
+
`_transport.post(...)`, same arguments. The `timeout=REFRESH_HTTP_TIMEOUT` stays.
|
|
151
|
+
5. **`fastapi/routes.py`**: the same substitution in the `/callback` route and in
|
|
152
|
+
`_fetch_delegated_token`. The latter keeps its `Authorization` header; the function merges it
|
|
153
|
+
under the SDK header. Both keep their `httpx.HTTPError` handling, which still applies since
|
|
154
|
+
the function raises what `httpx.post` raises. The `import httpx` stays, for the exception
|
|
155
|
+
type.
|
|
156
|
+
6. **`streamlit.py`**: the same substitution in `_exchange_token`, keeping `auth=` and `data=`.
|
|
157
|
+
The `import httpx` stays, for `httpx.BasicAuth`.
|
|
158
|
+
7. **`CHANGELOG.md`**: see section 5.
|
|
159
|
+
|
|
160
|
+
Nothing in the body, auth, timeout or error path of any of the four calls changes. A diff of
|
|
161
|
+
each site is one token: `httpx.post` to `_transport.post`, plus the import.
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## 4. Tests
|
|
165
|
+
|
|
166
|
+
All offline, all under `tests/`. `make check` must pass with no silenced findings.
|
|
167
|
+
|
|
168
|
+
**The function** (`tests/test_transport.py`, where the HTTP layer is tested):
|
|
169
|
+
|
|
170
|
+
- `post` sends `User-Agent: python-sweatstack/<version>` and passes the URL and keyword
|
|
171
|
+
arguments through unchanged.
|
|
172
|
+
- A caller's `Authorization` header survives the merge.
|
|
173
|
+
- A caller's own `User-Agent` wins, so the merge rule in the docstring is true.
|
|
174
|
+
|
|
175
|
+
**The guard** (`tests/test_transport.py`): the AST walk of D5. It fails with the file, line and
|
|
176
|
+
call that broke the rule, so the fix is obvious from the message.
|
|
177
|
+
|
|
178
|
+
**The four call sites** (new `tests/test_user_agent.py`): one test each, patching
|
|
179
|
+
`httpx.post` and asserting the request's `User-Agent` equals `constants.USER_AGENT` and that
|
|
180
|
+
what the site sent before is still sent (grant type, code, credentials, bearer).
|
|
181
|
+
|
|
182
|
+
- `_refresh_access_token` is a function; call it directly.
|
|
183
|
+
- `_fetch_delegated_token` is a function; call it directly and also assert the bearer is
|
|
184
|
+
still on the request.
|
|
185
|
+
- The `/callback` route runs through `fastapi.testclient.TestClient` on an app configured as
|
|
186
|
+
in `tests/test_access_token_cache.py`, with a `state` query parameter that matches the state
|
|
187
|
+
cookie, so the handler reaches the exchange.
|
|
188
|
+
- `StreamlitAuth._exchange_token` runs with the session fixtures from
|
|
189
|
+
`tests/test_streamlit_auth.py`.
|
|
190
|
+
|
|
191
|
+
Patching `httpx.post` rather than injecting `httpx.MockTransport` is deliberate: a module-level
|
|
192
|
+
`httpx.post` builds its own client and accepts no transport, and the function keeps that shape
|
|
193
|
+
on purpose (D2).
|
|
194
|
+
|
|
195
|
+
**Done when:**
|
|
196
|
+
|
|
197
|
+
- The four tests above and the three function tests pass.
|
|
198
|
+
- The guard passes on `main` plus this change, and fails when a bare `httpx.post` is put back
|
|
199
|
+
into any of the four sites.
|
|
200
|
+
- `grep -rn "python-sweatstack/" src/` finds exactly one line, in `constants.py`.
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
## 5. Release
|
|
204
|
+
|
|
205
|
+
Part of the next release, with the scope work already under `[Unreleased]`. Add there:
|
|
206
|
+
|
|
207
|
+
> ### Fixed
|
|
208
|
+
> - Token calls from the FastAPI and Streamlit integrations (login, refresh and delegated
|
|
209
|
+
> tokens) now send the `python-sweatstack/<version>` User-Agent, like every other request
|
|
210
|
+
> the SDK makes. Before, they identified themselves as plain httpx.
|
|
211
|
+
|
|
212
|
+
Not breaking: no name, parameter or behaviour visible to a caller changes, so there is no
|
|
213
|
+
*Upgrading* section and nothing for `_renames`. The skill in `skills/sweatstack-python/` and the
|
|
214
|
+
docs describe the public surface, which is unchanged, so neither moves.
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## 6. Seen while looking, left alone
|
|
218
|
+
|
|
219
|
+
Three things this plan does not change. Each would be its own decision.
|
|
220
|
+
|
|
221
|
+
- **`cli.py`** keeps its bare `httpx.get`. It talks to a developer's local server and is
|
|
222
|
+
allow-listed by the guard with a comment saying why.
|
|
223
|
+
- **Timeouts differ across the four sites.** The refresh passes `REFRESH_HTTP_TIMEOUT`; the two
|
|
224
|
+
logins and the delegation use httpx's default. Whether the three should share a constant is a
|
|
225
|
+
reliability question, not a header question.
|
|
226
|
+
- **Error handling differs across the four sites.** One calls `raise_for_status`, one
|
|
227
|
+
translates to the SDK's exceptions, one redirects with an error code, one raises
|
|
228
|
+
`SweatStackAuthError`. Each is right for its caller; unifying them is not this plan's job.
|
|
@@ -105,7 +105,7 @@ client.whoami() # the user this cl
|
|
|
105
105
|
```python
|
|
106
106
|
client.portal.sessions.create("manage-integrations", return_url=None) # user token; mint on click, never store
|
|
107
107
|
|
|
108
|
-
client.oauth.userinfo() # OpenID claims + issue (needs
|
|
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
110
|
client.oauth.authorization_url(client_id="YOUR_CLIENT_ID", redirect_uri="http://localhost:8000/callback", code_challenge=challenge)
|
|
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
|
|
@@ -29,7 +29,7 @@ taxonomy is preserved (`is_standard` is `False`) instead of failing.
|
|
|
29
29
|
- `Metric`: data streams such as `power`, `speed`, `heart_rate`, `cadence`, `altitude`,
|
|
30
30
|
`distance`, `temperature`, `core_temperature`, `smo2`, `lactate`, `rpe`. `metric.display_name()`
|
|
31
31
|
gives a readable form. Open set: unknown values from a newer server still parse.
|
|
32
|
-
- `Scope`: `Scope.
|
|
32
|
+
- `Scope`: `Scope.activities_read` (`"activities:read"`), `activities_write`, `wellness_read`, `wellness_write`, `reproductive_health_read`, `reproductive_health_write`, `workouts_read`, `workouts_write`, `sharing_read`, `sharing_write`, `openid`, `profile`, `email`, `offline_access`, `admin`; `data_read` and `data_write` are the retired bundles. Open set.
|
|
33
33
|
- `DailyMeasure`: `body_mass`, `body_fat_pct`, `resting_hr`, `hrv`, `sleep_duration`,
|
|
34
34
|
`sleep_altitude`, `menstrual_cycle_day`. Open set.
|
|
35
35
|
|
|
@@ -50,7 +50,7 @@ configure(
|
|
|
50
50
|
client_secret: str = None, # SWEATSTACK_CLIENT_SECRET
|
|
51
51
|
app_url: str = None, # APP_URL
|
|
52
52
|
session_secret: str = None, # SWEATSTACK_SESSION_SECRET (Fernet key)
|
|
53
|
-
scopes: list[str] =
|
|
53
|
+
scopes: list[str] = DEFAULT_SCOPE.split(), # openid profile email activities:read offline_access
|
|
54
54
|
cookie_secure: bool = None, # auto-detected from app_url scheme
|
|
55
55
|
cookie_max_age: int = 86400, # session lifetime in seconds
|
|
56
56
|
auth_route_prefix: str = "/auth/sweatstack",
|
|
@@ -49,7 +49,7 @@ st.dataframe(auth.client.activities.list(output="pandas")) # st.dataframe also
|
|
|
49
49
|
| `SWEATSTACK_CLIENT_ID` | OAuth client ID |
|
|
50
50
|
| `SWEATSTACK_CLIENT_SECRET` | OAuth client secret |
|
|
51
51
|
| `SWEATSTACK_REDIRECT_URI` | OAuth callback URI |
|
|
52
|
-
| `SWEATSTACK_SCOPES` | Comma-separated scopes (default: `
|
|
52
|
+
| `SWEATSTACK_SCOPES` | Comma-separated scopes (default: `sweatstack.constants.DEFAULT_SCOPE`, i.e. `openid,profile,email,activities:read,offline_access`) |
|
|
53
53
|
|
|
54
54
|
## Selector Components
|
|
55
55
|
|
|
@@ -115,7 +115,7 @@ activities = auth.client.activities.list()
|
|
|
115
115
|
- **Never cache `auth` or `auth.client` across sessions** (`st.cache_resource`, module globals): they hold one user's tokens. Build them at the top of every run.
|
|
116
116
|
- **Session state keys:** `sweatstack_api_key`, `sweatstack_refresh_token`, and in proxy mode `sweatstack_proxy_api_key`, `sweatstack_proxy_refresh_token`. Don't overwrite these; `auth.client` keeps a renewed token there.
|
|
117
117
|
- **`select_user()` replaces `auth.client`** with a client delegated to the selected user (its own access and refresh token). Call `auth.switch_to_principal_user()` to go back.
|
|
118
|
-
- **Scopes default to `
|
|
118
|
+
- **Scopes default to `DEFAULT_SCOPE`** (`openid profile email activities:read offline_access`), the same set as `Client.authenticate()` and the FastAPI plugin. Add `wellness:read`, `reproductive_health:read`, `workouts:write` or `sharing:read` when the app uses them.
|
|
119
119
|
|
|
120
120
|
## Account Status and the Portal
|
|
121
121
|
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
The policy follows the OpenAI, Anthropic and Stripe clients, with one difference: ``POST`` is
|
|
4
4
|
never retried, because the SweatStack API has no idempotency keys and a retried create could
|
|
5
5
|
create twice.
|
|
6
|
+
|
|
7
|
+
This module is also the only place in the package that calls ``httpx`` to make a request:
|
|
8
|
+
through a client's pool, or through :func:`post` for the token calls the FastAPI and Streamlit
|
|
9
|
+
integrations make without a client. Both put the SDK's ``User-Agent`` on the wire, and
|
|
10
|
+
``tests/test_transport.py`` fails on a request made from anywhere else.
|
|
6
11
|
"""
|
|
7
12
|
|
|
8
13
|
from __future__ import annotations
|
|
@@ -14,6 +19,8 @@ from typing import Any
|
|
|
14
19
|
|
|
15
20
|
import httpx
|
|
16
21
|
|
|
22
|
+
from .constants import USER_AGENT
|
|
23
|
+
|
|
17
24
|
RETRY_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
|
|
18
25
|
"""Statuses worth retrying: timeouts, rate limits and transient server errors."""
|
|
19
26
|
|
|
@@ -73,6 +80,18 @@ def wait_before_retry(
|
|
|
73
80
|
return wait
|
|
74
81
|
|
|
75
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.
|
|
85
|
+
|
|
86
|
+
For the one-off token calls the integrations make without a ``Client``. It adds the header
|
|
87
|
+
and nothing else: no retry (a ``POST`` is never retried), no error translation and no
|
|
88
|
+
timeout, so each caller keeps its own.
|
|
89
|
+
"""
|
|
90
|
+
headers = httpx.Headers({"User-Agent": USER_AGENT})
|
|
91
|
+
headers.update(kwargs.pop("headers", None) or {})
|
|
92
|
+
return httpx.post(url, headers=headers, **kwargs)
|
|
93
|
+
|
|
94
|
+
|
|
76
95
|
class Session:
|
|
77
96
|
"""One request scope on the client's shared connection pool, with its own headers.
|
|
78
97
|
|
|
@@ -15,7 +15,6 @@ from datetime import date, datetime
|
|
|
15
15
|
from enum import Enum
|
|
16
16
|
from functools import cached_property, wraps
|
|
17
17
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
-
from importlib.metadata import version
|
|
19
18
|
from inspect import getmembers, isfunction
|
|
20
19
|
from pathlib import Path
|
|
21
20
|
from typing import Any, Literal
|
|
@@ -27,7 +26,7 @@ from pydantic import SecretStr
|
|
|
27
26
|
|
|
28
27
|
from . import _frames, _renames, _transport
|
|
29
28
|
from ._frames import FrameOutput, set_output
|
|
30
|
-
from .constants import DEFAULT_URL
|
|
29
|
+
from .constants import DEFAULT_SCOPE, DEFAULT_URL, USER_AGENT
|
|
31
30
|
from .exceptions import (
|
|
32
31
|
SweatStackAPIError,
|
|
33
32
|
SweatStackAuthError,
|
|
@@ -121,11 +120,6 @@ AUTH_SUCCESSFUL_RESPONSE = """<!DOCTYPE html>
|
|
|
121
120
|
</html>"""
|
|
122
121
|
OAUTH2_CLIENT_ID = "5382f68b0d254378"
|
|
123
122
|
|
|
124
|
-
try:
|
|
125
|
-
__version__ = version("sweatstack")
|
|
126
|
-
except ImportError:
|
|
127
|
-
__version__ = "unknown"
|
|
128
|
-
|
|
129
123
|
|
|
130
124
|
def _to_secret(value: str | SecretStr | None) -> SecretStr | None:
|
|
131
125
|
"""Convert a string to SecretStr, or return None if value is None."""
|
|
@@ -647,7 +641,7 @@ class Client:
|
|
|
647
641
|
client_id=OAUTH2_CLIENT_ID,
|
|
648
642
|
redirect_uri=f"http://localhost:{port}",
|
|
649
643
|
code_challenge=code_challenge,
|
|
650
|
-
scope=
|
|
644
|
+
scope=DEFAULT_SCOPE,
|
|
651
645
|
prompt=None,
|
|
652
646
|
)
|
|
653
647
|
|
|
@@ -894,7 +888,7 @@ class Client:
|
|
|
894
888
|
return built[1]
|
|
895
889
|
pool = httpx.Client(
|
|
896
890
|
base_url=url,
|
|
897
|
-
headers={"User-Agent":
|
|
891
|
+
headers={"User-Agent": USER_AGENT},
|
|
898
892
|
timeout=self.timeout,
|
|
899
893
|
)
|
|
900
894
|
self.__dict__["_pool_entry"] = (url, pool)
|
|
@@ -918,7 +912,7 @@ class Client:
|
|
|
918
912
|
For the token exchange, which authenticates with the app's own credentials in
|
|
919
913
|
the body, so a user's bearer is never sent where it is not needed.
|
|
920
914
|
"""
|
|
921
|
-
headers = httpx.Headers({"User-Agent":
|
|
915
|
+
headers = httpx.Headers({"User-Agent": USER_AGENT})
|
|
922
916
|
if not auth:
|
|
923
917
|
token = None
|
|
924
918
|
elif skip_token_check:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
DEFAULT_URL = "https://app.sweatstack.no"
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("sweatstack")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "unknown"
|
|
9
|
+
|
|
10
|
+
# What every request the SDK makes says it is, from the connection pool and from the token
|
|
11
|
+
# calls the integrations make outside it (``_transport.post``). One definition, so the server
|
|
12
|
+
# sees one SDK at one version wherever a request comes from.
|
|
13
|
+
USER_AGENT = f"python-sweatstack/{__version__}"
|
|
14
|
+
|
|
15
|
+
# The scopes the quickstart asks for: sign in, see the name and email, read activities, keep
|
|
16
|
+
# access. One constant for every entry point that carries a default, so they cannot drift.
|
|
17
|
+
# Wellness, reproductive health, writes and sharing are choices to type. Declared ``str`` so
|
|
18
|
+
# that ``DEFAULT_SCOPE.split()`` is a ``list[str]`` and not a ``list[LiteralString]``.
|
|
19
|
+
DEFAULT_SCOPE: str = "openid profile email activities:read offline_access"
|
|
@@ -11,6 +11,7 @@ from urllib.parse import quote
|
|
|
11
11
|
from cryptography.fernet import Fernet
|
|
12
12
|
from pydantic import SecretStr
|
|
13
13
|
|
|
14
|
+
from ..constants import DEFAULT_SCOPE
|
|
14
15
|
from .access_token_cache import AccessTokenCache, InMemoryAccessTokenCache
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
@@ -89,7 +90,7 @@ def configure(
|
|
|
89
90
|
session_secret: Fernet key(s) for cookie encryption. Can be a single key
|
|
90
91
|
or a list of keys for key rotation (first encrypts, all decrypt).
|
|
91
92
|
Falls back to SWEATSTACK_SESSION_SECRET env var.
|
|
92
|
-
scopes: OAuth scopes to request. Defaults to
|
|
93
|
+
scopes: OAuth scopes to request. Defaults to ``DEFAULT_SCOPE`` split on spaces.
|
|
93
94
|
cookie_secure: Whether to set the Secure flag on cookies. If not specified,
|
|
94
95
|
auto-detected from app_url (True for https, False for http).
|
|
95
96
|
cookie_max_age: Session cookie lifetime in seconds. Defaults to 86400 (24h).
|
|
@@ -151,7 +152,7 @@ def configure(
|
|
|
151
152
|
session_secret_obj = [_to_secret(s) for s in session_secret]
|
|
152
153
|
|
|
153
154
|
if scopes is None:
|
|
154
|
-
scopes =
|
|
155
|
+
scopes = DEFAULT_SCOPE.split()
|
|
155
156
|
|
|
156
157
|
# Normalize prefix (strip trailing slash)
|
|
157
158
|
auth_route_prefix = auth_route_prefix.rstrip("/")
|
|
@@ -13,6 +13,7 @@ from urllib.parse import quote
|
|
|
13
13
|
import httpx
|
|
14
14
|
from fastapi import Depends, HTTPException, Request, Response
|
|
15
15
|
|
|
16
|
+
from .. import _transport
|
|
16
17
|
from ..client import Client
|
|
17
18
|
from ..constants import DEFAULT_URL
|
|
18
19
|
from ..utils import decode_jwt_body
|
|
@@ -122,7 +123,7 @@ def _refresh_access_token(
|
|
|
122
123
|
on ``/oauth/token``; the tuple shape exists so that a future
|
|
123
124
|
server change is handled transparently.
|
|
124
125
|
"""
|
|
125
|
-
response =
|
|
126
|
+
response = _transport.post(
|
|
126
127
|
f"{DEFAULT_URL}/api/v1/oauth/token",
|
|
127
128
|
data={
|
|
128
129
|
"grant_type": "refresh_token",
|
|
@@ -12,6 +12,7 @@ import httpx
|
|
|
12
12
|
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
|
|
13
13
|
from fastapi.responses import RedirectResponse
|
|
14
14
|
|
|
15
|
+
from .. import _transport
|
|
15
16
|
from ..constants import DEFAULT_URL
|
|
16
17
|
from ..exceptions import SweatStackAPIError, SweatStackConnectionError
|
|
17
18
|
from ..utils import decode_jwt_body
|
|
@@ -101,7 +102,7 @@ def _fetch_delegated_token(
|
|
|
101
102
|
body["team_id"] = team_id
|
|
102
103
|
|
|
103
104
|
try:
|
|
104
|
-
response =
|
|
105
|
+
response = _transport.post(
|
|
105
106
|
f"{DEFAULT_URL}/api/v1/oauth/delegated-token",
|
|
106
107
|
headers={"Authorization": f"Bearer {principal_tokens.access_token}"},
|
|
107
108
|
json=body,
|
|
@@ -211,7 +212,7 @@ def create_router() -> APIRouter:
|
|
|
211
212
|
return error_redirect("missing_code")
|
|
212
213
|
|
|
213
214
|
try:
|
|
214
|
-
token_response =
|
|
215
|
+
token_response = _transport.post(
|
|
215
216
|
f"{DEFAULT_URL}/api/v1/oauth/token",
|
|
216
217
|
data={
|
|
217
218
|
"grant_type": "authorization_code",
|