Python SDK changelog¶
The changes in each release of the sweatstack package, from its CHANGELOG.md. For how to update your code, see Upgrading. For changes to the API itself, see the API changelog.
Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Until 1.0 a minor release may contain breaking changes. Every one has an Upgrading section
with a table from old to new. The procedure for moving across several releases, and a prompt
for a coding agent, are in UPGRADING.md next to this file and at
https://docs.sweatstack.no/learn/python/upgrading/.
[0.98.0] - 2026-10-30¶
Added¶
SWEATSTACK_CLIENT_IDandSWEATSTACK_CLIENT_SECRETname the app that issuedSWEATSTACK_API_KEYandSWEATSTACK_REFRESH_TOKEN, so a client that takes its tokens from the environment renews them with that app. Before, it renewed with the SDK's own client, which SweatStack refuses for a token issued to another app: tokens your app hands to a notebook, a job or a sandbox stopped working once the access token expired. Set the secret only if your app exchanged the sign-in code with it. The variables never apply to tokens saved byauthenticate()or passed toClient, andclient_id=andclient_secret=given toClientalways win.
Changed¶
- If your environment sets
SWEATSTACK_CLIENT_IDfor the Streamlit or FastAPI helper and also holds aSWEATSTACK_REFRESH_TOKENthatauthenticate()issued, a plainClient()there now renews that token with your app, which SweatStack refuses: the renewal fails once the access token expires, with an error that says why. UnsetSWEATSTACK_CLIENT_IDandSWEATSTACK_CLIENT_SECRETfor that process, or pass them to the helper asclient_id=andclient_secret=instead. DailySourceis an open enum, likeMetricandDailyMeasure: asourcethis version does not know (on a daily, a marker or a max effort) becomes aDailySourcewith that value instead of failing the whole response, so a new source on SweatStack never breaks this version.
Fixed¶
- Polars output works in the browser (Pyodide, as in JupyterLite). Pyodide's Polars has no
Parquet reader, so every time series and mean-max call raised an
AttributeError; the client now reads through pyarrow there, andsweatstack[polars]installs pyarrow on Pyodide.
[0.97.0] - 2026-10-22¶
Changed¶
- Requires Python 3.11 or later. On Python 3.10, pip and uv keep installing 0.96.
- The SDK sends its requests with
httpx2, the maintained continuation ofhttpx. Calling the SDK is unchanged, and errors are still theSweatStack*exceptions. - HTTPS certificates are checked against the operating system's trust store instead of the
certifibundle thathttpxshipped. A root certificate installed on the machine, such as a company proxy's, is now trusted. A Linux image without a CA bundle (theca-certificatespackage) can no longer connect; install it, or pointSSL_CERT_FILEat a bundle.
[0.96.0] - 2026-10-16¶
Added¶
client.oauth.authorization_url()takesnonce,integrationandemail_auth_mode, the authorization parameters it was missing.integration="garmin_connect"sends the user straight to Garmin, skipping the sign-in chooser.
Changed¶
- No sign-in link sends
promptany more:client.oauth.authorization_url()defaults toprompt=Noneinstead of"none", and the FastAPI plugin and the Streamlit helper leave it out. Users see what they saw before: SweatStack skips the consent screen by itself when the user's grant already covers the request. - The Streamlit helper separates scopes with spaces, as the other sign-in links do.
StreamlitAuth.get_authorization_url()raisesValueErrorwithout a client ID or redirect URI, instead of building a link that fails on the server.
[0.95.0] - 2026-10-13¶
Scopes are categories now. Request what your app uses. Breaking: the default scopes change without an error, and the metabolic map and capability names move; see Upgrading below, or hand the migration prompt to your coding agent.
Changed¶
- The default scope of
Client.login(),client.oauth.authorization_url(), the FastAPI plugin and the Streamlit helper is one constant,sweatstack.constants.DEFAULT_SCOPE:openid profile email activities:read offline_access. Addwellness:read,reproductive_health:read,workouts:writeorsharing:readwhen your app uses them. client.teams.authorize()grantsactivities:readby default; only the delegable scopes are ever granted.client.oauth.userinfo()needs theopenidscope; the name claims needprofile, the email needsemail.client.profile.status()needs no scope.Application.default_scopesandApplication.purposesreplacecapabilitiesonclient.applications(the old field is still accepted and translated).- The metabolic map responses are the markers themselves: no
inputs, nomarkerswrapper; the VO2 map hasvo2max_relative. - The
dailiescapability and its issue codes arewellness,wellness_not_grantedandwellness_no_source.
Added¶
sweatstack.__version__is the installed version.
Fixed¶
- Token calls from the FastAPI and Streamlit integrations (login, refresh and delegated tokens)
now send the
python-sweatstack/<version>User-Agent, like every other request the SDK makes. Before, they identified themselves as plain httpx.
Upgrading¶
| 0.94 | 0.95 |
|---|---|
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 |
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 |
userinfo() with profile |
userinfo() with openid profile email; without openid it raises SweatStackAuthError |
response.markers.fatmax on a metabolic map |
response.fatmax |
response.inputs.body_mass on a metabolic map |
not published; response.vo2max_relative on the VO2 map |
status.capabilities["dailies"] |
status.capabilities["wellness"] |
An app registered before the platform release keeps working with data:read and data:write
for six months; see the platform changelog.
Migration prompt. Paste this into your coding agent:
Upgrade this codebase to sweatstack 0.95, which replaces the data:read and data:write scope
bundles with per-category scopes. The upgrade table is the "Upgrading" section of the 0.95.0
entry in https://github.com/SweatStack/sweatstack-python/blob/main/CHANGELOG.md
(also at https://docs.sweatstack.no/learn/python/upgrading/). Read it first.
Find every place the app requests or grants scopes (Client.login(), oauth.authorization_url(),
sweatstack.fastapi.configure(scopes=...), StreamlitAuth(scopes=...) and SWEATSTACK_SCOPES,
teams.authorize(scopes=...)) and:
1. Replace data:read and data:write with the categories the app uses: activities, wellness,
reproductive_health, workouts and sharing, each as :read or :write. Start from
sweatstack.constants.DEFAULT_SCOPE (openid profile email activities:read offline_access)
and add to it. Do not keep the old bundles.
2. Where the app calls userinfo(), request openid, plus profile and email for the claims it
shows.
3. On the metabolic maps, read response.<marker> instead of response.markers.<marker>, drop
any use of response.inputs, and read status.capabilities["wellness"] where the app read
["dailies"].
Then run the code and the tests. A category the app forgot surfaces as SweatStackAuthError
on the first call that needs it; add that category, not a bundle.
[0.94.0] - 2026-10-12¶
A client's tokens come from one source. Upgrade every app that other people sign in to.
Security¶
- A
Clientin a process shared by several users (Streamlit, FastAPI, any server) could pick up a refresh token, or a whole token pair, that another user's client had saved on that machine, and act as that user. Credentials now come from a single source: the arguments you pass, else the environment, else the tokens saved byauthenticate(). A client never combines halves from different sources, and writes the saved tokens only when they came from there. The Streamlit and FastAPI integrations never read or write saved tokens.
Fixed¶
- Streamlit apps behind a proxy no longer fail after the access token expires. With a proxy
that forwards
X-SweatStack-Refresh-Token(the SweatStack Streamlit template does), the session renews its own token and keeps the renewed pair across reruns. Streamlit apps in OAuth mode refresh once per token lifetime instead of once per rerun. - A refresh response that carries a new refresh token replaces the old one.
StreamlitAuth.select_user()with a user that gets no refresh token (admin access) no longer keeps the previous user's refresh token.
Changed¶
Client(..., ambient_credentials=False)builds a client that never reads the environment or the saved tokens and never writes them. Use it for per-user clients in your own server.oauth.exchange_code()no longer saves the tokens on the machine by default; passpersist=Truefor that.authenticate()still saves them.StreamlitAuth.behind_proxy()checks token expiry again. Without a forwarded refresh token, calls after expiry raiseSweatStackTokenRefreshErrorwith a reload hint, where the API returned a 401.
Upgrading¶
| 0.93 | 0.94 |
|---|---|
Client(api_key=...) without a refresh token renewed with SWEATSTACK_REFRESH_TOKEN or the saved refresh token |
Silent: it never borrows one, and raises SweatStackTokenRefreshError on expiry. Pass refresh_token= too, or pass neither to use the environment or the saved tokens |
oauth.exchange_code(...) saved the tokens on this machine |
Silent: pass persist=True to keep that |
StreamlitAuth.behind_proxy() sent the handshake token as it was after expiry (a 401 from the API) |
Forward X-SweatStack-Refresh-Token from the proxy and the session renews itself. Without it, calls after expiry raise SweatStackTokenRefreshError with a reload hint |
[0.93.0] - 2026-10-09¶
Issues are only about the data your app uses. Requires the SweatStack API release that adds
capabilities to applications (see the platform changelog).
Changed¶
- BREAKING:
issue.statusis anIssueStatus,action_requiredorsyncing, onoauth.userinfo()andprofile.status(). Theunavailableissues (activity_history_unavailable,dailies_unavailable,workouts_unavailable) are gone: what no connected source provides is incapabilities, whereunavailablemeans the user could still connect a source that provides it. Three new codes, eachaction_requiredwith adestination:activities_blocked_upstream,dailies_no_source,workouts_no_source. A client on 0.90.0 or later accepts them without an upgrade (StatusIssueCodeis open). ApplicationCreate.capabilitiesandApplicationResponse.capabilities: the capabilities an app declares it uses (list[Capability]; omit it for the default,activitiesandactivity_history). SweatStack reports issues only for these.
Upgrading¶
| 0.92 | 0.93 |
|---|---|
issue.status is a CapabilityStatus |
IssueStatus: action_required or syncing. The .value strings are unchanged |
Branching on issue.code being activity_history_unavailable, dailies_unavailable or workouts_unavailable |
Silent: these codes are never sent. Read profile.status().capabilities instead, where unavailable means no connected source provides it |
| Only the known issue codes | Also activities_blocked_upstream, dailies_no_source and workouts_no_source: each action_required with a destination; handle them like any other issue |
| An app that uses dailies or workouts | Declare them in the app's capabilities (ApplicationCreate.capabilities); issues are reported only for declared capabilities |
[0.92.0] - 2026-10-08¶
Portal links are created with the user's token, when the user acts. Requires the SweatStack API
release that replaces action_url with destination (see the platform changelog).
Changed¶
- BREAKING:
issue.action_urlis replaced byissue.destinationonoauth.userinfo()andprofile.status(). Show a button only whendestinationis set, and on click pass it toportal.sessions.create()for the URL. A destination the client does not know yet parses instead of failing. - BREAKING:
portal.sessions.create()authenticates with the client's user token, not the app'sclient_idandclient_secret. Call it on a client that holds the user's access token; the Portal is branded for the app that token was issued to. A delegated token raisesSweatStackAuthError. - Without
return_urlthe Portal no longer links back to your app's URL: it tells the user to close the page.return_urlmust equal or sit under one of your app's registered redirect URIs.
Upgrading¶
| 0.91 | 0.92 |
|---|---|
user.issue.action_url |
client.portal.sessions.create(user.issue.destination, return_url=...).url |
Client(client_id=..., client_secret=...).portal.sessions.create(...) |
the same call on the client holding the user's token |
portal.sessions.create(destination) without return_url linking back to your app |
Silent: the Portal tells the user to close the page; pass return_url= (under a registered redirect URI) to link back |
[0.91.0] - 2026-10-08¶
One namespace per API resource: client.activities.list() instead of client.get_activities().
Every name now follows from the URL (/api/v1/activities/... is client.activities), so the
REST API reference doubles as the SDK's map. This is a breaking release with a mechanical
upgrade; see Upgrading below, or hand the migration prompt to your coding agent. Calling a
removed name raises an AttributeError that names its replacement.
Changed¶
- BREAKING: methods moved to resource namespaces:
activities,traces,tests,dailies,profile,users,teams,portalandoauth, onClientand at module level (sweatstack.activities.list()). The full table is under Upgrading. - BREAKING:
traces.replace()andtests.replace()are the oldupdate_trace()andupdate_test(). The new name says what they do: every field you leave out is cleared, including a trace'stest_id. - BREAKING:
sport=replacessports=on every filter, as on the server (which deprecatedsports). It takes one sport or a list:sport="cycling"orsport=["cycling", "running"].tags=andmetrics=also take a single value now. - BREAKING:
activities.latest()calls the API's/activities/latest: it takes onlysport=, and returnsNonewhen there is no activity (it raisedStopIteration). - BREAKING: keyword-only arguments where they were positional:
metric=onactivities.mean_max()andactivities.awd(),segmentation_on=andmetrics=onactivities.data(),first_name=andlast_name=onusers.create(),only_root=onprofile.sports(),scopes=onteams.authorize(), and every argument ofoauth.authorization_url()andoauth.exchange_code(). - BREAKING: longitudinal
date=/window_days=(deprecated since 0.70) are gone; usestart=andend=, typed asdateobjects (start=date(2026, 1, 1)).sport=is a required argument of all three longitudinal methods; the API rejected a request without one. - BREAKING:
activities.upload()returns the processing status of each file (list[SourceResponse]) instead of a raw dict, and no longer takessport=: the server never read it. A CSV carries its sport in asportcolumn (an Open Sport Taxonomy code such ascycling.road). - BREAKING:
oauth.exchange_code()no longer takesredirect_uri=; the token endpoint never read it. - BREAKING:
users.retrieve(user_id)is the server'sGET /users/{id}, which only finds users you manage. Find anyone else withusers.list(name=...), which returns every user whose name contains the text, ignoring case. - BREAKING:
switch_user()andswitch_back()are removed. They changed which user a shared client acted as, invisibly to everything else holding it.client.delegated_client(user)does the same job and returns a new client. It takes a user ID or aUserSummary, not a name:switch_user("Carla")becomes a lookup withusers.list(name="Carla")first. StreamlitAuth.select_activity()takessport=instead ofsports=.-
Clients from
delegated_client()andprincipal_client()keep the app'sclient_idandclient_secret, soportal.sessions.create()works on them. -
Automatic retries:
GET,PUTandDELETErequests are retried up to twice after a connection error, a timeout, or a 408, 429 or 5xx response, with exponential backoff and the server'sRetry-After(at most 30 s of waiting per call).POSTis never retried, so a create can't happen twice.Client(max_retries=0)turns retries off, e.g. inside a web request.
Added¶
Client(timeout=60.0, max_retries=2): the timeout in seconds, and how often to retry. Clients fromdelegated_client()andprincipal_client()keep both.- One connection pool per client, reused across requests (each request opened a new
connection).
client.close()releases it;with Client() as client:closes on exit. users.retrieve(),users.update()(changes only the fields you pass) andusers.delete()for managed users.users.list(include_managed=, include_shared=, name=)andteams.users(team_id, name=).SourceResponseandSourceError, the upload status models.vo2=ontraces.create()andtraces.replace(); the field was already onTraceDetails.
Fixed¶
activities.watch_backfill_status(auto_reconnect=True)reconnects after a dropped connection; it raised instead.activities.backfill_status()andwatch_backfill_status()return the server's updates. Its timestamps carry no UTC offset, which failed validation, and every update was skipped silently. A line the client cannot parse is now logged as a warning.StreamlitAuth.select_user()stores the selected user's own refresh token. It kept the signed-in user's, so after the first token refresh the app silently showed the signed-in user's data under the selected user's name.StreamlitAuthkeeps the app'sclient_idandclient_secretafter switching users.- Threads sharing a client refresh an expired token once; they all refreshed, which fails with rotating refresh tokens.
activities.watch_backfill_status()no longer times out on a quiet stream: streams have no read timeout.- Searching users by name ignores case;
get_user("Carla")found nobody where"carla"worked.
Removed¶
- BREAKING: the
sweatlabandsweatshellcommands, their example notebook, and the[jupyter]extra. In any notebook:uv add "sweatstack[pandas]" jupyterlab, thensweatstack.authenticate()in the first cell. - BREAKING:
client.jwt; useclient.api_key.
Upgrading¶
Removed names raise an AttributeError naming the replacement, so running your code points at
each change. The module-level functions moved the same way: sweatstack.get_activities() is
sweatstack.activities.list(). Only from sweatstack import get_activities fails with Python's
plain ImportError; look the name up in the table.
| 0.90 | 0.91 |
|---|---|
get_activities |
client.activities.list() |
get_activity |
client.activities.retrieve(activity_id) |
get_latest_activity |
client.activities.latest() |
get_activity_data |
client.activities.data(activity_id) |
get_activity_mean_max |
client.activities.mean_max(activity_id, metric=...) |
get_activity_awd |
client.activities.awd(activity_id) |
get_latest_activity_data |
client.activities.data(client.activities.latest().id) |
get_latest_activity_mean_max |
client.activities.mean_max(client.activities.latest().id, metric=...) |
get_longitudinal_data |
client.activities.longitudinal.data(...) |
get_longitudinal_mean_max |
client.activities.longitudinal.mean_max(...) |
get_longitudinal_awd |
client.activities.longitudinal.awd(...) |
upload |
client.activities.upload(files) |
get_backfill_status |
client.activities.backfill_status() |
watch_backfill_status |
client.activities.watch_backfill_status() |
set_activity_app_metadata |
client.activities.app_metadata.set(activity_id, data=...) |
delete_activity_app_metadata |
client.activities.app_metadata.delete(activity_id) |
get_traces |
client.traces.list() |
create_trace |
client.traces.create(...) |
update_trace |
client.traces.replace(trace_id, ...) |
delete_trace |
client.traces.delete(trace_id) |
set_trace_app_metadata |
client.traces.app_metadata.set(trace_id, data=...) |
delete_trace_app_metadata |
client.traces.app_metadata.delete(trace_id) |
get_tests |
client.tests.list() |
get_test |
client.tests.retrieve(test_id) |
create_test |
client.tests.create(...) |
update_test |
client.tests.replace(test_id, ...) |
delete_test |
client.tests.delete(test_id) |
set_test_app_metadata |
client.tests.app_metadata.set(test_id, data=...) |
delete_test_app_metadata |
client.tests.app_metadata.delete(test_id) |
get_dailies |
client.dailies.list(measure, start=..., end=...) |
set_daily |
client.dailies.set(measure, date=..., value=...) |
delete_daily |
client.dailies.delete(measure, date=...) |
get_profile_status |
client.profile.status() |
get_sports |
client.profile.sports() |
get_tags |
client.profile.tags() |
set_user_app_metadata |
client.profile.app_metadata.set(data=...) |
delete_user_app_metadata |
client.profile.app_metadata.delete() |
get_users |
client.users.list() |
get_user |
client.users.list(name=...): every match, so check the length |
create_user |
client.users.create(first_name=...) |
get_teams |
client.teams.list() |
get_team_users |
client.teams.users(team_id) |
get_team_user |
client.teams.users(team_id, name=...): every match |
get_authorized_teams |
client.teams.authorized() |
authorize_team |
client.teams.authorize(team_id) |
create_portal_session |
client.portal.sessions.create(destination) |
get_userinfo |
client.oauth.userinfo() |
get_authorization_url |
client.oauth.authorization_url(...) |
exchange_code_for_token |
client.oauth.exchange_code(...) |
generate_pkce_params |
client.oauth.generate_pkce_params() |
switch_user |
client.delegated_client(user): a new client; this one is left unchanged |
switch_back |
keep the original client, or client.principal_client() |
client.jwt |
client.api_key |
Changes that a rename alone doesn't cover. Most raise a TypeError naming the argument; the
ones marked silent don't fail loudly, so check for them.
| 0.90 | 0.91 |
|---|---|
sports=["cycling"] (every filter, and StreamlitAuth.select_activity) |
sport="cycling" or sport=["cycling", "running"] |
get_activity_mean_max(id, "power"), get_activity_awd(id, "power") |
activities.mean_max(id, metric="power"), activities.awd(id, metric="power") |
get_activity_data(id, "power", ["power"]) |
activities.data(id, segmentation_on="power", metrics=["power"]) |
create_user("Carla", "Smith") |
users.create(first_name="Carla", last_name="Smith") |
get_sports(True), authorize_team(id, scopes) |
profile.sports(only_root=True), teams.authorize(id, scopes=scopes) |
Positional arguments to get_authorization_url() / exchange_code_for_token() |
Keywords: oauth.authorization_url(client_id=..., ...) |
get_latest_activity(start=, end=, tag=) |
activities.latest(sport=); for the rest, activities.list(start=..., end=..., tags=..., limit=1) |
get_latest_activity() raising StopIteration when there is none |
Silent: activities.latest() returns None; check before using .id |
get_longitudinal_*(date=..., window_days=...) |
start= and end= |
get_longitudinal_*(...) without sport |
sport= is required (the API already rejected a request without one) |
upload(files, sport=...) |
activities.upload(files): no sport=; a CSV needs a sport column |
upload(...) returning a dict |
Silent: activities.upload(...) returns list[SourceResponse]; read .status / .error |
exchange_code_for_token(..., redirect_uri=...) |
oauth.exchange_code(...) without redirect_uri= |
get_user("Carla") returning one UserSummary |
Silent: users.list(name="Carla") returns a list, every match; search_mode= is gone |
switch_user("Carla") (a name) |
client.delegated_client(users.list(name="Carla")[0]): a name is not accepted |
client.jwt |
client.api_key |
sweatlab, sweatshell, pip install "sweatstack[jupyter]" |
uv add "sweatstack[pandas]" jupyterlab; sweatstack.authenticate() in the first cell |
New defaults that change behaviour without changing code: GET, PUT and DELETE requests are
retried (Client(max_retries=0) restores single attempts), and requests time out after 60 s
(Client(timeout=...)).
Migration prompt. Paste this into your coding agent:
Upgrade this codebase to sweatstack 0.91, which moved every method to a resource namespace.
The full upgrade guide, with both tables, is the "Upgrading" section of the 0.91.0 entry in
https://github.com/SweatStack/sweatstack-python/blob/main/CHANGELOG.md
(also at https://docs.sweatstack.no/learn/python/upgrading/). Read it first.
Find every use of the sweatstack client (a Client instance, the sweatstack module, and
StreamlitAuth or FastAPI user clients) and:
1. Rename calls per the first table, e.g. get_activities() -> activities.list(),
get_activity_data(id) -> activities.data(id),
get_longitudinal_mean_max(...) -> activities.longitudinal.mean_max(...),
update_trace(id, ...) -> traces.replace(id, ...), get_users() -> users.list().
2. Apply every row of the second table. In particular:
- sports= -> sport= (one value or a list).
- Pass by keyword: metric= (mean_max, awd), segmentation_on= and metrics= (data),
first_name= and last_name= (users.create), only_root= (profile.sports),
scopes= (teams.authorize), and all arguments of oauth.authorization_url() and
oauth.exchange_code().
- Longitudinal methods: sport= is required, and date=/window_days= become start=/end=
(datetime.date objects).
- activities.latest() takes only sport= and returns None when there is no activity:
handle None.
- activities.upload() takes no sport= (a CSV needs a sport column) and returns
list[SourceResponse], not a dict.
- oauth.exchange_code() takes no redirect_uri=.
- get_user(x) -> users.list(name=x), which returns a list of every match.
- switch_user(user) -> athlete = client.delegated_client(user_id_or_summary); use that
new client for the athlete's calls. It doesn't accept a name: look the user up first.
switch_back() -> keep using the original client.
- client.jwt -> client.api_key.
3. Replace get_latest_activity_data(...) with activities.data(activities.latest().id, ...),
after checking that latest() returned an activity.
4. If the project used sweatlab, sweatshell or the [jupyter] extra, depend on
"sweatstack[pandas]" (or [polars]) and jupyterlab instead.
Then run the code and the tests. Fix any AttributeError (its message names the replacement)
and TypeError (a renamed or keyword-only argument). Do not add compatibility shims.
[0.90.0] - 2026-09-28¶
Changed¶
- BREAKING: mean-max curves are one row per duration.
get_activity_mean_max,get_latest_activity_mean_maxandget_longitudinal_mean_maxreturnduration, the metric (W or m/s) andstart(UTC timestamp of the best effort), plusactivity_id,sportandafteron the longitudinal curve. By default 19 durations from 1 s to 6 h; the curve can rise again at longer durations and is returned as it is.
Added¶
durations=on all three mean-max methods:Nonefor the 19 defaults,"all"for the full grid, or a list of seconds. Keyword-only.
Removed¶
- BREAKING:
segmentationonget_activity_mean_maxandget_latest_activity_mean_max(it never reduced the payload; the server ignores it). It was positional: passingTruein that slot now raisesTypeError. - BREAKING:
byonget_longitudinal_mean_max, and itsDeprecationWarning. Every curve is duration-oriented.
Upgrading¶
| 0.89 | 0.90 |
|---|---|
A mean-max frame with one row per intensity: the metric value, and the duration it was held |
Silent: one row per duration, 19 by default: duration, the metric (power or speed) and start; the longitudinal curve adds activity_id, sport and after. The column names still exist, so code keeps running with the axes swapped: df[df.power >= 300] becomes df[df.duration == 300] |
| The full curve | durations="all", or a list of seconds |
get_activity_mean_max(id, "power", True) or segmentation=True |
Drop the argument |
get_longitudinal_mean_max(..., by="duration") or by="intensity" |
Drop by=; every curve is by duration |
[0.89.0] - 2026-09-28¶
Frames on your terms. Every method that returns a collection takes output=:
"pandas", "polars", "arrow" or "bytes" for time-series endpoints, and "models"
(default), "pandas", "polars" or "arrow" for list endpoints. Set it per call, per client
(Client(output="polars")) or once for everything (sweatstack.set_output("polars")). When you
don't say, time series come back in the frame library you installed: Polars, then pandas,
then Arrow.
Four breaking changes come with it; the upgrade is mechanical, see Upgrading below.
Added¶
get_profile_status()(beta): why this user has little or no data, and what the account can supply. Returnsissue(None, or one{code, status, message, action_url}) andcapabilities(activities,activity_history,dailies,workouts, eachready,syncing,action_requiredorunavailable). Acceptsdata:readorprofile.get_userinfo()now carries the sameissue(beta). The whole integration isif user.issue: banner(user.issue.message, user.issue.action_url);action_urlisNoneon delegated tokens and on issues the user cannot act on.create_portal_session(destination, return_url=None)(beta): mints a SweatStack Portal link branded for the client's app, using the app's ownclient_id/client_secretfrom the constructor and no user token. Works fromsweatstack.fastapidependencies andStreamlitAuthas is.- New models and enums:
AccountStatusResponse,StatusIssueResponse,Capability,CapabilityStatus,StatusIssueCode,PortalDestination,PortalSessionResponse.StatusIssueCodeandCapabilityare open sets: values a newer server adds parse as pseudo-members instead of failing validation. output=onget_activity_data,get_activity_mean_max,get_activity_awd,get_latest_activity_data,get_latest_activity_mean_max,get_longitudinal_data,get_longitudinal_mean_max,get_longitudinal_awd,get_activities,get_traces,get_testsandget_dailies.Client(output=...)andsweatstack.set_output(...)to choose once. A per-call value always wins. Delegated clients inherit the setting.- Polars frames keep the compact wire dtypes (Int16, Float32, Categorical, Duration) and
give nested fields as typed structs (
df.unnest("summary")), as do Arrow tables. On time-series endpoints Arrow tables are the response as-is."bytes"is the raw parquet, ready forduckdb.sql("... from 'file.parquet'"). sweatstack[polars]andsweatstack[arrow]extras.[arrow]is pyarrow alone: whatoutput="arrow"needs, and what DuckDB needs to query any in-memory frame. See the README's "Using DuckDB" for the three routes.
Fixed¶
whoami()raisedAttributeErroron every call since the helper it relied on was removed. It resolves the token's user throughget_user()again, for principal and delegated clients alike, and still needs noprofilescope.
Changed¶
- Breaking: pandas is no longer installed by default. Install
sweatstack[pandas](pandas + pyarrow),sweatstack[polars]orsweatstack[arrow]. Thestreamlitandjupyterextras include pandas. FastAPI services and webhook consumers can stay on the base package. Asking for an output whose library is missing raises anImportErrornaming the extra to install. - Breaking: the default frame library is the one you installed, in the order Polars,
pandas, Arrow. An environment with pandas and Polars now gets Polars frames from the time-series
methods unless you set
output(per call,Client(output="pandas"), orsweatstack.set_output("pandas")once). - Breaking:
as_dataframe=Trueis removed. Useoutput="pandas". - Breaking: no frame carries an index any more, on any backend.
timestamp(time series), the metric value (mean-max and AWD curves) anddate(dailies) are now regular columns, in first position. The set of columns is unchanged. Code that relied on the index needs.set_index("timestamp")(or"power","date", ...) once, or should use the column directly. This also applies to the fatigue mean-max (after=) frame, which was previously re-indexed by the client. - pandas frames keep the float64 / nanosecond dtype policy. Polars and Arrow do not upcast.
Upgrading¶
- Change the install line:
uv add "sweatstack[pandas]"(or[polars], or[arrow]for DuckDB). Streamlit and Jupyter users:sweatstack[streamlit]/sweatstack[jupyter]already include pandas. - To keep pandas frames in an environment that also has Polars, add
sweatstack.set_output("pandas")once (orClient(output="pandas")). - Replace
as_dataframe=Truewithoutput="pandas". - Search for
.index,.loc[<timestamp>],.resample(,.plot()on frames from the time-series, mean-max, AWD and dailies methods. Where the index mattered, add.set_index("timestamp")(or the metric name, or"date") right after the call. - Silent:
as_dataframe=Trueonget_activitiesandget_traceskept nested fields as dict columns (df["summary"][0]["power"]).output="pandas"flattens them to dotted columns (df["summary.power.mean"]); Polars and Arrow give structs (df.unnest("summary")).
[0.88.0] - 2026-08-06¶
Changed¶
- Breaking:
create_trace,update_trace,create_test, andupdate_testnow require timezone-aware datetimes fortimestamp,start, andend. A naive datetime raisesValueErrorbefore the request is sent. Attach a zone, e.g.datetime(..., tzinfo=ZoneInfo("Europe/Amsterdam"))ordatetime.now(timezone.utc); the offset is stored alongside the instant. start,end, andtimestampon responses are absolute UTC instants (ISO 8601 with aZsuffix), no longer a fixed per-record local offset. For wall-clock display use the companionstart_local/end_local/timestamp_localfields, which are always present. The Streamlit activity selector now labels activities by their local date.- Requires a SweatStack server with offset-based timezone handling. Against an older server the aware-datetime writes still work, but responses keep the previous fixed-offset
start/end/timestamp.
Removed¶
- Token refresh no longer sends a
tzfield. The server derives all timezone information from the offsets stored with each record, so the client has nothing to pass.
Upgrading¶
| 0.87 | 0.88 |
|---|---|
create_trace(timestamp=datetime(2026, 1, 1, 10, 0)), and start/end on create_test, update_test, update_trace |
Aware: datetime(2026, 1, 1, 10, 0, tzinfo=ZoneInfo("Europe/Amsterdam")) or datetime.now(timezone.utc). Naive raises ValueError |
activity.start, trace.timestamp shown as the athlete's wall-clock time |
Silent: these are UTC now. Show start_local, end_local and timestamp_local, which are always present |
[0.87.0] - 2026-06-30¶
Changed¶
- Breaking: the codec previously named NLEC is now AISC (Adaptive Intensity Segmentation Codec), and its parameters are renamed:
nlec_onis nowsegmentation_on(onget_activity_data,get_latest_activity_data,get_longitudinal_data) andnlecis nowsegmentation(onget_activity_mean_max,get_latest_activity_mean_max). No backwards-compatible aliases.
Upgrading¶
| 0.86 | 0.87 |
|---|---|
nlec_on= on get_activity_data, get_latest_activity_data, get_longitudinal_data |
segmentation_on= |
nlec= on get_activity_mean_max, get_latest_activity_mean_max |
segmentation= (removed in 0.90) |
[0.86.0] - 2026-06-18¶
Changed¶
- Breaking: renamed the adaptive-sampling parameters to NLEC (near-lossless effort codec).
adaptive_sampling_onis nownlec_on(onget_activity_data,get_latest_activity_data,get_longitudinal_data) andadaptive_samplingis nownlec(onget_activity_mean_max,get_latest_activity_mean_max). No backwards-compatible aliases; requires the SweatStack server 0.107.0 or later.
Upgrading¶
| 0.85 | 0.86 |
|---|---|
adaptive_sampling_on= on get_activity_data, get_latest_activity_data, get_longitudinal_data |
nlec_on= (segmentation_on= from 0.87) |
adaptive_sampling= on get_activity_mean_max, get_latest_activity_mean_max |
nlec= (segmentation= from 0.87; removed in 0.90) |
[0.85.0] - 2026-06-17¶
Changed¶
get_longitudinal_mean_max(by=...)now defaults toNone: thebyparameter is omitted from the request so the server picks the orientation. Forafter(fatigue) withmetric="power"the server now defaults toby="duration"; every other case staysby="intensity". The returned frame is indexed on whichever orientation the server used. Passingbyexplicitly still works.
Deprecated¶
by="intensity"for theafter(fatigue) case is deprecated and now raises aDeprecationWarning;by="duration"is the default and only supported orientation going forward. Passby="duration"or leavebyunset. (by="intensity"remains the only orientation formetric="speed".)
[0.84.0] - 2026-06-17¶
Added¶
get_longitudinal_mean_max(by=...): passby="duration"(withafter, forpower) to get the fatigue curve indexed by duration instead of by intensity. Defaultby="intensity"is unchanged.
Changed¶
- Require
pyarrow>=20: pyarrow 18/19 fail to read the server's parquet ("Repetition level histogram size mismatch") for longitudinal and adaptive-sampling responses; pyarrow 20+ reads them correctly.
[0.83.0] - 2026-06-16¶
Added¶
- Trace responses now include
testandtest_match(server-side test matching).
Fixed¶
- Fixes Dailies response schema.
[0.82.0] - 2026-06-16¶
Added¶
get_longitudinal_mean_max(after=...): fatigue-state mean-max. Pass one or more thresholds (kJ of work forpower; metres of distance forspeed, experimental) to get, per state, the mean-max over the portion of each ride after that threshold. The returned DataFrame stays metric-indexed with an addedaftercolumn. Max 5 states; the date range is capped at 1 year whenafteris used.
[0.81.0] - 2026-06-16¶
The SweatStack API has fully adopted OpenSportTaxonomy
(OST), and so has this client. sweatstack.Sport is now the OST Sport type instead of a bespoke enum.
This is a breaking change for code that uses Sport.
Changed (breaking)¶
sweatstack.Sportis nowopen_sport_taxonomy.Sport— a rich type (.code,.label,.modifiers,.parent,.is_subsport_of(),.resolve(),Sport.parse(),Sport.all()) rather than a string enum. There are noSport.cycling_road-style members; construct a known sport withSport("cycling.road")or parse external input withSport.parse(value). The bespoke helpers (root_sport(),parent_sport(),is_sub_sport_of(),is_root_sport(),display_name()) are removed in favour of OST's native API.- Sport values are now OST values, e.g.
cycling.trainer→cycling+stationary,cycling.tt→cycling.time_trial,cross_country_skiing→xc_skiing,unknown→generic. Response data, longitudinal DataFrames andget_sports()all return OST values; requests send OST values.
Added¶
sweatstack.Modifier(re-exported from OpenSportTaxonomy) for inspecting sport modifiers. (For typed sport annotations,StandardSportis available fromopen_sport_taxonomydirectly.)open-sport-taxonomy[pydantic]is now a runtime dependency. Response models consumesportvia OST's permissiveSportField, so sports newer than the bundled taxonomy are preserved rather than rejected.
Upgrading¶
| 0.80 | 0.81 |
|---|---|
Sport.cycling_road and other enum members |
Sport("cycling.road"); Sport.parse(value) for external input |
sport.value |
str(sport) (with modifiers) or sport.code |
sport.display_name() |
sport.label |
sport.parent_sport() |
sport.parent |
sport.is_sub_sport_of(x) |
sport.is_subsport_of(x) (one Sport; any(...) for a list) |
sport.root_sport(), sport.is_root_sport() |
Sport(sport.code.split(".")[0]), "." not in sport.code and not sport.modifiers |
for s in Sport |
for s in Sport.all() |
Hardcoded cycling.trainer, running.treadmill, rowing.ergometer, cycling.tt, cycling.mountainbike, cross_country_skiing[.classic\|.skate], unknown |
Silent in string comparisons: cycling+stationary, running+stationary, rowing+stationary, cycling.time_trial, cycling.mountain, xc_skiing[.classic\|.skate], generic |
Migration prompt. Paste this into your coding agent:
Migrate this codebase to sweatstack 0.81.0, which replaces its custom `Sport` enum with the
OpenSportTaxonomy type (`open_sport_taxonomy.Sport`). `from sweatstack import Sport` is now that type.
1. Construction (there are no enum members like `Sport.cycling_road`):
- Known sport in app code -> `Sport("cycling.road")` (raises on an unknown code/modifier).
- From an API/string value -> `Sport.parse(value)` (permissive; never raises; preserves unknown).
- For typed annotations/autocomplete of the standard catalogue ->
`from open_sport_taxonomy import StandardSport` (a Literal).
2. These sport VALUES changed; update hardcoded strings or members:
cycling.trainer->cycling+stationary, running.treadmill->running+stationary,
rowing.ergometer->rowing+stationary, cycling.tt->cycling.time_trial,
cycling.mountainbike->cycling.mountain, cross_country_skiing[.classic|.skate]->xc_skiing[...],
unknown->generic. ("stationary" etc. are now modifiers, appended with `+`; see sport.modifiers.)
3. Methods / attributes:
Sport.cycling_road -> Sport("cycling.road")
sport.value -> str(sport) (canonical, incl. modifiers) or sport.code
sport.display_name() -> sport.label
sport.parent_sport() -> sport.parent
sport.is_sub_sport_of(x) -> sport.is_subsport_of(x) (x is a single Sport; for a list use
any(sport.is_subsport_of(s) for s in xs))
sport.root_sport() -> Sport(sport.code.split(".")[0])
sport.is_root_sport() -> ("." not in sport.code and not sport.modifiers)
for s in Sport: ... -> for s in Sport.all(): ...
4. Equality works: `activity.sport == Sport("cycling+stationary")`. Compare against the NEW value.
After editing, run the test suite and fix any remaining references. Do not add a compatibility shim;
migrate call sites to the OST API directly.
[0.80.0] - 2026-06-12¶
Changed¶
- Makes the Sport enum forward compatible with OpenSportTaxonomy sports.
[0.79.0] - 2026-05-28¶
Fixed¶
sweatstack.fastapi: a single page load that fans out into many concurrent requests no longer produces a burst of duplicate/oauth/tokenrefreshes when the session's access token is on the edge of expiring. Concurrent requests for the same session now serialise on a per-session lock and share the resulting refresh, so an N-way race collapses to a single token endpoint call.
Added¶
sweatstack.fastapi.AccessTokenCache(Protocol) andInMemoryAccessTokenCache(default), exposed viaconfigure(access_token_cache=...). The default is correct for single-worker deployments. Multi-worker deployments that want cross-worker de-duplication can plug in a shared-state implementation (e.g. Redis-backed). The default implementation is bounded by an LRU cap (10k entries) and uses striped locking, so memory growth is bounded regardless of session churn.sweatstack.fastapi: refresh-token rotation is now handled transparently. If a future/oauth/tokenresponse returns a new refresh token, the cache installs the result under both the old and new keys (so in-flight peers still hit), and the cookie / token store is rewritten with the new value. Current SweatStack servers do not rotate, so this is a forward-compatibility measure.sweatstack.fastapi.RefreshLockTimeout: a waiter that cannot acquire the per-session refresh lock within 15 seconds now raises this rather than blocking a FastAPI threadpool worker indefinitely. The/oauth/tokencall itself also has an explicit 10-second timeout.sweatstack.fastapi: debug logs (sweatstack.fastapi.dependencieslogger) on every cache hit / seed / refresh / failure, keyed by a short SHA-256 fingerprint of the refresh token. Enable withlogging.getLogger("sweatstack.fastapi.dependencies").setLevel(logging.DEBUG).
[0.78.0] - 2026-05-19¶
Added¶
update_trace()anddelete_trace()are now exposed as module-level functions (e.g.sweatstack.update_trace(...)), matching the rest of the CRUD surface. They were previously only reachable via aClientinstance.- The package now declares an
__all__, sofrom sweatstack import *and tooling that inspects the public surface (Sphinx, IDEs, type-checkers) see a well-defined list.
Changed¶
- Minimum Python version is now declared as
>=3.10(the code already required 3.10+ syntax; the previous>=3.9declaration was incorrect). - Exception types in docstrings now reference the typed hierarchy introduced in 0.76.0 (
SweatStackAPIError,SweatStackNotFoundError,SweatStackAuthError,SweatStackBadRequestError) instead of the now-incorrectHTTPStatusError. - Breaking: with
__all__in place, the helpers that leaked to the top level by accident are no longer there:sweatstack.make_dataframe_streamlit_compatible,convert_to_standard_dtypesanddecode_jwt_bodylive insweatstack.utils,DEFAULT_URLinsweatstack.constants.
Upgrading¶
| 0.77 | 0.78 |
|---|---|
from sweatstack import make_dataframe_streamlit_compatible (also convert_to_standard_dtypes, decode_jwt_body) |
from sweatstack.utils import make_dataframe_streamlit_compatible |
sweatstack.DEFAULT_URL |
sweatstack.constants.DEFAULT_URL |
| Python 3.9 | Python 3.10 or later |
[0.77.1] - 2026-05-19¶
Fixed¶
- Reverted unrelated OpenAPI schema drift.
[0.77.0] - 2026-05-19¶
Added¶
- Link a trace to a test via the new
test_idargument oncreate_trace()andupdate_trace(). A linked trace appears in the test's traces regardless of its timestamp. get_test()acceptstrace_resolution=TraceResolution.linkedto return only traces explicitly linked to the test. Defaults toTraceResolution.auto(unchanged behaviour).
Changed¶
update_trace()replaces all fields, includingtest_id. Callers that omittest_idwill clear any existing link — pass it back in to keep the trace linked.
[0.76.2] - 2026-04-27¶
Fixed¶
registered_atonUserInfoResponseandUserResponsenow accepts both aware and naive datetimes, working around the API returning naive timestamps for this field.
[0.76.1] - 2026-04-27¶
Fixed¶
- Local datetime fields (
start_local,end_local,timestamp_local) in OpenAPI schemas corrected fromAwareDatetimetoNaiveDatetime.
[0.76.0] - 2026-04-27¶
Added¶
- Typed exception hierarchy (
sweatstack.exceptions). All API errors now raise specific exception types:SweatStackAuthError(401/403),SweatStackNotFoundError(404),SweatStackRateLimitError(429),SweatStackBadRequestError(other 4xx),SweatStackServerError(5xx). Transport failures raiseSweatStackConnectionError. - Structured error metadata on all API exceptions:
status_code,url,method,request_id,body.
Changed¶
- BREAKING: All client methods now raise
SweatStackAPIErrorsubclasses instead ofhttpx.HTTPStatusError. Code that catcheshttpx.HTTPStatusErrormust switch to catchingSweatStackAPIError(or a specific subclass). - BREAKING:
TokenRefreshErrorrenamed toSweatStackTokenRefreshErrorand moved tosweatstack.exceptions. Import path changed fromfrom sweatstack import TokenRefreshErrortofrom sweatstack import SweatStackTokenRefreshError. - BREAKING: 422 responses now raise
SweatStackBadRequestErrorinstead ofValueError. - Transport errors (DNS, timeouts, connection refused) now raise
SweatStackConnectionErrorinstead of leaking raw httpx exceptions.
Removed¶
httpxis no longer part of the public error surface. Consumers do not need to importhttpxto handle errors.
Upgrading¶
| 0.75 | 0.76 |
|---|---|
except httpx.HTTPStatusError as e |
except SweatStackAPIError as e, or a subclass: SweatStackNotFoundError (404), SweatStackAuthError (401, 403), SweatStackRateLimitError (429), SweatStackBadRequestError (other 4xx), SweatStackServerError (5xx) |
e.response.status_code, e.request.url |
e.status_code, e.url; also e.method, e.request_id, e.body |
except ValueError around a call, for a 422 |
except SweatStackBadRequestError |
except httpx.ConnectError, httpx.TimeoutException |
except SweatStackConnectionError |
from sweatstack import TokenRefreshError |
from sweatstack import SweatStackTokenRefreshError |
[0.75.0] - 2026-04-23¶
Added¶
- Update and delete traces:
update_trace()anddelete_trace()methods for full trace lifecycle management.
[0.74.0] - 2026-04-22¶
Added¶
- Team listing —
get_teams()returns teams you own or belong to,get_authorized_teams()returns teams you've granted data access to.
[0.73.0] - 2026-04-09¶
Added¶
- Full support for fitness tests — create, retrieve, update, and delete physiological assessments (threshold tests, VO2max tests, etc.) and their results.
- App metadata — store and retrieve per-app JSON data on activities, traces, tests, and users.
- Dailies — get, set, and delete daily health metrics (body mass, HRV, resting HR, etc.) with optional server-side interpolation.
[0.72.0] - 2026-03-13¶
Added¶
sweatstack.enable_cache()function to enable local caching via a simple API call instead of environment variables.- Local caching for
get_longitudinal_mean_max()responses (previously onlyget_longitudinal_data()was cached).
Changed¶
- Cache directory now defaults to the platform cache dir (
platformdirs.user_cache_dir) instead of the system temp directory.
Fixed¶
AttributeErroron Python <3.11 when the API returns an error response (add_noteis a Python 3.11+ feature).
Removed¶
- Breaking:
SWEATSTACK_LOCAL_CACHEandSWEATSTACK_CACHE_DIRenvironment variables. Usesweatstack.enable_cache()instead.
Upgrading¶
| 0.71 | 0.72 |
|---|---|
SWEATSTACK_LOCAL_CACHE=true in the environment |
sweatstack.enable_cache() in code. Silent: the variable is ignored, so nothing is cached until you call it |
SWEATSTACK_CACHE_DIR=/some/dir |
sweatstack.enable_cache("/some/dir") |
[0.71.0] - 2026-03-13¶
Added¶
sports(list) parameter onget_longitudinal_mean_max()andget_longitudinal_awd()for multi-sport support. The existingsport(single) parameter remains for backwards compatibility.startandenddate-range parameters onget_longitudinal_mean_max()andget_longitudinal_awd().
Changed¶
get_activities(),get_traces(), andget_longitudinal_data()now send thesportquery key to the API instead ofsports.
Deprecated¶
sport(singular) parameter onget_longitudinal_mean_max(),get_longitudinal_awd(), andget_longitudinal_data(). Usesports(list) instead.dateandwindow_daysparameters onget_longitudinal_mean_max()andget_longitudinal_awd(). Usestart/endinstead.
[0.70.0] - 2026-03-13¶
Added¶
- Added
get_team_user(*, team_id, user, search_mode)method to find a single team-authorized user by ID or name.
Changed¶
- Refactored internal user lookup into reusable
_find_user(),_find_user_by_name(), and_find_user_by_id()helpers shared byget_user()andget_team_user().
[0.69.0] - 2026-03-12¶
Added¶
- Passing team id when switching users now also works with the FastAPI and Streamlit integrations.
[0.68.0] - 2026-03-12¶
Added¶
- Added
create_user()method for creating managed users (no login credentials). - Added
get_team_users()method to list users who have authorized a team. - Added
authorize_team()method to grant a team access to user data. - Added
upload()method for uploading activity files (CSV or FIT). - Added
team_idparameter toswitch_user()anddelegated_client()to support delegation via team membership.
Fixed¶
- In proxy mode, the Streamlit login button now opens with
target="_blank"so iOS standalone PWAs use real Safari (with existing sessions) instead of the in-app browser overlay.
[0.67.0] - 2026-03-06¶
Added¶
- Added
login_uriparameter toStreamlitAuth.behind_proxy()(defaults to"/login"). In proxy mode the login button now points to the proxy's login endpoint instead of building an OAuth URL directly, enabling custom login flows such as PWA popup authentication.
[0.66.0] - 2026-03-06¶
Fixed¶
- Fixed
TokenRefreshErrorwhen usingStreamlitAuth.behind_proxy()with an expired access token. The SDK no longer checks token expiry in proxy mode, since token lifecycle is managed by the proxy.
Added¶
- Added
skip_token_expiry_checkparameter toClientfor cases where token lifecycle is managed externally.
[0.65.0] - 2026-02-12¶
Added¶
- The local browser auth flow now requests the
offline_accessscope so it receives a refresh token.
[0.64.0] - 2026-02-06¶
Added¶
- Automatic conversion of optimized API dtypes (Int16, float16, etc.) to standard dtypes (float64) for all DataFrame-returning methods.
[0.63.0] - 2026-02-05¶
Fixed¶
- Fixed token refresh failing when tokens were loaded from persistent storage.
- Fixed refreshed tokens not being persisted to storage.
- Fixed
switch_user()andget_user()not recognizing ULID format for user IDs.
Changed¶
- Simplified
authenticate()signature:force_login→force,persist_api_key→persist. - Made
login()private. Useauthenticate(force=True)instead.
Added¶
- Added
TokenRefreshErrorexception for explicit refresh failure handling.
[0.62.0] - 2026-02-02¶
Added¶
- Adds webhook support to FastAPI integration.
[0.61.0] - 2026-01-29¶
Added¶
- Added user switching support to FastAPI integration.
[0.60.0] - 2026-01-28¶
Added¶
- Added a FastAPI integration.
Changed¶
- Converted sensitive variables to SecretStr to prevent accidental logging.
[0.59.0] - 2026-01-27¶
Changed¶
- Future-proofed response enums.
[0.58.0] - 2026-01-24¶
Fixed¶
- Fixes missing altitude metric.
[0.57.0] - 2025-12-04¶
Added¶
- Added a new "proxy mode" to the
ss.StreamlitAuthclass that allows running Streamlit apps behind a proxy. The proxy mode is enabled by callingss.StreamlitAuth.behind_proxy(). The proxy should handle the OAuth callback and token exchange and pass the access token to the app via theX-SweatStack-Token(configurable) header. The OAuth2 flow is still initiated by thess.StreamlitAuthclass.
[0.56.0] - 2025-11-21¶
Fixed¶
- Fixed an issue where refreshing the token would not succeed with the Streamlit integration.
[0.55.0] - 2025-10-24¶
Added¶
- Added a new
get_activity_awd()method to thess.Clientclass that allows for getting the accumulated work duration (AWD) data for a specific activity. - Added a new
get_longitudinal_awd()method to thess.Clientclass that allows for getting the AWD data for a specific date range.
[0.54.0] - 2025-09-11¶
Added¶
- Added new methods
get_authorization_url(),exchange_code_for_token()andget_pkce_params()to thess.Clientclass that allow for getting the authorization URL and exchanging a code for tokens. This should make it easier for clients to implement the SweatStack OAuth2 flow.
Fixed¶
- Fixed an issue where the
ss.get_activities()withas_dataframe=Truemethod would raise an error if no activities were found.
[0.53.0] - 2025-09-11¶
Added¶
- Added a new
sportparameter to thess.create_trace()method that allows for associating a trace with a specific sport.
[0.52.0] - 2025-09-10¶
Changed¶
- Changed the default timeout for the HTTP client to 60 seconds.
[0.51.0] - 2025-08-28¶
Added¶
- Added a new
show_logoutparameter to thess.StreamlitAuth.authenticate()method that allows for disabling the logout button. The logout button can be shown by callingss.StreamlitAuth.logout_button(). This is for example useful when you want to show the login button on the main page, but the logout button in the sidebar.
[0.50.0] - 2025-08-25¶
Added¶
- Added a new
offsetparameter to thess.get_activities(),ss.get_activity_data(),ss.get_latest_activity_data(),ss.get_traces(), andss.get_trace_data()methods that allows for pagination of the results.
[0.49.0] - 2025-08-18¶
Added¶
- Added a new
metricsparameter to thess.get_activity_data()andss.get_latest_activity_data()methods that allows for filtering the data by specific metrics.
[0.48.0] - 2025-08-12¶
Added¶
- Added optional local caching of longitudinal data, enabled by setting the
SWEATSTACK_CACHE_ENABLEDenvironment variable totrue. The cache directory can be specified by setting theSWEATSTACK_CACHE_DIRenvironment variable. The cache can be cleared by callingss.clear_cache().
[0.47.0] - 2025-08-07¶
Added¶
- Added a new
ss.get_backfill_status()method that returns the current backfill status from the activities backfill-status endpoint. - Added a new
ss.watch_backfill_status()method that watches the backfill status from the activities backfill-status endpoint.
[0.46.0] - 2025-08-01¶
Added¶
- Added a new
registered_atfield to theUserInfoResponsemodel that is returned byss.get_userinfo(). This field is the timestamp of the user's registration with SweatStack.
[0.45.0] - 2025-06-24¶
Added¶
- Added a new
ss.whoami()method that returns the authenticated user's summary information. This method is recommended overss.get_userinfo()which only exists for OpenID compatibility and requires theprofilescope. - Added a new
ss.Metric.display_name()method that returns a human-readable display name for a metric. For example,ss.Metric.heart_rate.display_name()returns "heart rate".
[0.44.0] - 2025-06-18¶
Added¶
- Added support for persistent storage of API keys and refresh tokens.
- Added a new
ss.authenticate()method that handles authentication comprehensively, including callingss.login()when needed. This method is now the recommended way to authenticate the client.
Changed¶
- The
sweatlabandsweatshellcommands now use the newss.authenticate()method.