burtenshaw commited on
Commit
84c7caa
·
1 Parent(s): c6d1623

add strava auth setup

Browse files
Files changed (3) hide show
  1. strava_mcp/api.py +56 -24
  2. strava_mcp/gradio_server.py +112 -7
  3. strava_mcp/models.py +62 -21
strava_mcp/api.py CHANGED
@@ -1,4 +1,5 @@
1
  import logging
 
2
  from datetime import datetime
3
 
4
  import httpx
@@ -71,27 +72,39 @@ class StravaAPI:
71
  if self.access_token and self.token_expires_at and now < self.token_expires_at:
72
  return self.access_token
73
 
74
- # If we don't have a refresh token, try to get one through standalone OAuth flow
75
  if not self.settings.refresh_token:
76
- logger.warning("No refresh token available, launching standalone OAuth server")
77
- try:
78
- # Import here to avoid circular import
79
- from strava_mcp.oauth_server import get_refresh_token_from_oauth
80
-
81
- logger.info("Starting OAuth flow to get refresh token")
82
- self.settings.refresh_token = await get_refresh_token_from_oauth(
83
- self.settings.client_id, self.settings.client_secret
84
- )
85
- logger.info("Successfully obtained refresh token from OAuth flow")
86
- except Exception as e:
87
- error_msg = f"Failed to get refresh token through OAuth flow: {e}"
88
- logger.error(error_msg)
89
-
90
- # No fallback to MCP-integrated auth flow anymore
91
  raise Exception(
92
- "No refresh token available and OAuth flow failed. "
93
- "Please set STRAVA_REFRESH_TOKEN manually in your environment variables."
94
- ) from e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  # Now that we have a refresh token, refresh the access token
97
  async with httpx.AsyncClient() as client:
@@ -144,15 +157,30 @@ class StravaAPI:
144
  response = await self._client.request(method, url, headers=headers, **kwargs)
145
 
146
  if not response.is_success:
147
- error_msg = f"Strava API request failed: {response.status_code} - {response.text}"
 
 
148
  logger.error(error_msg)
149
 
150
  try:
151
  error_data = response.json()
152
  error = ErrorResponse(**error_data)
153
- raise Exception(f"Strava API error: {error.message} (code: {error.code})")
 
 
 
 
 
 
 
 
 
 
154
  except Exception as err:
155
- msg = f"Strava API failed: {response.status_code} - {response.text[:50]}"
 
 
 
156
  raise Exception(msg) from err
157
 
158
  return response
@@ -186,7 +214,9 @@ class StravaAPI:
186
 
187
  return [Activity(**activity) for activity in data]
188
 
189
- async def get_activity(self, activity_id: int, include_all_efforts: bool = False) -> DetailedActivity:
 
 
190
  """Get a specific activity.
191
 
192
  Args:
@@ -200,7 +230,9 @@ class StravaAPI:
200
  if include_all_efforts:
201
  params["include_all_efforts"] = "true"
202
 
203
- response = await self._request("GET", f"/activities/{activity_id}", params=params)
 
 
204
  data = response.json()
205
 
206
  return DetailedActivity(**data)
 
1
  import logging
2
+ import os
3
  from datetime import datetime
4
 
5
  import httpx
 
72
  if self.access_token and self.token_expires_at and now < self.token_expires_at:
73
  return self.access_token
74
 
75
+ # If we don't have a refresh token, check if we're in a Hugging Face Space environment
76
  if not self.settings.refresh_token:
77
+ # Check if we're running in Hugging Face Spaces
78
+ space_id = os.environ.get("SPACE_ID")
79
+ if space_id:
80
+ # We're in Hugging Face Spaces - can't do OAuth flow automatically
 
 
 
 
 
 
 
 
 
 
 
81
  raise Exception(
82
+ "No refresh token available. In Hugging Face Spaces, you must provide a STRAVA_REFRESH_TOKEN "
83
+ "environment variable or use the Authentication tab to set your refresh token manually. "
84
+ "Use the OAuth Helper tab for instructions on how to get a refresh token."
85
+ )
86
+ else:
87
+ # We're running locally - try OAuth flow
88
+ logger.warning(
89
+ "No refresh token available, launching standalone OAuth server"
90
+ )
91
+ try:
92
+ # Import here to avoid circular import
93
+ from strava_mcp.oauth_server import get_refresh_token_from_oauth
94
+
95
+ logger.info("Starting OAuth flow to get refresh token")
96
+ self.settings.refresh_token = await get_refresh_token_from_oauth(
97
+ self.settings.client_id, self.settings.client_secret
98
+ )
99
+ logger.info("Successfully obtained refresh token from OAuth flow")
100
+ except Exception as e:
101
+ error_msg = f"Failed to get refresh token through OAuth flow: {e}"
102
+ logger.error(error_msg)
103
+
104
+ raise Exception(
105
+ "No refresh token available and OAuth flow failed. "
106
+ "Please set STRAVA_REFRESH_TOKEN manually in your environment variables."
107
+ ) from e
108
 
109
  # Now that we have a refresh token, refresh the access token
110
  async with httpx.AsyncClient() as client:
 
157
  response = await self._client.request(method, url, headers=headers, **kwargs)
158
 
159
  if not response.is_success:
160
+ error_msg = (
161
+ f"Strava API request failed: {response.status_code} - {response.text}"
162
+ )
163
  logger.error(error_msg)
164
 
165
  try:
166
  error_data = response.json()
167
  error = ErrorResponse(**error_data)
168
+ if error.errors:
169
+ # Extract more specific error information
170
+ error_details = []
171
+ for err in error.errors:
172
+ field = err.get("field", "unknown")
173
+ code = err.get("code", "unknown")
174
+ error_details.append(f"{field}: {code}")
175
+ details = ", ".join(error_details)
176
+ raise Exception(f"Strava API error: {error.message} ({details})")
177
+ else:
178
+ raise Exception(f"Strava API error: {error.message}")
179
  except Exception as err:
180
+ # If we can't parse the error response, fall back to the raw response
181
+ msg = (
182
+ f"Strava API failed: {response.status_code} - {response.text[:200]}"
183
+ )
184
  raise Exception(msg) from err
185
 
186
  return response
 
214
 
215
  return [Activity(**activity) for activity in data]
216
 
217
+ async def get_activity(
218
+ self, activity_id: int, include_all_efforts: bool = False
219
+ ) -> DetailedActivity:
220
  """Get a specific activity.
221
 
222
  Args:
 
230
  if include_all_efforts:
231
  params["include_all_efforts"] = "true"
232
 
233
+ response = await self._request(
234
+ "GET", f"/activities/{activity_id}", params=params
235
+ )
236
  data = response.json()
237
 
238
  return DetailedActivity(**data)
strava_mcp/gradio_server.py CHANGED
@@ -17,17 +17,18 @@ logger = logging.getLogger(__name__)
17
  service: Optional[StravaService] = None
18
 
19
 
20
- async def initialize_service():
21
  """Initialize the Strava service with settings from environment variables."""
22
  global service
23
 
24
- if service is not None:
25
  return service
26
 
27
  try:
28
  settings = StravaSettings(
29
  client_id=os.getenv("STRAVA_CLIENT_ID", ""),
30
  client_secret=os.getenv("STRAVA_CLIENT_SECRET", ""),
 
31
  base_url="https://www.strava.com/api/v3",
32
  )
33
 
@@ -35,6 +36,10 @@ async def initialize_service():
35
  raise ValueError("STRAVA_CLIENT_ID environment variable is not set")
36
  if not settings.client_secret:
37
  raise ValueError("STRAVA_CLIENT_SECRET environment variable is not set")
 
 
 
 
38
 
39
  # Initialize service without FastAPI app for Gradio
40
  service = StravaService(settings, None)
@@ -46,6 +51,69 @@ async def initialize_service():
46
  raise
47
 
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  async def get_user_activities(
50
  before: Optional[int] = None,
51
  after: Optional[int] = None,
@@ -124,6 +192,31 @@ async def get_activity_segments(activity_id: int) -> List[Dict]:
124
  def create_interface():
125
  """Create the Gradio interface for the Strava MCP server."""
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  # Activities interface
128
  activities_interface = gr.Interface(
129
  fn=get_user_activities,
@@ -134,7 +227,7 @@ def create_interface():
134
  gr.Number(label="Per Page", value=30, precision=0),
135
  ],
136
  outputs=gr.JSON(label="Activities"),
137
- title="Get User Activities",
138
  description="Retrieve the authenticated user's activities from Strava",
139
  )
140
 
@@ -146,7 +239,7 @@ def create_interface():
146
  gr.Checkbox(label="Include All Efforts", value=False),
147
  ],
148
  outputs=gr.JSON(label="Activity Details"),
149
- title="Get Activity Details",
150
  description="Get detailed information about a specific activity",
151
  )
152
 
@@ -157,14 +250,26 @@ def create_interface():
157
  gr.Number(label="Activity ID", precision=0),
158
  ],
159
  outputs=gr.JSON(label="Activity Segments"),
160
- title="Get Activity Segments",
161
  description="Get segment efforts for a specific activity",
162
  )
163
 
164
  # Combine interfaces in a tabbed interface
165
  demo = gr.TabbedInterface(
166
- [activities_interface, details_interface, segments_interface],
167
- ["Activities", "Activity Details", "Activity Segments"],
 
 
 
 
 
 
 
 
 
 
 
 
168
  title="Strava MCP Server",
169
  )
170
 
 
17
  service: Optional[StravaService] = None
18
 
19
 
20
+ async def initialize_service(refresh_token: str = None):
21
  """Initialize the Strava service with settings from environment variables."""
22
  global service
23
 
24
+ if service is not None and refresh_token is None:
25
  return service
26
 
27
  try:
28
  settings = StravaSettings(
29
  client_id=os.getenv("STRAVA_CLIENT_ID", ""),
30
  client_secret=os.getenv("STRAVA_CLIENT_SECRET", ""),
31
+ refresh_token=refresh_token or os.getenv("STRAVA_REFRESH_TOKEN", ""),
32
  base_url="https://www.strava.com/api/v3",
33
  )
34
 
 
36
  raise ValueError("STRAVA_CLIENT_ID environment variable is not set")
37
  if not settings.client_secret:
38
  raise ValueError("STRAVA_CLIENT_SECRET environment variable is not set")
39
+ if not settings.refresh_token:
40
+ raise ValueError(
41
+ "STRAVA_REFRESH_TOKEN is required for Hugging Face Spaces deployment"
42
+ )
43
 
44
  # Initialize service without FastAPI app for Gradio
45
  service = StravaService(settings, None)
 
51
  raise
52
 
53
 
54
+ async def setup_authentication(refresh_token: str) -> str:
55
+ """Set up authentication with the provided refresh token.
56
+
57
+ Args:
58
+ refresh_token: The Strava refresh token
59
+
60
+ Returns:
61
+ Status message
62
+ """
63
+ try:
64
+ if not refresh_token.strip():
65
+ raise ValueError("Refresh token cannot be empty")
66
+
67
+ global service
68
+ service = None # Reset service to force re-initialization
69
+ await initialize_service(refresh_token.strip())
70
+ return "✅ Authentication successful! You can now use the Strava API functions."
71
+ except Exception as e:
72
+ logger.error(f"Authentication failed: {str(e)}")
73
+ return f"❌ Authentication failed: {str(e)}"
74
+
75
+
76
+ def get_authorization_url() -> str:
77
+ """Get the Strava authorization URL for manual OAuth flow.
78
+
79
+ Returns:
80
+ The authorization URL and instructions
81
+ """
82
+ client_id = os.getenv("STRAVA_CLIENT_ID", "")
83
+ if not client_id:
84
+ return "❌ STRAVA_CLIENT_ID environment variable is not set"
85
+
86
+ # For Hugging Face Spaces, we need to provide a redirect URI that points to a manual flow
87
+ redirect_uri = "http://localhost:3008/exchange_token" # This is just for display
88
+ auth_url = f"https://www.strava.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope=activity:read_all,read_all,profile:read_all&approval_prompt=force"
89
+
90
+ instructions = f"""
91
+ 🔐 **Manual OAuth Setup Instructions:**
92
+
93
+ 1. **Click this link to authorize with Strava:**
94
+ {auth_url}
95
+
96
+ 2. **After authorization, you'll be redirected to a page that might show an error (this is expected)**
97
+
98
+ 3. **Copy the 'code' parameter from the URL** (it will look like: `?code=abc123...`)
99
+
100
+ 4. **Exchange the code for a refresh token** using this curl command:
101
+ ```bash
102
+ curl -X POST https://www.strava.com/oauth/token \\
103
+ -d client_id={client_id} \\
104
+ -d client_secret=YOUR_CLIENT_SECRET \\
105
+ -d code=THE_CODE_FROM_STEP_3 \\
106
+ -d grant_type=authorization_code
107
+ ```
108
+
109
+ 5. **Copy the 'refresh_token' from the response** and paste it in the "Refresh Token" field above.
110
+
111
+ ⚠️ **Note:** You'll need your STRAVA_CLIENT_SECRET for step 4. Contact the app administrator if you don't have it.
112
+ """
113
+
114
+ return instructions
115
+
116
+
117
  async def get_user_activities(
118
  before: Optional[int] = None,
119
  after: Optional[int] = None,
 
192
  def create_interface():
193
  """Create the Gradio interface for the Strava MCP server."""
194
 
195
+ # Authentication interface
196
+ auth_interface = gr.Interface(
197
+ fn=setup_authentication,
198
+ inputs=[
199
+ gr.Textbox(
200
+ label="Refresh Token",
201
+ placeholder="Enter your Strava refresh token here",
202
+ type="password",
203
+ lines=1,
204
+ )
205
+ ],
206
+ outputs=gr.Textbox(label="Status"),
207
+ title="🔐 Authentication",
208
+ description="Enter your Strava refresh token to authenticate. If you don't have one, use the OAuth Helper tab to get it.",
209
+ )
210
+
211
+ # OAuth helper interface
212
+ oauth_interface = gr.Interface(
213
+ fn=get_authorization_url,
214
+ inputs=[],
215
+ outputs=gr.Markdown(label="OAuth Instructions"),
216
+ title="🔗 OAuth Helper",
217
+ description="Get instructions for manually obtaining a Strava refresh token",
218
+ )
219
+
220
  # Activities interface
221
  activities_interface = gr.Interface(
222
  fn=get_user_activities,
 
227
  gr.Number(label="Per Page", value=30, precision=0),
228
  ],
229
  outputs=gr.JSON(label="Activities"),
230
+ title="📊 Get User Activities",
231
  description="Retrieve the authenticated user's activities from Strava",
232
  )
233
 
 
239
  gr.Checkbox(label="Include All Efforts", value=False),
240
  ],
241
  outputs=gr.JSON(label="Activity Details"),
242
+ title="🔍 Get Activity Details",
243
  description="Get detailed information about a specific activity",
244
  )
245
 
 
250
  gr.Number(label="Activity ID", precision=0),
251
  ],
252
  outputs=gr.JSON(label="Activity Segments"),
253
+ title="🏃 Get Activity Segments",
254
  description="Get segment efforts for a specific activity",
255
  )
256
 
257
  # Combine interfaces in a tabbed interface
258
  demo = gr.TabbedInterface(
259
+ [
260
+ auth_interface,
261
+ oauth_interface,
262
+ activities_interface,
263
+ details_interface,
264
+ segments_interface,
265
+ ],
266
+ [
267
+ "🔐 Authentication",
268
+ "🔗 OAuth Helper",
269
+ "📊 Activities",
270
+ "🔍 Activity Details",
271
+ "🏃 Activity Segments",
272
+ ],
273
  title="Strava MCP Server",
274
  )
275
 
strava_mcp/models.py CHANGED
@@ -11,11 +11,15 @@ class Activity(BaseModel):
11
  distance: float = Field(..., description="The distance in meters")
12
  moving_time: int = Field(..., description="Moving time in seconds")
13
  elapsed_time: int = Field(..., description="Elapsed time in seconds")
14
- total_elevation_gain: float = Field(..., description="Total elevation gain in meters")
 
 
15
  type: str = Field(..., description="Type of activity")
16
  sport_type: str = Field(..., description="Type of sport")
17
  start_date: datetime = Field(..., description="Start date and time in UTC")
18
- start_date_local: datetime = Field(..., description="Start date and time in athlete's timezone")
 
 
19
  timezone: str = Field(..., description="The timezone of the activity")
20
  achievement_count: int = Field(..., description="The number of achievements")
21
  kudos_count: int = Field(..., description="The number of kudos")
@@ -23,7 +27,9 @@ class Activity(BaseModel):
23
  athlete_count: int = Field(..., description="The number of athletes")
24
  photo_count: int = Field(..., description="The number of photos")
25
  map: dict | None = Field(None, description="The map of the activity")
26
- trainer: bool = Field(..., description="Whether this activity was recorded on a training machine")
 
 
27
  commute: bool = Field(..., description="Whether this activity is a commute")
28
  manual: bool = Field(..., description="Whether this activity was created manually")
29
  private: bool = Field(..., description="Whether this activity is private")
@@ -31,9 +37,15 @@ class Activity(BaseModel):
31
  workout_type: int | None = Field(None, description="The workout type")
32
  average_speed: float = Field(..., description="Average speed in meters per second")
33
  max_speed: float = Field(..., description="Maximum speed in meters per second")
34
- has_heartrate: bool = Field(..., description="Whether the activity has heartrate data")
35
- average_heartrate: float | None = Field(None, description="Average heartrate during activity")
36
- max_heartrate: float | None = Field(None, description="Maximum heartrate during activity")
 
 
 
 
 
 
37
  elev_high: float | None = Field(None, description="The highest elevation")
38
  elev_low: float | None = Field(None, description="The lowest elevation")
39
 
@@ -44,13 +56,19 @@ class DetailedActivity(Activity):
44
  description: str | None = Field(None, description="The description of the activity")
45
  athlete: dict = Field(..., description="The athlete who performed the activity")
46
  calories: float | None = Field(None, description="Calories burned during activity")
47
- segment_efforts: list[dict] | None = Field(None, description="List of segment efforts")
 
 
48
  splits_metric: list[dict] | None = Field(None, description="Splits in metric units")
49
- splits_standard: list[dict] | None = Field(None, description="Splits in standard units")
 
 
50
  best_efforts: list[dict] | None = Field(None, description="List of best efforts")
51
  photos: dict | None = Field(None, description="Photos associated with activity")
52
  gear: dict | None = Field(None, description="Gear used during activity")
53
- device_name: str | None = Field(None, description="Name of device used to record activity")
 
 
54
 
55
 
56
  class Segment(BaseModel):
@@ -60,19 +78,35 @@ class Segment(BaseModel):
60
  name: str = Field(..., description="The name of the segment")
61
  activity_type: str = Field(..., description="The activity type of the segment")
62
  distance: float = Field(..., description="The segment's distance in meters")
63
- average_grade: float = Field(..., description="The segment's average grade, in percents")
64
- maximum_grade: float = Field(..., description="The segments's maximum grade, in percents")
65
- elevation_high: float = Field(..., description="The segments's highest elevation, in meters")
66
- elevation_low: float = Field(..., description="The segments's lowest elevation, in meters")
67
- total_elevation_gain: float = Field(..., description="The segments's total elevation gain, in meters")
68
- start_latlng: list[float] = Field(..., description="Start coordinates [latitude, longitude]")
69
- end_latlng: list[float] = Field(..., description="End coordinates [latitude, longitude]")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  climb_category: int = Field(..., description="The category of the climb [0, 5]")
71
  city: str | None = Field(None, description="The city this segment is in")
72
  state: str | None = Field(None, description="The state this segment is in")
73
  country: str | None = Field(None, description="The country this segment is in")
74
  private: bool = Field(..., description="Whether this segment is private")
75
- starred: bool = Field(..., description="Whether this segment is starred by the authenticated athlete")
 
 
76
 
77
 
78
  class SegmentEffort(BaseModel):
@@ -85,13 +119,19 @@ class SegmentEffort(BaseModel):
85
  elapsed_time: int = Field(..., description="The elapsed time in seconds")
86
  moving_time: int = Field(..., description="The moving time in seconds")
87
  start_date: datetime = Field(..., description="Start date and time in UTC")
88
- start_date_local: datetime = Field(..., description="Start date and time in athlete's timezone")
 
 
89
  distance: float = Field(..., description="The effort's distance in meters")
90
  average_watts: float | None = Field(None, description="Average wattage")
91
- device_watts: bool | None = Field(None, description="Whether power data comes from a power meter")
 
 
92
  average_heartrate: float | None = Field(None, description="Average heartrate")
93
  max_heartrate: float | None = Field(None, description="Maximum heartrate")
94
- pr_rank: int | None = Field(None, description="Personal record rank (1-3), 0 if not a PR")
 
 
95
  achievements: list[dict] | None = Field(None, description="List of achievements")
96
  athlete: dict = Field(..., description="The athlete who performed the effort")
97
  segment: Segment = Field(..., description="The segment")
@@ -101,4 +141,5 @@ class ErrorResponse(BaseModel):
101
  """Represents an error response from the Strava API."""
102
 
103
  message: str = Field(..., description="Error message")
104
- code: int = Field(..., description="Error code")
 
 
11
  distance: float = Field(..., description="The distance in meters")
12
  moving_time: int = Field(..., description="Moving time in seconds")
13
  elapsed_time: int = Field(..., description="Elapsed time in seconds")
14
+ total_elevation_gain: float = Field(
15
+ ..., description="Total elevation gain in meters"
16
+ )
17
  type: str = Field(..., description="Type of activity")
18
  sport_type: str = Field(..., description="Type of sport")
19
  start_date: datetime = Field(..., description="Start date and time in UTC")
20
+ start_date_local: datetime = Field(
21
+ ..., description="Start date and time in athlete's timezone"
22
+ )
23
  timezone: str = Field(..., description="The timezone of the activity")
24
  achievement_count: int = Field(..., description="The number of achievements")
25
  kudos_count: int = Field(..., description="The number of kudos")
 
27
  athlete_count: int = Field(..., description="The number of athletes")
28
  photo_count: int = Field(..., description="The number of photos")
29
  map: dict | None = Field(None, description="The map of the activity")
30
+ trainer: bool = Field(
31
+ ..., description="Whether this activity was recorded on a training machine"
32
+ )
33
  commute: bool = Field(..., description="Whether this activity is a commute")
34
  manual: bool = Field(..., description="Whether this activity was created manually")
35
  private: bool = Field(..., description="Whether this activity is private")
 
37
  workout_type: int | None = Field(None, description="The workout type")
38
  average_speed: float = Field(..., description="Average speed in meters per second")
39
  max_speed: float = Field(..., description="Maximum speed in meters per second")
40
+ has_heartrate: bool = Field(
41
+ ..., description="Whether the activity has heartrate data"
42
+ )
43
+ average_heartrate: float | None = Field(
44
+ None, description="Average heartrate during activity"
45
+ )
46
+ max_heartrate: float | None = Field(
47
+ None, description="Maximum heartrate during activity"
48
+ )
49
  elev_high: float | None = Field(None, description="The highest elevation")
50
  elev_low: float | None = Field(None, description="The lowest elevation")
51
 
 
56
  description: str | None = Field(None, description="The description of the activity")
57
  athlete: dict = Field(..., description="The athlete who performed the activity")
58
  calories: float | None = Field(None, description="Calories burned during activity")
59
+ segment_efforts: list[dict] | None = Field(
60
+ None, description="List of segment efforts"
61
+ )
62
  splits_metric: list[dict] | None = Field(None, description="Splits in metric units")
63
+ splits_standard: list[dict] | None = Field(
64
+ None, description="Splits in standard units"
65
+ )
66
  best_efforts: list[dict] | None = Field(None, description="List of best efforts")
67
  photos: dict | None = Field(None, description="Photos associated with activity")
68
  gear: dict | None = Field(None, description="Gear used during activity")
69
+ device_name: str | None = Field(
70
+ None, description="Name of device used to record activity"
71
+ )
72
 
73
 
74
  class Segment(BaseModel):
 
78
  name: str = Field(..., description="The name of the segment")
79
  activity_type: str = Field(..., description="The activity type of the segment")
80
  distance: float = Field(..., description="The segment's distance in meters")
81
+ average_grade: float = Field(
82
+ ..., description="The segment's average grade, in percents"
83
+ )
84
+ maximum_grade: float = Field(
85
+ ..., description="The segments's maximum grade, in percents"
86
+ )
87
+ elevation_high: float = Field(
88
+ ..., description="The segments's highest elevation, in meters"
89
+ )
90
+ elevation_low: float = Field(
91
+ ..., description="The segments's lowest elevation, in meters"
92
+ )
93
+ total_elevation_gain: float = Field(
94
+ ..., description="The segments's total elevation gain, in meters"
95
+ )
96
+ start_latlng: list[float] = Field(
97
+ ..., description="Start coordinates [latitude, longitude]"
98
+ )
99
+ end_latlng: list[float] = Field(
100
+ ..., description="End coordinates [latitude, longitude]"
101
+ )
102
  climb_category: int = Field(..., description="The category of the climb [0, 5]")
103
  city: str | None = Field(None, description="The city this segment is in")
104
  state: str | None = Field(None, description="The state this segment is in")
105
  country: str | None = Field(None, description="The country this segment is in")
106
  private: bool = Field(..., description="Whether this segment is private")
107
+ starred: bool = Field(
108
+ ..., description="Whether this segment is starred by the authenticated athlete"
109
+ )
110
 
111
 
112
  class SegmentEffort(BaseModel):
 
119
  elapsed_time: int = Field(..., description="The elapsed time in seconds")
120
  moving_time: int = Field(..., description="The moving time in seconds")
121
  start_date: datetime = Field(..., description="Start date and time in UTC")
122
+ start_date_local: datetime = Field(
123
+ ..., description="Start date and time in athlete's timezone"
124
+ )
125
  distance: float = Field(..., description="The effort's distance in meters")
126
  average_watts: float | None = Field(None, description="Average wattage")
127
+ device_watts: bool | None = Field(
128
+ None, description="Whether power data comes from a power meter"
129
+ )
130
  average_heartrate: float | None = Field(None, description="Average heartrate")
131
  max_heartrate: float | None = Field(None, description="Maximum heartrate")
132
+ pr_rank: int | None = Field(
133
+ None, description="Personal record rank (1-3), 0 if not a PR"
134
+ )
135
  achievements: list[dict] | None = Field(None, description="List of achievements")
136
  athlete: dict = Field(..., description="The athlete who performed the effort")
137
  segment: Segment = Field(..., description="The segment")
 
141
  """Represents an error response from the Strava API."""
142
 
143
  message: str = Field(..., description="Error message")
144
+ errors: list[dict] | None = Field(None, description="Detailed error information")
145
+ code: int | None = Field(None, description="Error code (optional)")