Spaces:
Running
Running
burtenshaw
commited on
Commit
·
84c7caa
1
Parent(s):
c6d1623
add strava auth setup
Browse files- strava_mcp/api.py +56 -24
- strava_mcp/gradio_server.py +112 -7
- 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,
|
| 75 |
if not self.settings.refresh_token:
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 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
|
| 93 |
-
"
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
|
|
|
|
|
|
| 148 |
logger.error(error_msg)
|
| 149 |
|
| 150 |
try:
|
| 151 |
error_data = response.json()
|
| 152 |
error = ErrorResponse(**error_data)
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
except Exception as err:
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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 |
-
[
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 48 |
splits_metric: list[dict] | None = Field(None, description="Splits in metric units")
|
| 49 |
-
splits_standard: list[dict] | None = Field(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
| 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)")
|