Yorrick Jansen commited on
Commit
7045b70
·
1 Parent(s): 9fbd63d
strava_mcp/api.py CHANGED
@@ -49,9 +49,12 @@ class StravaAPI:
49
  # If it's a FastAPI app, use it directly
50
  if hasattr(self.app, "add_api_route"):
51
  fastapi_app = self.app
52
- # If it's a FastMCP server, try to get its FastAPI app
53
- elif hasattr(self.app, "_app"):
54
- fastapi_app = self.app._app
 
 
 
55
 
56
  if not fastapi_app:
57
  logger.warning("Could not get FastAPI app from the provided object, auth flow will not be available")
 
49
  # If it's a FastAPI app, use it directly
50
  if hasattr(self.app, "add_api_route"):
51
  fastapi_app = self.app
52
+ # If it's a FastMCP server, try to get its underlying app
53
+ # FastMCP doesn't have a public API for this, but we can use type checking
54
+ # to treat it as a FastAPI instance as it extends FastAPI
55
+ else:
56
+ # Just use the app itself since it should be a FastAPI instance or subclass
57
+ fastapi_app = self.app
58
 
59
  if not fastapi_app:
60
  logger.warning("Could not get FastAPI app from the provided object, auth flow will not be available")
strava_mcp/auth.py CHANGED
@@ -137,7 +137,7 @@ class StravaAuthenticator:
137
  }
138
  return f"{AUTHORIZE_URL}?{urlencode(params)}"
139
 
140
- def setup_routes(self, app: FastAPI = None):
141
  """Set up the routes for authentication.
142
 
143
  Args:
@@ -147,6 +147,10 @@ class StravaAuthenticator:
147
  if not target_app:
148
  raise ValueError("No FastAPI app provided")
149
 
 
 
 
 
150
  # Add route for the token exchange
151
  target_app.add_api_route(self.redirect_path, self.exchange_token, methods=["GET"])
152
 
 
137
  }
138
  return f"{AUTHORIZE_URL}?{urlencode(params)}"
139
 
140
+ def setup_routes(self, app: FastAPI | None = None):
141
  """Set up the routes for authentication.
142
 
143
  Args:
 
147
  if not target_app:
148
  raise ValueError("No FastAPI app provided")
149
 
150
+ # Make sure we have a valid FastAPI app
151
+ if not hasattr(target_app, "add_api_route"):
152
+ raise ValueError("Provided app does not appear to be a valid FastAPI instance")
153
+
154
  # Add route for the token exchange
155
  target_app.add_api_route(self.redirect_path, self.exchange_token, methods=["GET"])
156
 
strava_mcp/config.py CHANGED
@@ -1,4 +1,4 @@
1
- from pydantic import Field
2
  from pydantic_settings import BaseSettings, SettingsConfigDict
3
 
4
 
@@ -14,3 +14,23 @@ class StravaSettings(BaseSettings):
14
  base_url: str = Field("https://www.strava.com/api/v3", description="Strava API base URL")
15
 
16
  model_config = SettingsConfigDict(env_prefix="STRAVA_", env_file=".env", env_file_encoding="utf-8")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import Field, model_validator
2
  from pydantic_settings import BaseSettings, SettingsConfigDict
3
 
4
 
 
14
  base_url: str = Field("https://www.strava.com/api/v3", description="Strava API base URL")
15
 
16
  model_config = SettingsConfigDict(env_prefix="STRAVA_", env_file=".env", env_file_encoding="utf-8")
17
+
18
+ @model_validator(mode="after")
19
+ def load_from_env(self):
20
+ """Load values from environment variables if not directly provided."""
21
+ import os
22
+
23
+ # Only override empty values with environment values
24
+ if not self.client_id and os.environ.get("STRAVA_CLIENT_ID"):
25
+ self.client_id = os.environ["STRAVA_CLIENT_ID"]
26
+
27
+ if not self.client_secret and os.environ.get("STRAVA_CLIENT_SECRET"):
28
+ self.client_secret = os.environ["STRAVA_CLIENT_SECRET"]
29
+
30
+ if not self.refresh_token and os.environ.get("STRAVA_REFRESH_TOKEN"):
31
+ self.refresh_token = os.environ["STRAVA_REFRESH_TOKEN"]
32
+
33
+ if not self.base_url and os.environ.get("STRAVA_BASE_URL"):
34
+ self.base_url = os.environ["STRAVA_BASE_URL"]
35
+
36
+ return self
strava_mcp/oauth_server.py CHANGED
@@ -122,8 +122,12 @@ class StravaOAuthServer:
122
 
123
  async def _run_server(self):
124
  """Run the uvicorn server."""
 
 
 
 
125
  config = uvicorn.Config(
126
- app=self.app,
127
  host=self.host,
128
  port=self.port,
129
  log_level="info",
 
122
 
123
  async def _run_server(self):
124
  """Run the uvicorn server."""
125
+ # Ensure app is not None before passing to uvicorn
126
+ if not self.app:
127
+ raise ValueError("FastAPI app not initialized")
128
+
129
  config = uvicorn.Config(
130
+ app=self.app, # The type checker should now be satisfied
131
  host=self.host,
132
  port=self.port,
133
  log_level="info",
strava_mcp/server.py CHANGED
@@ -1,8 +1,9 @@
1
  import logging
2
  from collections.abc import AsyncIterator
3
  from contextlib import asynccontextmanager
4
- from typing import Any
5
 
 
6
  from mcp.server.fastmcp import Context, FastMCP
7
 
8
  from strava_mcp.config import StravaSettings
@@ -28,16 +29,20 @@ async def lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:
28
  """
29
  # Load settings from environment variables
30
  try:
31
- settings = StravaSettings()
 
 
 
 
32
  logger.info("Loaded Strava API settings")
33
  except Exception as e:
34
  logger.error(f"Failed to load Strava API settings: {str(e)}")
35
  raise
36
 
37
- # Use the FastMCP server itself as we'll adapt our StravaService to work with it
38
- fastapi_app = server
39
 
40
- # Initialize the Strava service with the FastAPI app (or None)
41
  service = StravaService(settings, fastapi_app)
42
  logger.info("Initialized Strava service")
43
 
@@ -81,9 +86,16 @@ async def get_user_activities(
81
  Returns:
82
  List of activities
83
  """
84
- service = ctx.request_context.lifespan_context["service"]
85
-
86
  try:
 
 
 
 
 
 
 
 
 
87
  activities = await service.get_activities(before, after, page, per_page)
88
  return [activity.model_dump() for activity in activities]
89
  except Exception as e:
@@ -107,9 +119,16 @@ async def get_activity(
107
  Returns:
108
  The activity details
109
  """
110
- service = ctx.request_context.lifespan_context["service"]
111
-
112
  try:
 
 
 
 
 
 
 
 
 
113
  activity = await service.get_activity(activity_id, include_all_efforts)
114
  return activity.model_dump()
115
  except Exception as e:
@@ -131,9 +150,16 @@ async def get_activity_segments(
131
  Returns:
132
  List of segment efforts for the activity
133
  """
134
- service = ctx.request_context.lifespan_context["service"]
135
-
136
  try:
 
 
 
 
 
 
 
 
 
137
  segments = await service.get_activity_segments(activity_id)
138
  return [segment.model_dump() for segment in segments]
139
  except Exception as e:
 
1
  import logging
2
  from collections.abc import AsyncIterator
3
  from contextlib import asynccontextmanager
4
+ from typing import Any, cast
5
 
6
+ from fastapi import FastAPI
7
  from mcp.server.fastmcp import Context, FastMCP
8
 
9
  from strava_mcp.config import StravaSettings
 
29
  """
30
  # Load settings from environment variables
31
  try:
32
+ settings = StravaSettings(
33
+ client_id="", # Will be overridden by env vars
34
+ client_secret="", # Will be overridden by env vars
35
+ base_url="https://www.strava.com/api/v3",
36
+ )
37
  logger.info("Loaded Strava API settings")
38
  except Exception as e:
39
  logger.error(f"Failed to load Strava API settings: {str(e)}")
40
  raise
41
 
42
+ # FastMCP extends FastAPI, so we can safely cast it for type checking
43
+ fastapi_app = cast(FastAPI, server)
44
 
45
+ # Initialize the Strava service with the FastAPI app
46
  service = StravaService(settings, fastapi_app)
47
  logger.info("Initialized Strava service")
48
 
 
86
  Returns:
87
  List of activities
88
  """
 
 
89
  try:
90
+ # Safely access service from context
91
+ if not ctx.request_context.lifespan_context:
92
+ raise ValueError("Lifespan context not available")
93
+
94
+ # Cast service to StravaService to satisfy type checker
95
+ service = cast(StravaService, ctx.request_context.lifespan_context.get("service"))
96
+ if not service:
97
+ raise ValueError("Service not available in context")
98
+
99
  activities = await service.get_activities(before, after, page, per_page)
100
  return [activity.model_dump() for activity in activities]
101
  except Exception as e:
 
119
  Returns:
120
  The activity details
121
  """
 
 
122
  try:
123
+ # Safely access service from context
124
+ if not ctx.request_context.lifespan_context:
125
+ raise ValueError("Lifespan context not available")
126
+
127
+ # Cast service to StravaService to satisfy type checker
128
+ service = cast(StravaService, ctx.request_context.lifespan_context.get("service"))
129
+ if not service:
130
+ raise ValueError("Service not available in context")
131
+
132
  activity = await service.get_activity(activity_id, include_all_efforts)
133
  return activity.model_dump()
134
  except Exception as e:
 
150
  Returns:
151
  List of segment efforts for the activity
152
  """
 
 
153
  try:
154
+ # Safely access service from context
155
+ if not ctx.request_context.lifespan_context:
156
+ raise ValueError("Lifespan context not available")
157
+
158
+ # Cast service to StravaService to satisfy type checker
159
+ service = cast(StravaService, ctx.request_context.lifespan_context.get("service"))
160
+ if not service:
161
+ raise ValueError("Service not available in context")
162
+
163
  segments = await service.get_activity_segments(activity_id)
164
  return [segment.model_dump() for segment in segments]
165
  except Exception as e:
tests/test_api.py CHANGED
@@ -14,6 +14,7 @@ def settings():
14
  client_id="test_client_id",
15
  client_secret="test_client_secret",
16
  refresh_token="test_refresh_token",
 
17
  )
18
 
19
 
 
14
  client_id="test_client_id",
15
  client_secret="test_client_secret",
16
  refresh_token="test_refresh_token",
17
+ base_url="https://www.strava.com/api/v3",
18
  )
19
 
20
 
tests/test_config.py CHANGED
@@ -15,6 +15,7 @@ def test_strava_settings_defaults():
15
  client_id="test_client_id",
16
  client_secret="test_client_secret",
17
  refresh_token=None,
 
18
  )
19
 
20
  assert settings.client_id == "test_client_id"
@@ -34,7 +35,12 @@ def test_strava_settings_from_env():
34
  "STRAVA_BASE_URL": "https://custom.strava.api/v3",
35
  },
36
  ):
37
- settings = StravaSettings()
 
 
 
 
 
38
 
39
  assert settings.client_id == "env_client_id"
40
  assert settings.client_secret == "env_client_secret"
@@ -54,7 +60,9 @@ def test_strava_settings_override():
54
  ):
55
  settings = StravaSettings(
56
  client_id="direct_client_id",
 
57
  refresh_token="direct_refresh_token",
 
58
  )
59
 
60
  # Direct values should override environment variables
@@ -65,6 +73,9 @@ def test_strava_settings_override():
65
 
66
  def test_strava_settings_model_config():
67
  """Test model configuration for StravaSettings."""
68
- assert StravaSettings.model_config["env_prefix"] == "STRAVA_"
69
- assert StravaSettings.model_config["env_file"] == ".env"
70
- assert StravaSettings.model_config["env_file_encoding"] == "utf-8"
 
 
 
 
15
  client_id="test_client_id",
16
  client_secret="test_client_secret",
17
  refresh_token=None,
18
+ base_url="https://www.strava.com/api/v3",
19
  )
20
 
21
  assert settings.client_id == "test_client_id"
 
35
  "STRAVA_BASE_URL": "https://custom.strava.api/v3",
36
  },
37
  ):
38
+ # Even with env vars, we need to provide required params for type checking
39
+ settings = StravaSettings(
40
+ client_id="", # Will be overridden by env vars
41
+ client_secret="", # Will be overridden by env vars
42
+ base_url="", # Will be overridden by env vars
43
+ )
44
 
45
  assert settings.client_id == "env_client_id"
46
  assert settings.client_secret == "env_client_secret"
 
60
  ):
61
  settings = StravaSettings(
62
  client_id="direct_client_id",
63
+ client_secret="", # Will be taken from env vars
64
  refresh_token="direct_refresh_token",
65
+ base_url="https://www.strava.com/api/v3",
66
  )
67
 
68
  # Direct values should override environment variables
 
73
 
74
  def test_strava_settings_model_config():
75
  """Test model configuration for StravaSettings."""
76
+ # Access model_config safely, with type handling
77
+ model_config = StravaSettings.model_config
78
+ # We can safely access these fields as we know they exist in our configuration
79
+ assert model_config.get("env_prefix") == "STRAVA_"
80
+ assert model_config.get("env_file") == ".env"
81
+ assert model_config.get("env_file_encoding") == "utf-8"
tests/test_server.py CHANGED
@@ -1,8 +1,9 @@
 
1
  from unittest.mock import AsyncMock, MagicMock, patch
2
 
3
  import pytest
4
 
5
- from strava_mcp.models import Activity, DetailedActivity, SegmentEffort
6
 
7
  # Patch the StravaOAuthServer._run_server method to prevent coroutine warnings
8
  # This must be at the module level before any imports that might create the coroutine
@@ -45,8 +46,8 @@ async def test_get_user_activities(mock_ctx, mock_service):
45
  total_elevation_gain=50,
46
  type="Run",
47
  sport_type="Run",
48
- start_date="2023-01-01T10:00:00Z",
49
- start_date_local="2023-01-01T10:00:00Z",
50
  timezone="Europe/London",
51
  achievement_count=2,
52
  kudos_count=5,
@@ -63,6 +64,11 @@ async def test_get_user_activities(mock_ctx, mock_service):
63
  has_heartrate=True,
64
  average_heartrate=140,
65
  max_heartrate=160,
 
 
 
 
 
66
  )
67
  mock_service.get_activities.return_value = [mock_activity]
68
 
@@ -92,8 +98,8 @@ async def test_get_activity(mock_ctx, mock_service):
92
  total_elevation_gain=50,
93
  type="Run",
94
  sport_type="Run",
95
- start_date="2023-01-01T10:00:00Z",
96
- start_date_local="2023-01-01T10:00:00Z",
97
  timezone="Europe/London",
98
  achievement_count=2,
99
  kudos_count=5,
@@ -112,6 +118,19 @@ async def test_get_activity(mock_ctx, mock_service):
112
  max_heartrate=160,
113
  athlete={"id": 123},
114
  description="Test description",
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  )
116
  mock_service.get_activity.return_value = mock_activity
117
 
@@ -139,26 +158,36 @@ async def test_get_activity_segments(mock_ctx, mock_service):
139
  name="Test Segment",
140
  elapsed_time=180,
141
  moving_time=180,
142
- start_date="2023-01-01T10:05:00Z",
143
- start_date_local="2023-01-01T10:05:00Z",
144
  distance=1000,
145
  athlete={"id": 123},
146
- segment={
147
- "id": 12345,
148
- "name": "Test Segment",
149
- "activity_type": "Run",
150
- "distance": 1000,
151
- "average_grade": 5.0,
152
- "maximum_grade": 10.0,
153
- "elevation_high": 200,
154
- "elevation_low": 150,
155
- "total_elevation_gain": 50,
156
- "start_latlng": [51.5, -0.1],
157
- "end_latlng": [51.5, -0.2],
158
- "climb_category": 0,
159
- "private": False,
160
- "starred": False,
161
- },
 
 
 
 
 
 
 
 
 
 
162
  )
163
  mock_service.get_activity_segments.return_value = [mock_segment]
164
 
 
1
+ from datetime import datetime
2
  from unittest.mock import AsyncMock, MagicMock, patch
3
 
4
  import pytest
5
 
6
+ from strava_mcp.models import Activity, DetailedActivity, Segment, SegmentEffort
7
 
8
  # Patch the StravaOAuthServer._run_server method to prevent coroutine warnings
9
  # This must be at the module level before any imports that might create the coroutine
 
46
  total_elevation_gain=50,
47
  type="Run",
48
  sport_type="Run",
49
+ start_date=datetime.fromisoformat("2023-01-01T10:00:00+00:00"),
50
+ start_date_local=datetime.fromisoformat("2023-01-01T10:00:00+00:00"),
51
  timezone="Europe/London",
52
  achievement_count=2,
53
  kudos_count=5,
 
64
  has_heartrate=True,
65
  average_heartrate=140,
66
  max_heartrate=160,
67
+ # Add required fields with default values
68
+ map=None,
69
+ workout_type=None,
70
+ elev_high=None,
71
+ elev_low=None,
72
  )
73
  mock_service.get_activities.return_value = [mock_activity]
74
 
 
98
  total_elevation_gain=50,
99
  type="Run",
100
  sport_type="Run",
101
+ start_date=datetime.fromisoformat("2023-01-01T10:00:00+00:00"),
102
+ start_date_local=datetime.fromisoformat("2023-01-01T10:00:00+00:00"),
103
  timezone="Europe/London",
104
  achievement_count=2,
105
  kudos_count=5,
 
118
  max_heartrate=160,
119
  athlete={"id": 123},
120
  description="Test description",
121
+ # Add required fields with default values
122
+ map=None,
123
+ workout_type=None,
124
+ elev_high=None,
125
+ elev_low=None,
126
+ calories=None,
127
+ segment_efforts=None,
128
+ splits_metric=None,
129
+ splits_standard=None,
130
+ best_efforts=None,
131
+ photos=None,
132
+ gear=None,
133
+ device_name=None,
134
  )
135
  mock_service.get_activity.return_value = mock_activity
136
 
 
158
  name="Test Segment",
159
  elapsed_time=180,
160
  moving_time=180,
161
+ start_date=datetime.fromisoformat("2023-01-01T10:05:00+00:00"),
162
+ start_date_local=datetime.fromisoformat("2023-01-01T10:05:00+00:00"),
163
  distance=1000,
164
  athlete={"id": 123},
165
+ segment=Segment(
166
+ id=12345,
167
+ name="Test Segment",
168
+ activity_type="Run",
169
+ distance=1000,
170
+ average_grade=5.0,
171
+ maximum_grade=10.0,
172
+ elevation_high=200,
173
+ elevation_low=150,
174
+ total_elevation_gain=50,
175
+ start_latlng=[51.5, -0.1],
176
+ end_latlng=[51.5, -0.2],
177
+ climb_category=0,
178
+ private=False,
179
+ starred=False,
180
+ city=None,
181
+ state=None,
182
+ country=None,
183
+ ),
184
+ # Add required fields with default values
185
+ average_watts=None,
186
+ device_watts=None,
187
+ average_heartrate=None,
188
+ max_heartrate=None,
189
+ pr_rank=None,
190
+ achievements=None,
191
  )
192
  mock_service.get_activity_segments.return_value = [mock_segment]
193
 
tests/test_service.py CHANGED
@@ -1,9 +1,10 @@
 
1
  from unittest.mock import AsyncMock, patch
2
 
3
  import pytest
4
 
5
  from strava_mcp.config import StravaSettings
6
- from strava_mcp.models import Activity, DetailedActivity, SegmentEffort
7
  from strava_mcp.service import StravaService
8
 
9
 
@@ -13,6 +14,7 @@ def settings():
13
  client_id="test_client_id",
14
  client_secret="test_client_secret",
15
  refresh_token="test_refresh_token",
 
16
  )
17
 
18
 
@@ -44,8 +46,8 @@ async def test_get_activities(service, mock_api):
44
  total_elevation_gain=50,
45
  type="Run",
46
  sport_type="Run",
47
- start_date="2023-01-01T10:00:00Z",
48
- start_date_local="2023-01-01T10:00:00Z",
49
  timezone="Europe/London",
50
  achievement_count=2,
51
  kudos_count=5,
@@ -62,6 +64,11 @@ async def test_get_activities(service, mock_api):
62
  has_heartrate=True,
63
  average_heartrate=140,
64
  max_heartrate=160,
 
 
 
 
 
65
  )
66
  mock_api.get_activities.return_value = [mock_activity]
67
 
@@ -88,8 +95,8 @@ async def test_get_activity(service, mock_api):
88
  total_elevation_gain=50,
89
  type="Run",
90
  sport_type="Run",
91
- start_date="2023-01-01T10:00:00Z",
92
- start_date_local="2023-01-01T10:00:00Z",
93
  timezone="Europe/London",
94
  achievement_count=2,
95
  kudos_count=5,
@@ -108,6 +115,19 @@ async def test_get_activity(service, mock_api):
108
  max_heartrate=160,
109
  athlete={"id": 123},
110
  description="Test description",
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  )
112
  mock_api.get_activity.return_value = mock_activity
113
 
@@ -131,26 +151,36 @@ async def test_get_activity_segments(service, mock_api):
131
  name="Test Segment",
132
  elapsed_time=180,
133
  moving_time=180,
134
- start_date="2023-01-01T10:05:00Z",
135
- start_date_local="2023-01-01T10:05:00Z",
136
  distance=1000,
137
  athlete={"id": 123},
138
- segment={
139
- "id": 12345,
140
- "name": "Test Segment",
141
- "activity_type": "Run",
142
- "distance": 1000,
143
- "average_grade": 5.0,
144
- "maximum_grade": 10.0,
145
- "elevation_high": 200,
146
- "elevation_low": 150,
147
- "total_elevation_gain": 50,
148
- "start_latlng": [51.5, -0.1],
149
- "end_latlng": [51.5, -0.2],
150
- "climb_category": 0,
151
- "private": False,
152
- "starred": False,
153
- },
 
 
 
 
 
 
 
 
 
 
154
  )
155
  mock_api.get_activity_segments.return_value = [mock_segment]
156
 
 
1
+ from datetime import datetime
2
  from unittest.mock import AsyncMock, patch
3
 
4
  import pytest
5
 
6
  from strava_mcp.config import StravaSettings
7
+ from strava_mcp.models import Activity, DetailedActivity, Segment, SegmentEffort
8
  from strava_mcp.service import StravaService
9
 
10
 
 
14
  client_id="test_client_id",
15
  client_secret="test_client_secret",
16
  refresh_token="test_refresh_token",
17
+ base_url="https://www.strava.com/api/v3",
18
  )
19
 
20
 
 
46
  total_elevation_gain=50,
47
  type="Run",
48
  sport_type="Run",
49
+ start_date=datetime.fromisoformat("2023-01-01T10:00:00+00:00"),
50
+ start_date_local=datetime.fromisoformat("2023-01-01T10:00:00+00:00"),
51
  timezone="Europe/London",
52
  achievement_count=2,
53
  kudos_count=5,
 
64
  has_heartrate=True,
65
  average_heartrate=140,
66
  max_heartrate=160,
67
+ # Add required fields with default values
68
+ map=None,
69
+ workout_type=None,
70
+ elev_high=None,
71
+ elev_low=None,
72
  )
73
  mock_api.get_activities.return_value = [mock_activity]
74
 
 
95
  total_elevation_gain=50,
96
  type="Run",
97
  sport_type="Run",
98
+ start_date=datetime.fromisoformat("2023-01-01T10:00:00+00:00"),
99
+ start_date_local=datetime.fromisoformat("2023-01-01T10:00:00+00:00"),
100
  timezone="Europe/London",
101
  achievement_count=2,
102
  kudos_count=5,
 
115
  max_heartrate=160,
116
  athlete={"id": 123},
117
  description="Test description",
118
+ # Add required fields with default values
119
+ map=None,
120
+ workout_type=None,
121
+ elev_high=None,
122
+ elev_low=None,
123
+ calories=None,
124
+ segment_efforts=None,
125
+ splits_metric=None,
126
+ splits_standard=None,
127
+ best_efforts=None,
128
+ photos=None,
129
+ gear=None,
130
+ device_name=None,
131
  )
132
  mock_api.get_activity.return_value = mock_activity
133
 
 
151
  name="Test Segment",
152
  elapsed_time=180,
153
  moving_time=180,
154
+ start_date=datetime.fromisoformat("2023-01-01T10:05:00+00:00"),
155
+ start_date_local=datetime.fromisoformat("2023-01-01T10:05:00+00:00"),
156
  distance=1000,
157
  athlete={"id": 123},
158
+ segment=Segment(
159
+ id=12345,
160
+ name="Test Segment",
161
+ activity_type="Run",
162
+ distance=1000,
163
+ average_grade=5.0,
164
+ maximum_grade=10.0,
165
+ elevation_high=200,
166
+ elevation_low=150,
167
+ total_elevation_gain=50,
168
+ start_latlng=[51.5, -0.1],
169
+ end_latlng=[51.5, -0.2],
170
+ climb_category=0,
171
+ private=False,
172
+ starred=False,
173
+ city=None,
174
+ state=None,
175
+ country=None,
176
+ ),
177
+ # Add required fields with default values
178
+ average_watts=None,
179
+ device_watts=None,
180
+ average_heartrate=None,
181
+ max_heartrate=None,
182
+ pr_rank=None,
183
+ achievements=None,
184
  )
185
  mock_api.get_activity_segments.return_value = [mock_segment]
186