Skip to content

Upgrading

How to move a project to a newer sweatstack release, from any older one. Until 1.0 a minor release may contain breaking changes; every one has its upgrade path below, newest first, and the full list of changes is in the changelog.

Procedure

  1. Find your version. uv pip show sweatstack, or your lockfile.
  2. Inventory your SDK usage before changing anything: every call on a Client or on the sweatstack module (import sweatstack as ss is common), StreamlitAuth and sweatstack.fastapi, the model fields you read, the frame columns and indexes you rely on, the exceptions you catch, and the install line with its extras.
  3. Read the Upgrading section of every release between your version and the target; a release without one has nothing to do. Work oldest first: each table uses the names of the release before it. adaptive_sampling_on became nlec_on in 0.86 and segmentation_on in 0.87, so a name may change more than once; follow the chain.
  4. Change the pin and run your code and tests. A removed name raises an AttributeError naming its replacement, a renamed or keyword-only argument raises TypeError, and from sweatstack import <removed name> raises Python's plain ImportError. The rows marked Silent don't fail at all: check each one by hand.
  5. Don't add compatibility shims. Move every call site to the new name.

The install line changes too: Python 3.10 or later (0.78), the frame library as an extra (sweatstack[pandas], [polars] or [arrow]; [streamlit] includes pandas) and pyarrow 20 or later (0.84, 0.89), and no [jupyter] extra (0.91).

Prompt for a coding agent

Fill in the two versions and paste it into your agent. It works for any distance between them.

Upgrade this project from sweatstack <OLD> to sweatstack <NEW>.

1. Inventory. Before changing anything, list every use of the SDK: calls on a Client or on the
   sweatstack module (also as "ss"), StreamlitAuth and sweatstack.fastapi, model fields read
   from responses, frame columns and indexes, exceptions caught, and the dependency line with
   its extras.
2. Plan. Read https://docs.sweatstack.no/learn/python/upgrading/ (the same content is
   UPGRADING.md and the "Upgrading" sections of CHANGELOG.md in
   https://github.com/SweatStack/sweatstack-python). Take every release between <OLD> and <NEW>
   that has an Upgrading section, oldest first, and map each row of its table onto the
   inventory. A name may be renamed more than once across releases; follow the chain to the
   final name.
3. Apply the changes, oldest release first. Do not add compatibility shims.
4. Install: uv add "sweatstack[<extras>]==<NEW>". The frame library is an extra ([pandas],
   [polars] or [arrow]); [streamlit] includes pandas.
5. Run the code and the tests. Fix every AttributeError (its message names the replacement),
   ImportError and TypeError. Then check every row marked "Silent" against the inventory:
   those change behaviour without raising.
6. For the current API, install the SDK's agent skill (npx skills add
   SweatStack/sweatstack-python) or read https://docs.sweatstack.no/llms.txt.

Report what you changed, and every item of the inventory you could not map to a table row.

0.94

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

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

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

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

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

Upgrading

  1. 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.
  2. To keep pandas frames in an environment that also has Polars, add sweatstack.set_output("pandas") once (or Client(output="pandas")).
  3. Replace as_dataframe=True with output="pandas".
  4. 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.
  5. Silent: as_dataframe=True on get_activities and get_traces kept 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

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

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

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.81

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.78

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.76

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.72

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")