A strava clone service for storing running, hiking and ski routes
Run the service using docker-compose:
cp .env.example .env
docker compose up --buildThe dev compose setup reads LOGFIRE_TOKEN from .env and passes it into the service container so Logfire can export telemetry.
Set OPENAI_API_KEY in .env as well if you want to use the conversational route search endpoint.
Install dependencies and development tooling with uv:
uv sync --devRun checks and tests:
uv run ruff format --check .
uv run ruff check .
uv run pytest -s --junitxml=./test-report.xml --cov=./ --cov-report=xml .Install the development dependencies first:
uv sync --devWith the local compose stack running on http://localhost:8080, start Locust with:
make load-testThat opens the Locust UI on http://localhost:8089 and targets http://localhost:8080 by default.
To generate report files locally, run:
make load-test-reportThat writes artifacts to loadtest/reports/ by default:
loadtest/reports/locust.htmlloadtest/reports/locust_stats.csvloadtest/reports/locust_failures.csvloadtest/reports/locust_exceptions.csv
You can override the defaults with LOCUST_USERS, LOCUST_SPAWN_RATE, LOCUST_RUN_TIME, LOCUST_HOST, LOAD_TEST_REPORT_DIR, and LOAD_TEST_REPORT_PREFIX.
To run headless against local compose:
LOCUST_HOST=http://localhost:8080 uv run locust -f loadtest/locustfile.py --headless --users 10 --spawn-rate 2 --run-time 1mTo point the same test at a dev or prod service in a cluster, change only the target host and, if needed, the API prefix:
LOCUST_HOST=https://strava.dev.example.com STRAVA_API_PREFIX=/strava/v1 uv run locust -f loadtest/locustfile.py --headless --users 25 --spawn-rate 5 --run-time 5mThis service manages creating routes for activities much like strava does. A route can have a name and description, is an activity that can be one of RUNNING, HIKING or SKIING and has a route. We use geojson to describe the route and specifically a route is a 2D LineString to make things simple.
Let's create our first route:
curl -X 'POST' \
'http://0.0.0.0:8080/strava/v1/routes/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"name": "Sydney Harbour Loop",
"route": {
"type": "LineString",
"coordinates": [
[151.2093, -33.8688],
[151.2153, -33.8568],
[151.2217, -33.8523]
]
},
"activity": "RUNNING",
"description": "A run from Circular Quay up toward the Opera House"
}' | jqThis request uses an invalid activity value and should return 422 Unprocessable Entity:
curl -X 'POST' \
'http://0.0.0.0:8080/strava/v1/routes/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"name": "broken route",
"route": {
"type": "LineString",
"coordinates": [
[0.0, 0.0],
[1.0, 1.0]
]
},
"activity": "SWIMMING",
"description": "this should fail"
}' | jqThis request uses an invalid bbox value and should also return 422 Unprocessable Entity:
curl -X 'GET' \
'http://0.0.0.0:8080/strava/v1/routes/?bbox=1.0,2.0,3.0' \
-H 'accept: application/json' | jqNow we can list the routes we've created:
curl -X 'GET' \
'http://0.0.0.0:8080/strava/v1/routes/' \
-H 'accept: application/json' | jqThis endpoint uses pydantic_ai with the OpenAI Python library to translate natural language into route filters. When a place is mentioned, the agent can resolve it to a bounding box before querying the database.
curl -X 'POST' \
'http://0.0.0.0:8080/strava/v1/routes/search' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"query": "Running routes in Sydney Australia"
}' | jqWe also provide an endpoint for doing spatial queries (intersection) given a Geometry. This can be any of the geometry types in the geojson spec.
To execute a spatial query, POST a geojson containing your query shape to /strava/v1/routes/intersect:
curl -X 'POST' \
'http://localhost:8080/strava/v1/routes/intersect' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"type": "Polygon",
"coordinates": [
[
[-123.98835979521789,49.415858366810966],
[-122.91169963896789,49.729333082944635],
[-122.01082073271789,49.27270395054359],
[-122.25251995146789,48.73940752346975],
[-123.98835979521789,49.415858366810966]
]
]
}' | jqTo create a new migration, run the docker compose and open a shell into the service container:
docker exec -it strava-service-1 bashThen use alembic's autogenerate feature:
alembic revision --autogenerate -m "Added route table"