Skip to content

Commit e5797ed

Browse files
committed
add missing files
1 parent d8dda47 commit e5797ed

5 files changed

Lines changed: 280 additions & 0 deletions

File tree

.github/copilot-instructions.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Mobility Feed API - AI Coding Assistant Instructions
2+
3+
## Project Architecture
4+
5+
This is a **mobility data API service** built with FastAPI, serving open mobility data from across the world. The architecture follows a **code-generation pattern** with clear separation between generated and implementation code.
6+
7+
### Core Components
8+
9+
- **`api/`**: Main FastAPI application with spec-first development using OpenAPI Generator
10+
- **`functions-python/`**: Google Cloud Functions for data processing (batch jobs, validation, analytics)
11+
- **`web-app/`**: Frontend application
12+
- **PostgreSQL + PostGIS**: Database with geospatial support for mobility data
13+
14+
### Key Generated vs Implementation Split
15+
16+
- **Generated code** (never edit): `api/src/feeds_gen/` and `api/src/shared/database_gen/`
17+
- **Implementation code**: `api/src/feeds/impl/` contains actual business logic
18+
- **Schema source**: `docs/DatabaseCatalogAPI.yaml` drives code generation
19+
20+
## Critical Development Workflows
21+
22+
### Initial Setup
23+
```bash
24+
# One-time OpenAPI setup
25+
scripts/setup-openapi-generator.sh
26+
27+
# Install dependencies
28+
cd api && pip3 install -r requirements.txt -r requirements_dev.txt
29+
30+
# Start local database
31+
docker-compose --env-file ./config/.env.local up -d --force-recreate
32+
33+
# Generate API stubs (run after schema changes)
34+
scripts/api-gen.sh
35+
scripts/db-gen.sh
36+
```
37+
38+
### Common Development Commands
39+
```bash
40+
# Start API server (includes Swagger UI at http://localhost:8080/docs/)
41+
scripts/api-start.sh
42+
43+
# Run tests with coverage
44+
scripts/api-tests.sh
45+
# Run specific test file
46+
scripts/api-tests.sh my_test_file.py
47+
48+
# Lint checks (Flake8 + Black)
49+
scripts/lint-tests.sh
50+
51+
# Reset and populate local database
52+
./scripts/docker-localdb-rebuild-data.sh --populate-db
53+
# Include test datasets
54+
./scripts/docker-localdb-rebuild-data.sh --populate-db --populate-test-data
55+
```
56+
57+
## Project-Specific Patterns
58+
59+
### Error Handling Convention
60+
- Use `shared.common.error_handling.InternalHTTPException` for internal errors
61+
- Convert to FastAPI HTTPException using `feeds.impl.error_handling.convert_exception()`
62+
- Store error messages as Finals in `api/src/feeds/impl/error_handling.py`
63+
- Error responses follow: `{"details": "The error message"}`
64+
65+
### Database Patterns
66+
- **Polymorphic inheritance**: `Feed` base class with `GtfsFeed`, `GbfsFeed`, `GtfsRTFeed` subclasses
67+
- **SQLAlchemy ORM**: Models in `shared/database_gen/sqlacodegen_models.py` (generated)
68+
- **Session management**: Use `@with_db_session` decorator for database operations
69+
- **Unique IDs**: Generate with `generate_unique_id()` (36-char UUID4)
70+
71+
### API Implementation Structure
72+
- Endpoints in `feeds/impl/*_api_impl.py` extend generated base classes from `feeds_gen/`
73+
- Filter classes in `shared/feed_filters/` for query parameter handling
74+
- Model implementations in `feeds/impl/models/` extend generated models
75+
76+
### Code Generation Workflow
77+
1. Modify `docs/DatabaseCatalogAPI.yaml` for API changes
78+
2. Run `scripts/api-gen.sh` to regenerate FastAPI stubs
79+
3. Run `scripts/db-gen.sh` for database schema changes
80+
4. Implement business logic in `feeds/impl/` classes
81+
82+
### Testing Patterns
83+
- Tests use empty local test DB (reset with `--use-test-db` flag)
84+
- Coverage reports in `scripts/coverage_reports/`
85+
- Python path configured to `src/` in `pyproject.toml`
86+
87+
### Functions Architecture
88+
- **Google Cloud Functions** in `functions-python/` for background processing
89+
- Shared database models via `database_gen/` symlink
90+
- Each function has its own deployment configuration
91+
- Tasks include: validation reports, batch datasets, GBFS validation, BigQuery ingestion
92+
93+
### Authentication
94+
- **OAuth2 Bearer tokens** for API access
95+
- Refresh tokens from mobilitydatabase.org account
96+
- Access tokens valid for 1 hour
97+
- Test endpoint: `/v1/metadata` with Bearer token
98+
99+
## Integration Points
100+
101+
- **BigQuery**: Analytics data pipeline via `big_query_ingestion/` function
102+
- **PostGIS**: Geospatial queries for location-based feed filtering
103+
- **Liquibase**: Database schema migrations in `liquibase/` directory
104+
- **Docker**: Multi-service setup with PostgreSQL, test DB, and schema documentation
105+
106+
## File Exclusions for AI Context
107+
- Skip `src/feeds_gen/*` and `src/shared/database_gen/*` (generated code)
108+
- Skip `data/` and `data-test/` (database volumes)
109+
- Skip `htmlcov/` (coverage reports)
110+
- Black formatter excludes these paths automatically
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from packaging.version import Version
2+
3+
4+
def compare_java_versions(v1: str | None, v2: str | None):
5+
"""
6+
Compare two version strings v1 and v2.
7+
Returns 1 if v1 > v2, -1 if v1 < v2,
8+
otherwise 0.
9+
The version strings are expected to be in the format of
10+
major.minor.patch[-SNAPSHOT]
11+
"""
12+
if v1 is None and v2 is None:
13+
return 0
14+
if v1 is None:
15+
return -1
16+
if v2 is None:
17+
return 1
18+
# clean version strings replacing the SNAPSHOT suffix with .dev0
19+
v1 = v1.replace("-SNAPSHOT", ".dev0")
20+
v2 = v2.replace("-SNAPSHOT", ".dev0")
21+
if Version(v1) > Version(v2):
22+
return 1
23+
elif Version(v1) < Version(v2):
24+
return -1
25+
else:
26+
return 0
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from feeds_gen.models.operation_gtfs_feed import OperationGtfsFeed
2+
from shared.database_gen.sqlacodegen_models import Gtfsfeed
3+
from shared.db_models.gtfs_feed_impl import GtfsFeedImpl
4+
5+
6+
class OperationGtfsFeedImpl(GtfsFeedImpl, OperationGtfsFeed):
7+
"""Base implementation of the feeds models."""
8+
9+
class Config:
10+
"""Pydantic configuration.
11+
Enabling `from_attributes` method to create a model instance from a SQLAlchemy row object.
12+
"""
13+
14+
from_attributes = True
15+
16+
def __init__(self, **data):
17+
super().__init__(**data)
18+
self.locations = self.locations or []
19+
self.redirects = self.redirects or []
20+
21+
@classmethod
22+
def from_orm(cls, feed: Gtfsfeed | None) -> OperationGtfsFeed | None:
23+
"""Convert a SQLAlchemy row object to a Pydantic model."""
24+
if not feed:
25+
return None
26+
operation_gtfs_feed = super().from_orm(feed)
27+
if not operation_gtfs_feed:
28+
return None
29+
30+
data = dict(operation_gtfs_feed.__dict__)
31+
# Override id and add stable_id
32+
data["id"] = feed.id
33+
data["stable_id"] = feed.stable_id
34+
# Add missing fields from public API model
35+
data["operational_status"] = feed.operational_status
36+
37+
return cls.model_construct(**data)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from feeds_gen.models.operation_gtfs_rt_feed import OperationGtfsRtFeed
2+
from shared.database_gen.sqlacodegen_models import Gtfsrealtimefeed
3+
from shared.db_models.gtfs_rt_feed_impl import GtfsRTFeedImpl
4+
5+
6+
class OperationGtfsRtFeedImpl(GtfsRTFeedImpl, OperationGtfsRtFeed):
7+
"""Base implementation of the feeds models."""
8+
9+
class Config:
10+
"""Pydantic configuration.
11+
Enabling `from_attributes` method to create a model instance from a SQLAlchemy row object.
12+
"""
13+
14+
from_attributes = True
15+
16+
def __init__(self, **data):
17+
super().__init__(**data)
18+
self.entity_types = self.entity_types or []
19+
self.locations = self.locations or []
20+
self.redirects = self.redirects or []
21+
self.feed_references = self.feed_references or []
22+
23+
@classmethod
24+
def from_orm(cls, feed: Gtfsrealtimefeed | None) -> OperationGtfsRtFeed | None:
25+
"""Convert a SQLAlchemy row object to a Pydantic model."""
26+
if not feed:
27+
return None
28+
operation_gtfs_feed = super().from_orm(feed)
29+
if not operation_gtfs_feed:
30+
return None
31+
32+
data = dict(operation_gtfs_feed.__dict__)
33+
# Override id and add stable_id
34+
data["id"] = feed.id
35+
data["stable_id"] = feed.stable_id
36+
# Add missing fields from public API model
37+
data["operational_status"] = feed.operational_status
38+
39+
return cls.model_construct(**data)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Usage:
5+
# ./scripts/api-operations-update-schema.sh \
6+
# [--source ./docs/DatabaseCatalogAPI.yaml] \
7+
# [--dest ./docs/OperationsAPI.yaml]
8+
#
9+
# Behavior:
10+
# - Replaces components.schemas in Operations with those from Catalog.
11+
# - Preserves only schemas in Operations that have x-operation: true at the schema root (these override source).
12+
# - Removes any non-operation schemas that exist only in Operations.
13+
14+
SCRIPT_DIR="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
15+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
16+
17+
SOURCE=""
18+
DEST=""
19+
20+
while [[ $# -gt 0 ]]; do
21+
case "$1" in
22+
--source|-s) SOURCE="${2:-}"; shift 2 ;;
23+
--dest|-d) DEST="${2:-}"; shift 2 ;;
24+
-h|--help) echo "Usage: $0 [--source <CatalogYAML>] [--dest <OperationsYAML>]"; exit 0 ;;
25+
*) echo "Unknown arg: $1" >&2; exit 1 ;;
26+
esac
27+
done
28+
29+
: "${SOURCE:=${REPO_ROOT}/docs/DatabaseCatalogAPI.yaml}"
30+
: "${DEST:=${REPO_ROOT}/docs/OperationsAPI.yaml}"
31+
32+
if ! command -v yq >/dev/null 2>&1; then
33+
echo "yq not found. Install with: brew install yq" >&2
34+
exit 1
35+
fi
36+
YQ_MAJOR="$(yq --version 2>/dev/null | sed -n 's/.*version v\([0-9][0-9]*\).*/\1/p')"
37+
if [[ -z "${YQ_MAJOR:-}" || "${YQ_MAJOR}" -lt 4 ]]; then
38+
echo "yq v4+ required. Current: $(yq --version 2>/dev/null)" >&2
39+
exit 1
40+
fi
41+
42+
[[ -f "${SOURCE}" ]] || { echo "Source not found: ${SOURCE}" >&2; exit 1; }
43+
[[ -f "${DEST}" ]] || { echo "Dest not found: ${DEST}" >&2; exit 1; }
44+
45+
cp -f "${DEST}" "${DEST}.bak"
46+
47+
# Merge strategy:
48+
# - Start from source schemas (Catalog): ensures Operations aligns with source by default
49+
# - Overlay ONLY the destination schemas that are explicitly marked with x-operation: true
50+
# (these are preserved and override the source)
51+
# - Any non-operation schemas that exist only in Operations are DROPPED
52+
SRC_ABS="$(cd "$(dirname "${SOURCE}")" && pwd)/$(basename "${SOURCE}")"
53+
export SRC="${SRC_ABS}"
54+
55+
yq -i '
56+
(.components.schemas // {}) as $dst
57+
| (load(strenv(SRC)).components.schemas // {}) as $src
58+
| .components.schemas = (
59+
$src
60+
* (
61+
$dst
62+
| with_entries( select(.value."x-operation" == true) )
63+
)
64+
)
65+
' "${DEST}"
66+
67+
echo "Synced schemas from ${SOURCE} -> ${DEST} (${DEST}.bak created)."
68+
echo "Note: Schemas in Operations with x-operation: true were preserved."

0 commit comments

Comments
 (0)