sweatstack 0.56.0__tar.gz → 0.58.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {sweatstack-0.56.0 → sweatstack-0.58.0}/CHANGELOG.md +13 -1
  2. {sweatstack-0.56.0 → sweatstack-0.58.0}/PKG-INFO +1 -1
  3. sweatstack-0.58.0/docs/index.rst +7 -0
  4. {sweatstack-0.56.0 → sweatstack-0.58.0}/pyproject.toml +1 -1
  5. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/openapi_schemas.py +135 -41
  6. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/streamlit.py +102 -31
  7. {sweatstack-0.56.0 → sweatstack-0.58.0}/uv.lock +1 -1
  8. sweatstack-0.56.0/docs/index.rst +0 -0
  9. {sweatstack-0.56.0 → sweatstack-0.58.0}/.claude/settings.local.json +0 -0
  10. {sweatstack-0.56.0 → sweatstack-0.58.0}/.gitignore +0 -0
  11. {sweatstack-0.56.0 → sweatstack-0.58.0}/.python-version +0 -0
  12. {sweatstack-0.56.0 → sweatstack-0.58.0}/DEVELOPMENT.md +0 -0
  13. {sweatstack-0.56.0 → sweatstack-0.58.0}/Makefile +0 -0
  14. {sweatstack-0.56.0 → sweatstack-0.58.0}/README.md +0 -0
  15. {sweatstack-0.56.0 → sweatstack-0.58.0}/docs/conf.py +0 -0
  16. {sweatstack-0.56.0 → sweatstack-0.58.0}/docs/everything.rst +0 -0
  17. {sweatstack-0.56.0 → sweatstack-0.58.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
  18. {sweatstack-0.56.0 → sweatstack-0.58.0}/playground/README.md +0 -0
  19. {sweatstack-0.56.0 → sweatstack-0.58.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
  20. {sweatstack-0.56.0 → sweatstack-0.58.0}/playground/Untitled.ipynb +0 -0
  21. {sweatstack-0.56.0 → sweatstack-0.58.0}/playground/hello.py +0 -0
  22. {sweatstack-0.56.0 → sweatstack-0.58.0}/playground/pyproject.toml +0 -0
  23. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  24. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/__init__.py +0 -0
  25. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/cli.py +0 -0
  26. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/client.py +0 -0
  27. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/constants.py +0 -0
  28. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/ipython_init.py +0 -0
  29. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  30. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/py.typed +0 -0
  31. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/schemas.py +0 -0
  32. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/sweatshell.py +0 -0
  33. {sweatstack-0.56.0 → sweatstack-0.58.0}/src/sweatstack/utils.py +0 -0
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [0.58.0] - 2026-01-24
10
+
11
+ ### Fixed
12
+ - Fixes missing altitude metric.
13
+
14
+
15
+ ## [0.57.0] - 2025-12-04
16
+
17
+ ### Added
18
+ - Added a new "proxy mode" to the `ss.StreamlitAuth` class that allows running Streamlit apps behind a proxy. The proxy mode is enabled by calling `ss.StreamlitAuth.behind_proxy()`. The proxy should handle the OAuth callback and token exchange and pass the access token to the app via the `X-SweatStack-Token` (configurable) header. The OAuth2 flow is still initiated by the `ss.StreamlitAuth` class.
19
+
20
+
9
21
  ## [0.56.0] - 2025-11-21
10
22
 
11
23
  ### Fixed
@@ -104,4 +116,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
104
116
 
105
117
  ## Changed
106
118
 
107
- - The `sweatlab` and `sweatshell` commands now use the new `ss.authenticate()` method.
119
+ - The `sweatlab` and `sweatshell` commands now use the new `ss.authenticate()` method.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.56.0
3
+ Version: 0.58.0
4
4
  Summary: The official Python client for SweatStack
5
5
  Author-email: Aart Goossens <aart@gssns.io>
6
6
  Requires-Python: >=3.9
@@ -0,0 +1,7 @@
1
+ MyPackage Documentation
2
+ =======================
3
+
4
+ .. toctree::
5
+ :maxdepth: 2
6
+
7
+ everything
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.56.0"
3
+ version = "0.58.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: openapi.json
3
- # timestamp: 2025-08-07T11:42:52+00:00
3
+ # timestamp: 2026-01-24T14:09:07+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -22,13 +22,33 @@ from pydantic import (
22
22
 
23
23
  class Activity(BaseModel):
24
24
  id: str = Field(..., title='Id')
25
- start_date_local: datetime = Field(..., title='Start Date Local')
25
+ start_date_local: Optional[datetime] = Field(None, title='Start Date Local')
26
26
 
27
27
 
28
28
  class ActivityUpdate(BaseModel):
29
29
  tags: Optional[List[str]] = Field(None, title='Tags')
30
30
 
31
31
 
32
+ class AltitudeSummary(BaseModel):
33
+ mean: Optional[float] = Field(None, title='Mean')
34
+ gain: Optional[Union[int, float]] = Field(None, title='Gain')
35
+ loss: Optional[Union[int, float]] = Field(None, title='Loss')
36
+ min: Optional[Union[int, float]] = Field(None, title='Min')
37
+ max: Optional[Union[int, float]] = Field(None, title='Max')
38
+
39
+
40
+ class ApplicationCreateOrUpdate(BaseModel):
41
+ name: str = Field(..., title='Name')
42
+ description: Optional[str] = Field(None, title='Description')
43
+ url: Optional[AnyUrl] = Field(None, title='Url')
44
+ image: Optional[AnyUrl] = Field(None, title='Image')
45
+ redirect_uris: Optional[List[AnyUrl]] = Field(None, title='Redirect Uris')
46
+ privacy_statement: Optional[AnyUrl] = Field(None, title='Privacy Statement')
47
+ published: Optional[bool] = Field(False, title='Published')
48
+ webhook_endpoints: Optional[List[AnyUrl]] = Field(None, title='Webhook Endpoints')
49
+ webhook_secret: Optional[SecretStr] = Field(None, title='Webhook Secret')
50
+
51
+
32
52
  class Prompt(Enum):
33
53
  none = 'none'
34
54
  login = 'login'
@@ -36,6 +56,10 @@ class Prompt(Enum):
36
56
  select_account = 'select_account'
37
57
 
38
58
 
59
+ class BackfillError(BaseModel):
60
+ error: str = Field(..., title='Error')
61
+
62
+
39
63
  class BackfillStatus(BaseModel):
40
64
  backfill_loaded_until: Optional[datetime] = Field(
41
65
  ..., title='Backfill Loaded Until'
@@ -46,16 +70,6 @@ class BodyAddEmailPartialsAddEmailPost(BaseModel):
46
70
  email: EmailStr = Field(..., title='Email')
47
71
 
48
72
 
49
- class BodyCreateApplicationApplicationsPost(BaseModel):
50
- name: str = Field(..., title='Name')
51
- description: str = Field(..., title='Description')
52
- url: AnyUrl = Field(..., title='Url')
53
- image: AnyUrl = Field(..., title='Image')
54
- redirect_uris: List[str] = Field(..., title='Redirect Uris')
55
- privacy_statement: AnyUrl = Field(..., title='Privacy Statement')
56
- published: Optional[bool] = Field(False, title='Published')
57
-
58
-
59
73
  class BodyCreateApplicationSecretApplicationsApplicationIdSecretsPost(BaseModel):
60
74
  label: str = Field(..., title='Label')
61
75
 
@@ -668,15 +682,23 @@ class Tz(Enum):
668
682
 
669
683
  class BodyLoginPostLoginPost(BaseModel):
670
684
  email: EmailStr = Field(..., title='Email')
671
- password: SecretStr = Field(..., title='Password')
672
- tz: Tz = Field(..., title='Tz')
685
+ tz: Optional[Tz] = Field('Europe/Oslo', title='Tz')
673
686
  state: Optional[str] = Field(None, title='State')
674
687
 
675
688
 
676
- class BodyRegisterPostRegisterPost(BaseModel):
689
+ class BodyPostCreateManagedUserSettingsManagedUsersPost(BaseModel):
690
+ first_name: str = Field(..., title='First Name')
691
+ last_name: Optional[str] = Field(None, title='Last Name')
692
+
693
+
694
+ class BodyPutUpdateManagedUserSettingsManagedUsersUserIdPut(BaseModel):
695
+ first_name: str = Field(..., title='First Name')
696
+ last_name: Optional[str] = Field(None, title='Last Name')
697
+
698
+
699
+ class BodyResendMagicLinkPartialsResendMagicLinkPost(BaseModel):
677
700
  email: EmailStr = Field(..., title='Email')
678
- password: SecretStr = Field(..., title='Password')
679
- tz: Tz = Field(..., title='Tz')
701
+ tz: Optional[Tz] = Field('Europe/Oslo', title='Tz')
680
702
 
681
703
 
682
704
  class BodySaveOrUpdateIntegrationProviderTenantsTenantIdIntegrationProvidersIntegrationNamePost(
@@ -687,19 +709,9 @@ class BodySaveOrUpdateIntegrationProviderTenantsTenantIdIntegrationProvidersInte
687
709
  redirect_url: str = Field(..., title='Redirect Url')
688
710
 
689
711
 
690
- class BodySendVerificationEmailPartialsSendVerificationEmailPost(BaseModel):
691
- email: EmailStr = Field(..., title='Email')
692
- tz: Tz = Field(..., title='Tz')
693
-
694
-
695
- class BodyUpdateApplicationApplicationsApplicationIdPut(BaseModel):
696
- name: str = Field(..., title='Name')
697
- description: str = Field(..., title='Description')
698
- url: AnyUrl = Field(..., title='Url')
699
- image: AnyUrl = Field(..., title='Image')
700
- redirect_uris: List[str] = Field(..., title='Redirect Uris')
701
- privacy_statement: AnyUrl = Field(..., title='Privacy Statement')
702
- published: Optional[bool] = Field(False, title='Published')
712
+ class BodyUpdateManagedUserApiV1UsersUserIdPut(BaseModel):
713
+ first_name: Optional[str] = Field(None, title='First Name')
714
+ last_name: Optional[str] = Field(None, title='Last Name')
703
715
 
704
716
 
705
717
  class BodyUpdateUserUsersUserIdPut(BaseModel):
@@ -715,6 +727,11 @@ class BodyUploadActivityFileDataUploadPost(BaseModel):
715
727
  files: List[bytes] = Field(..., title='Files')
716
728
 
717
729
 
730
+ class CadenceSummary(BaseModel):
731
+ mean: Optional[float] = Field(None, title='Mean')
732
+ max: Optional[float] = Field(None, title='Max')
733
+
734
+
718
735
  class CoreTemperatureSummary(BaseModel):
719
736
  mean: Optional[float] = Field(None, title='Mean')
720
737
  min: Optional[float] = Field(None, title='Min')
@@ -724,12 +741,7 @@ class CoreTemperatureSummary(BaseModel):
724
741
 
725
742
 
726
743
  class DistanceSummary(BaseModel):
727
- sum: Optional[int] = Field(None, title='Sum')
728
-
729
-
730
- class ElevationSummary(BaseModel):
731
- min: Optional[int] = Field(None, title='Min')
732
- max: Optional[int] = Field(None, title='Max')
744
+ sum: Optional[Union[int, float]] = Field(None, title='Sum')
733
745
 
734
746
 
735
747
  class GarminBackfillBody(BaseModel):
@@ -739,6 +751,11 @@ class GarminBackfillBody(BaseModel):
739
751
  user_id__not_in: Optional[List[str]] = Field(None, title='User Id Not In')
740
752
 
741
753
 
754
+ class GarminConnectUserPermission(Enum):
755
+ HISTORICAL_DATA_EXPORT = 'HISTORICAL_DATA_EXPORT'
756
+ ACTIVITY_EXPORT = 'ACTIVITY_EXPORT'
757
+
758
+
742
759
  class GarminDeregistration(BaseModel):
743
760
  userId: str = Field(..., title='Userid')
744
761
  userAccessToken: str = Field(..., title='Useraccesstoken')
@@ -754,6 +771,18 @@ class GarminFileTypes(Enum):
754
771
  TCX = 'TCX'
755
772
 
756
773
 
774
+ class GarminUserPermissionChange(BaseModel):
775
+ userId: str = Field(..., title='Userid')
776
+ userAccessToken: str = Field(..., title='Useraccesstoken')
777
+ permissions: List[GarminConnectUserPermission] = Field(..., title='Permissions')
778
+
779
+
780
+ class GarminUserPermissionsBody(BaseModel):
781
+ userPermissionsChange: List[GarminUserPermissionChange] = Field(
782
+ ..., title='Userpermissionschange'
783
+ )
784
+
785
+
757
786
  class GrantType(Enum):
758
787
  authorization_code = 'authorization_code'
759
788
  refresh_token = 'refresh_token'
@@ -796,6 +825,19 @@ class IntervalsIcuWebhookEventType(Enum):
796
825
  FITNESS_UPDATED = 'FITNESS_UPDATED'
797
826
 
798
827
 
828
+ class ManagedUserCreate(BaseModel):
829
+ first_name: str = Field(..., title='First Name')
830
+ last_name: Optional[str] = Field(..., title='Last Name')
831
+
832
+
833
+ class MetabolicMapResponse(BaseModel):
834
+ fatmax: float = Field(..., title='Fatmax')
835
+ mlss: float = Field(..., title='Mlss')
836
+ cp: float = Field(..., title='Cp')
837
+ vo2max: float = Field(..., title='Vo2Max')
838
+ vo2max_intensity: float = Field(..., title='Vo2Max Intensity')
839
+
840
+
799
841
  class Metric(Enum):
800
842
  duration = 'duration'
801
843
  lap = 'lap'
@@ -806,6 +848,7 @@ class Metric(Enum):
806
848
  smo2 = 'smo2'
807
849
  core_temperature = 'core_temperature'
808
850
  elevation = 'elevation'
851
+ altitude = 'altitude'
809
852
  cadence = 'cadence'
810
853
  temperature = 'temperature'
811
854
  distance = 'distance'
@@ -818,7 +861,6 @@ class Metric(Enum):
818
861
 
819
862
  class PowerSummary(BaseModel):
820
863
  mean: Optional[float] = Field(None, title='Mean')
821
- min: Optional[float] = Field(None, title='Min')
822
864
  max: Optional[float] = Field(None, title='Max')
823
865
 
824
866
 
@@ -850,7 +892,6 @@ class Smo2Summary(BaseModel):
850
892
 
851
893
  class SpeedSummary(BaseModel):
852
894
  mean: Optional[float] = Field(None, title='Mean')
853
- min: Optional[float] = Field(None, title='Min')
854
895
  max: Optional[float] = Field(None, title='Max')
855
896
 
856
897
 
@@ -897,10 +938,28 @@ class Sport(Enum):
897
938
  unknown = 'unknown'
898
939
 
899
940
 
941
+ class SubscriptionPlan(Enum):
942
+ free = 'free'
943
+ early_access = 'early_access'
944
+ individual = 'individual'
945
+ developer = 'developer'
946
+ team = 'team'
947
+
948
+
949
+ class TeamCreateOrUpdate(BaseModel):
950
+ name: str = Field(..., title='Name')
951
+ description: str = Field(..., title='Description')
952
+ url: AnyUrl = Field(..., title='Url')
953
+ image: AnyUrl = Field(..., title='Image')
954
+ privacy_statement: AnyUrl = Field(..., title='Privacy Statement')
955
+
956
+
900
957
  class TemperatureSummary(BaseModel):
901
958
  mean: Optional[float] = Field(None, title='Mean')
902
959
  min: Optional[float] = Field(None, title='Min')
903
960
  max: Optional[float] = Field(None, title='Max')
961
+ start: Optional[float] = Field(None, title='Start')
962
+ end: Optional[float] = Field(None, title='End')
904
963
 
905
964
 
906
965
  class TemplateDay(BaseModel):
@@ -965,12 +1024,23 @@ class UserInfoResponse(BaseModel):
965
1024
  name: str = Field(..., title='Name')
966
1025
 
967
1026
 
1027
+ class UserResponse(BaseModel):
1028
+ id: str = Field(..., title='Id')
1029
+ first_name: Optional[str] = Field(..., title='First Name')
1030
+ last_name: Optional[str] = Field(..., title='Last Name')
1031
+ admin: bool = Field(..., title='Admin')
1032
+ registered_at: datetime = Field(..., title='Registered At')
1033
+ display_name: str = Field(..., title='Display Name')
1034
+ is_managed: bool = Field(..., title='Is Managed')
1035
+
1036
+
968
1037
  class UserSummary(BaseModel):
969
1038
  id: str = Field(..., title='Id')
970
1039
  first_name: Optional[str] = Field(..., title='First Name')
971
1040
  last_name: Optional[str] = Field(..., title='Last Name')
972
1041
  scopes: List[Scope] = Field(..., title='Scopes')
973
1042
  display_name: str = Field(..., title='Display Name')
1043
+ is_managed: bool = Field(..., title='Is Managed')
974
1044
 
975
1045
 
976
1046
  class ValidationError(BaseModel):
@@ -990,12 +1060,19 @@ class VolumeQuantity(Enum):
990
1060
  distance = 'distance'
991
1061
 
992
1062
 
1063
+ class WebhookEventType(Enum):
1064
+ activity_created = 'activity_created'
1065
+ activity_updated = 'activity_updated'
1066
+ activity_deleted = 'activity_deleted'
1067
+
1068
+
993
1069
  class ActivitySummarySummary(BaseModel):
994
1070
  power: Optional[PowerSummary] = None
995
1071
  speed: Optional[SpeedSummary] = None
996
1072
  distance: Optional[DistanceSummary] = None
997
- elevation: Optional[ElevationSummary] = None
1073
+ altitude: Optional[AltitudeSummary] = None
998
1074
  heart_rate: Optional[HeartRateSummary] = None
1075
+ cadence: Optional[CadenceSummary] = None
999
1076
  temperature: Optional[TemperatureSummary] = None
1000
1077
  core_temperature: Optional[CoreTemperatureSummary] = None
1001
1078
  smo2: Optional[Smo2Summary] = None
@@ -1013,6 +1090,10 @@ class AuthorizeQueryParams(BaseModel):
1013
1090
  user_flow: Optional[UserFlow] = 'session'
1014
1091
 
1015
1092
 
1093
+ class AuthorizeTeamRequest(BaseModel):
1094
+ scopes: List[Scope] = Field(..., title='Scopes')
1095
+
1096
+
1016
1097
  class BodyAuthorizeOauthAuthorizePost(BaseModel):
1017
1098
  client_id: str = Field(..., title='Client Id')
1018
1099
  redirect_uri: Optional[str] = Field(None, title='Redirect Uri')
@@ -1023,6 +1104,10 @@ class BodyAuthorizeOauthAuthorizePost(BaseModel):
1023
1104
  code_challenge_method: Optional[str] = Field(None, title='Code Challenge Method')
1024
1105
 
1025
1106
 
1107
+ class BodyCreateCheckoutSessionApiV1PaymentStripeCheckoutPost(BaseModel):
1108
+ subscription_plan: SubscriptionPlan
1109
+
1110
+
1026
1111
  class BodyExpressAddEmailPostExpressAddEmailPost(BaseModel):
1027
1112
  email: EmailStr = Field(..., title='Email')
1028
1113
  state: Optional[str] = Field(None, title='State')
@@ -1062,7 +1147,7 @@ class GarminActivityFileData(BaseModel):
1062
1147
  callbackURL: str = Field(..., title='Callbackurl')
1063
1148
  startTimeInSeconds: datetime = Field(..., title='Starttimeinseconds')
1064
1149
  activityId: str = Field(..., title='Activityid')
1065
- activityName: str = Field(..., title='Activityname')
1150
+ activityName: Optional[str] = Field(None, title='Activityname')
1066
1151
  manual: Optional[bool] = Field(False, title='Manual')
1067
1152
 
1068
1153
 
@@ -1085,8 +1170,9 @@ class Lap(BaseModel):
1085
1170
  power: Optional[PowerSummary] = None
1086
1171
  speed: Optional[SpeedSummary] = None
1087
1172
  distance: Optional[DistanceSummary] = None
1088
- elevation: Optional[ElevationSummary] = None
1173
+ altitude: Optional[AltitudeSummary] = None
1089
1174
  heart_rate: Optional[HeartRateSummary] = None
1175
+ cadence: Optional[CadenceSummary] = None
1090
1176
  temperature: Optional[TemperatureSummary] = None
1091
1177
  core_temperature: Optional[CoreTemperatureSummary] = None
1092
1178
  smo2: Optional[Smo2Summary] = None
@@ -1133,6 +1219,13 @@ class RangeValueOutput(BaseModel):
1133
1219
  max: Optional[Value] = None
1134
1220
 
1135
1221
 
1222
+ class WebhookEventBody(BaseModel):
1223
+ user_id: str = Field(..., title='User Id')
1224
+ event_type: WebhookEventType
1225
+ resource_id: Optional[str] = Field(..., title='Resource Id')
1226
+ timestamp: datetime = Field(..., title='Timestamp')
1227
+
1228
+
1136
1229
  class IntervalInput(BaseModel):
1137
1230
  type: Literal['interval'] = Field('interval', title='Type')
1138
1231
  volume: Union[ConstantValueInput, RangeValueInput, RampValueInput] = Field(
@@ -1258,6 +1351,7 @@ class ActivityDetails(BaseModel):
1258
1351
  laps: Optional[List[Lap]] = Field(None, title='Laps')
1259
1352
  traces: Optional[List[TraceDetails]] = Field(None, title='Traces')
1260
1353
  distance: Optional[float] = Field(None, title='Distance')
1354
+ devices: Optional[List[str]] = Field(None, title='Devices')
1261
1355
  duration: timedelta = Field(..., title='Duration')
1262
1356
  start_local: datetime = Field(..., title='Start Local')
1263
1357
  end_local: datetime = Field(..., title='End Local')
@@ -114,6 +114,9 @@ class StreamlitAuth:
114
114
 
115
115
  self.redirect_uri = redirect_uri or os.environ.get("SWEATSTACK_REDIRECT_URI")
116
116
 
117
+ self._proxy_mode = False
118
+ self._logout_uri = None
119
+
117
120
  self.api_key = st.session_state.get("sweatstack_api_key")
118
121
  self.refresh_token = st.session_state.get("sweatstack_refresh_token")
119
122
  self.client = Client(
@@ -124,13 +127,97 @@ class StreamlitAuth:
124
127
  client_secret=self.client_secret,
125
128
  )
126
129
 
130
+ @classmethod
131
+ def behind_proxy(
132
+ cls,
133
+ redirect_uri: str,
134
+ header_name: str = "X-SweatStack-Token",
135
+ logout_uri: str = "/logout",
136
+ ) -> "StreamlitAuth":
137
+ """Create a StreamlitAuth instance for use behind a proxy.
138
+
139
+ Use this method when your Streamlit app runs behind a proxy that handles
140
+ authentication and passes the SweatStack access token via an HTTP header.
141
+
142
+ Args:
143
+ redirect_uri: The URI to redirect to after login (used by proxy).
144
+ header_name: The HTTP header name containing the access token.
145
+ Defaults to "X-SweatStack-Token".
146
+ logout_uri: The URI to redirect to for logout.
147
+ Defaults to "/logout".
148
+
149
+ Returns:
150
+ StreamlitAuth: An instance configured for proxy mode.
151
+
152
+ Example:
153
+ auth = StreamlitAuth.behind_proxy(
154
+ redirect_uri="https://myapp.example.com/app",
155
+ )
156
+
157
+ if not auth.is_authenticated():
158
+ st.error("Missing authentication header")
159
+ st.stop()
160
+
161
+ activities = auth.client.get_activities()
162
+ """
163
+ instance = cls(redirect_uri=redirect_uri)
164
+ instance._proxy_mode = True
165
+ instance._logout_uri = logout_uri
166
+
167
+ token = st.context.headers.get(header_name)
168
+ if token:
169
+ instance.api_key = token
170
+ instance.client = Client(token, streamlit_compatible=True)
171
+
172
+ return instance
173
+
174
+ def _show_styled_link_button(self, label: str, url: str):
175
+ """Displays a styled link button with hover effects.
176
+
177
+ Args:
178
+ label: Text to display on the button.
179
+ url: The URL to navigate to when clicked.
180
+ """
181
+ st.markdown(
182
+ f"""
183
+ <style>
184
+ .animated-button {{
185
+ }}
186
+ .animated-button:hover {{
187
+ transform: scale(1.05);
188
+ }}
189
+ .animated-button:active {{
190
+ transform: scale(1);
191
+ }}
192
+ </style>
193
+ <a href="{url}"
194
+ target="_top"
195
+ class="animated-button"
196
+ style="display: inline-block;
197
+ padding: 10px 20px;
198
+ background-color: #EF2B2D;
199
+ color: white;
200
+ text-decoration: none;
201
+ border-radius: 6px;
202
+ border: none;
203
+ transition: all 0.3s ease;
204
+ cursor: pointer;"
205
+ >{label}</a>
206
+ """,
207
+ unsafe_allow_html=True,
208
+ )
209
+
127
210
  def logout_button(self):
128
211
  """Displays a logout button and handles user logout.
129
212
 
130
- When clicked, this button clears the stored API key from session state,
131
- resets the client, and triggers a Streamlit rerun to update the UI.
213
+ In standard mode, clears the stored API key from session state,
214
+ resets the client, and triggers a Streamlit rerun.
215
+
216
+ In proxy mode, displays a styled link that redirects to the logout URI.
132
217
  """
133
- if st.button("Logout"):
218
+ if self._proxy_mode:
219
+ self._show_styled_link_button("Logout", self._logout_uri)
220
+ elif st.button("Logout"):
134
221
  self.api_key = None
135
222
  self.refresh_token = None
136
223
  self.client = Client(streamlit_compatible=True)
@@ -151,34 +238,7 @@ class StreamlitAuth:
151
238
  authorization_url = self.get_authorization_url()
152
239
  login_label = login_label or "Connect with SweatStack"
153
240
  if not self._running_on_streamlit_cloud():
154
- st.markdown(
155
- f"""
156
- <style>
157
- .animated-button {{
158
- }}
159
- .animated-button:hover {{
160
- transform: scale(1.05);
161
- }}
162
- .animated-button:active {{
163
- transform: scale(1);
164
- }}
165
- </style>
166
- <a href="{authorization_url}"
167
- target="_top"
168
- class="animated-button"
169
- style="display: inline-block;
170
- padding: 10px 20px;
171
- background-color: #EF2B2D;
172
- color: white;
173
- text-decoration: none;
174
- border-radius: 6px;
175
- border: none;
176
- transition: all 0.3s ease;
177
- cursor: pointer;"
178
- >{login_label}</a>
179
- """,
180
- unsafe_allow_html=True,
181
- )
241
+ self._show_styled_link_button(login_label, authorization_url)
182
242
  else:
183
243
  st.link_button(login_label, authorization_url)
184
244
 
@@ -275,12 +335,23 @@ class StreamlitAuth:
275
335
  to the Streamlit app with an authorization code, which is exchanged for an
276
336
  access token.
277
337
 
338
+ In proxy mode, this method only shows the login button if not authenticated.
339
+ The proxy handles the OAuth callback and token exchange.
340
+
278
341
  Args:
279
342
  login_label: The label to display on the login button. Defaults to "Login with SweatStack".
280
343
 
281
344
  Returns:
282
345
  None
283
346
  """
347
+ if self._proxy_mode:
348
+ if self.is_authenticated():
349
+ if show_logout:
350
+ self.logout_button()
351
+ else:
352
+ self._show_sweatstack_login(login_label)
353
+ return
354
+
284
355
  if self.is_authenticated():
285
356
  if not st.session_state.get("sweatstack_auth_toast_shown", False):
286
357
  st.toast("SweatStack authentication successful!", icon="✅")
@@ -2045,7 +2045,7 @@ wheels = [
2045
2045
 
2046
2046
  [[package]]
2047
2047
  name = "sweatstack"
2048
- version = "0.54.0"
2048
+ version = "0.57.0"
2049
2049
  source = { editable = "." }
2050
2050
  dependencies = [
2051
2051
  { name = "email-validator" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes