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.
[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¶
| Before | After |
|---|---|
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 |
[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().
| 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.
[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.
[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.
[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.
[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.
[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.
Migration¶
Hand the following prompt to a coding agent, or apply it by hand:
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.
[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.
[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¶
SWEATSTACK_LOCAL_CACHEandSWEATSTACK_CACHE_DIRenvironment variables. Usesweatstack.enable_cache()instead.
[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.