Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

@@ -0,0 +1 @@
# services/external/app/schemas/__init__.py

View File

@@ -0,0 +1,134 @@
# services/external/app/schemas/calendar.py
"""
Calendar Schemas - Request/Response types for school calendars and location context
"""
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from uuid import UUID
class SchoolCalendarResponse(BaseModel):
"""School calendar information"""
calendar_id: str
calendar_name: str
city_id: str
school_type: str
academic_year: str
holiday_periods: List[Dict[str, Any]]
school_hours: Dict[str, Any]
source: Optional[str] = None
enabled: bool = True
class Config:
json_schema_extra = {
"example": {
"calendar_id": "madrid_primary_2024_2025",
"calendar_name": "Madrid Primary School Calendar 2024-2025",
"city_id": "madrid",
"school_type": "primary",
"academic_year": "2024-2025",
"holiday_periods": [
{
"name": "Christmas Holiday",
"start_date": "2024-12-23",
"end_date": "2025-01-07",
"description": "Christmas and New Year break"
}
],
"school_hours": {
"morning_start": "09:00",
"morning_end": "14:00",
"has_afternoon_session": False
},
"source": "madrid_education_dept_2024",
"enabled": True
}
}
class SchoolCalendarListResponse(BaseModel):
"""List of school calendars for a city"""
city_id: str
calendars: List[SchoolCalendarResponse]
total: int
class CalendarCheckResponse(BaseModel):
"""Response for holiday check"""
date: str = Field(..., description="Date checked (ISO format)")
is_holiday: bool = Field(..., description="Whether the date is a school holiday")
holiday_name: Optional[str] = Field(None, description="Name of the holiday if applicable")
calendar_id: str
calendar_name: str
class TenantLocationContextResponse(BaseModel):
"""Tenant location context with calendar details"""
tenant_id: str
city_id: str
neighborhood: Optional[str] = None
local_events: Optional[List[Dict[str, Any]]] = None
notes: Optional[str] = None
calendar: Optional[Dict[str, Any]] = Field(
None,
description="Full calendar details if assigned"
)
class Config:
json_schema_extra = {
"example": {
"tenant_id": "fbffcf18-d02a-4104-b6e3-0b32006e3e47",
"city_id": "madrid",
"neighborhood": "Chamberí",
"local_events": [
{
"name": "Neighborhood Festival",
"date": "2025-06-15",
"impact": "high"
}
],
"notes": "Bakery near primary school",
"calendar": {
"calendar_id": "uuid",
"calendar_name": "Madrid Primary School Calendar 2024-2025",
"school_type": "primary",
"academic_year": "2024-2025",
"holiday_periods": [],
"school_hours": {},
"source": "madrid_education_dept_2024"
}
}
}
class TenantLocationContextCreateRequest(BaseModel):
"""Request to create/update tenant location context"""
city_id: str = Field(..., description="City ID (e.g., 'madrid')")
school_calendar_id: Optional[UUID] = Field(
None,
description="School calendar ID to assign"
)
neighborhood: Optional[str] = Field(None, description="Neighborhood name")
local_events: Optional[List[Dict[str, Any]]] = Field(
None,
description="Local events specific to this location"
)
notes: Optional[str] = Field(None, description="Additional notes")
class Config:
json_schema_extra = {
"example": {
"city_id": "madrid",
"school_calendar_id": "123e4567-e89b-12d3-a456-426614174000",
"neighborhood": "Chamberí",
"local_events": [
{
"name": "Local Market Day",
"date": "2025-05-20",
"impact": "medium"
}
],
"notes": "Bakery located near primary school entrance"
}
}

View File

@@ -0,0 +1,36 @@
# services/external/app/schemas/city_data.py
"""
City Data Schemas - New response types for city-based operations
"""
from pydantic import BaseModel, Field
from typing import Optional
class CityInfoResponse(BaseModel):
"""Information about a supported city"""
city_id: str
name: str
country: str
latitude: float
longitude: float
radius_km: float
weather_provider: str
traffic_provider: str
enabled: bool
class DataAvailabilityResponse(BaseModel):
"""Data availability for a city"""
city_id: str
city_name: str
weather_available: bool
weather_start_date: Optional[str] = None
weather_end_date: Optional[str] = None
weather_record_count: int = 0
traffic_available: bool
traffic_start_date: Optional[str] = None
traffic_end_date: Optional[str] = None
traffic_record_count: int = 0

106
services/external/app/schemas/traffic.py vendored Normal file
View File

@@ -0,0 +1,106 @@
# services/external/app/schemas/traffic.py
"""
Traffic Service Pydantic Schemas
"""
from pydantic import BaseModel, Field, field_validator
from datetime import datetime
from typing import Optional, List
from uuid import UUID
class TrafficDataBase(BaseModel):
"""Base traffic data schema"""
location_id: str = Field(..., max_length=100, description="Traffic monitoring location ID")
date: datetime = Field(..., description="Date and time of traffic measurement")
traffic_volume: Optional[int] = Field(None, ge=0, description="Vehicles per hour")
pedestrian_count: Optional[int] = Field(None, ge=0, description="Pedestrians per hour")
congestion_level: Optional[str] = Field(None, pattern="^(low|medium|high)$", description="Traffic congestion level")
average_speed: Optional[float] = Field(None, ge=0, le=200, description="Average speed in km/h")
source: str = Field("madrid_opendata", max_length=50, description="Data source")
raw_data: Optional[str] = Field(None, description="Raw data from source")
class TrafficDataCreate(TrafficDataBase):
"""Schema for creating traffic data"""
pass
class TrafficDataUpdate(BaseModel):
"""Schema for updating traffic data"""
traffic_volume: Optional[int] = Field(None, ge=0)
pedestrian_count: Optional[int] = Field(None, ge=0)
congestion_level: Optional[str] = Field(None, pattern="^(low|medium|high)$")
average_speed: Optional[float] = Field(None, ge=0, le=200)
raw_data: Optional[str] = None
class TrafficDataResponseDB(TrafficDataBase):
"""Schema for traffic data responses from database"""
id: str = Field(..., description="Unique identifier")
created_at: datetime = Field(..., description="Creation timestamp")
updated_at: datetime = Field(..., description="Last update timestamp")
@field_validator('id', mode='before')
@classmethod
def convert_uuid_to_string(cls, v):
if isinstance(v, UUID):
return str(v)
return v
class Config:
from_attributes = True
json_encoders = {
datetime: lambda v: v.isoformat()
}
class TrafficDataList(BaseModel):
"""Schema for paginated traffic data responses"""
data: List[TrafficDataResponseDB]
total: int = Field(..., description="Total number of records")
page: int = Field(..., description="Current page number")
per_page: int = Field(..., description="Records per page")
has_next: bool = Field(..., description="Whether there are more pages")
has_prev: bool = Field(..., description="Whether there are previous pages")
class TrafficAnalytics(BaseModel):
"""Schema for traffic analytics"""
location_id: str
period_start: datetime
period_end: datetime
avg_traffic_volume: Optional[float] = None
avg_pedestrian_count: Optional[float] = None
peak_traffic_hour: Optional[int] = None
peak_pedestrian_hour: Optional[int] = None
congestion_distribution: dict = Field(default_factory=dict)
avg_speed: Optional[float] = None
class TrafficDataResponse(BaseModel):
"""Schema for API traffic data responses"""
date: datetime = Field(..., description="Date and time of traffic measurement")
traffic_volume: Optional[int] = Field(None, ge=0, description="Vehicles per hour")
pedestrian_count: Optional[int] = Field(None, ge=0, description="Pedestrians per hour")
congestion_level: Optional[str] = Field(None, pattern="^(low|medium|high)$", description="Traffic congestion level")
average_speed: Optional[float] = Field(None, ge=0, le=200, description="Average speed in km/h")
source: str = Field(..., description="Data source")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
class LocationRequest(BaseModel):
latitude: float
longitude: float
address: Optional[str] = None
class DateRangeRequest(BaseModel):
start_date: datetime
end_date: datetime
class HistoricalTrafficRequest(BaseModel):
latitude: float
longitude: float
start_date: datetime
end_date: datetime
class TrafficForecastRequest(BaseModel):
latitude: float
longitude: float
hours: int = 24

173
services/external/app/schemas/weather.py vendored Normal file
View File

@@ -0,0 +1,173 @@
# services/external/app/schemas/weather.py
"""Weather data schemas"""
from pydantic import BaseModel, Field, field_validator
from datetime import datetime
from typing import Optional, List
from uuid import UUID
class WeatherDataBase(BaseModel):
"""Base weather data schema"""
location_id: str = Field(..., max_length=100, description="Weather monitoring location ID")
date: datetime = Field(..., description="Date and time of weather measurement")
temperature: Optional[float] = Field(None, ge=-50, le=60, description="Temperature in Celsius")
precipitation: Optional[float] = Field(None, ge=0, description="Precipitation in mm")
humidity: Optional[float] = Field(None, ge=0, le=100, description="Humidity percentage")
wind_speed: Optional[float] = Field(None, ge=0, le=200, description="Wind speed in km/h")
pressure: Optional[float] = Field(None, ge=800, le=1200, description="Atmospheric pressure in hPa")
description: Optional[str] = Field(None, max_length=200, description="Weather description")
source: str = Field("aemet", max_length=50, description="Data source")
raw_data: Optional[str] = Field(None, description="Raw data from source")
class WeatherDataCreate(WeatherDataBase):
"""Schema for creating weather data"""
pass
class WeatherDataUpdate(BaseModel):
"""Schema for updating weather data"""
temperature: Optional[float] = Field(None, ge=-50, le=60)
precipitation: Optional[float] = Field(None, ge=0)
humidity: Optional[float] = Field(None, ge=0, le=100)
wind_speed: Optional[float] = Field(None, ge=0, le=200)
pressure: Optional[float] = Field(None, ge=800, le=1200)
description: Optional[str] = Field(None, max_length=200)
raw_data: Optional[str] = None
class WeatherDataResponse(WeatherDataBase):
"""Schema for weather data responses"""
id: str = Field(..., description="Unique identifier")
created_at: datetime = Field(..., description="Creation timestamp")
updated_at: datetime = Field(..., description="Last update timestamp")
@field_validator('id', mode='before')
@classmethod
def convert_uuid_to_string(cls, v):
if isinstance(v, UUID):
return str(v)
return v
class Config:
from_attributes = True
json_encoders = {
datetime: lambda v: v.isoformat()
}
class WeatherForecastBase(BaseModel):
"""Base weather forecast schema"""
location_id: str = Field(..., max_length=100, description="Location ID")
forecast_date: datetime = Field(..., description="Date for forecast")
temperature: Optional[float] = Field(None, ge=-50, le=60, description="Forecasted temperature")
precipitation: Optional[float] = Field(None, ge=0, description="Forecasted precipitation")
humidity: Optional[float] = Field(None, ge=0, le=100, description="Forecasted humidity")
wind_speed: Optional[float] = Field(None, ge=0, le=200, description="Forecasted wind speed")
description: Optional[str] = Field(None, max_length=200, description="Forecast description")
source: str = Field("aemet", max_length=50, description="Data source")
raw_data: Optional[str] = Field(None, description="Raw forecast data")
class WeatherForecastCreate(WeatherForecastBase):
"""Schema for creating weather forecasts"""
pass
class WeatherForecastResponse(WeatherForecastBase):
"""Schema for weather forecast responses"""
id: str = Field(..., description="Unique identifier")
generated_at: datetime = Field(..., description="When forecast was generated")
created_at: datetime = Field(..., description="Creation timestamp")
updated_at: datetime = Field(..., description="Last update timestamp")
@field_validator('id', mode='before')
@classmethod
def convert_uuid_to_string(cls, v):
if isinstance(v, UUID):
return str(v)
return v
class Config:
from_attributes = True
json_encoders = {
datetime: lambda v: v.isoformat()
}
class WeatherDataList(BaseModel):
"""Schema for paginated weather data responses"""
data: List[WeatherDataResponse]
total: int = Field(..., description="Total number of records")
page: int = Field(..., description="Current page number")
per_page: int = Field(..., description="Records per page")
has_next: bool = Field(..., description="Whether there are more pages")
has_prev: bool = Field(..., description="Whether there are previous pages")
class WeatherForecastList(BaseModel):
"""Schema for paginated weather forecast responses"""
forecasts: List[WeatherForecastResponse]
total: int = Field(..., description="Total number of forecasts")
page: int = Field(..., description="Current page number")
per_page: int = Field(..., description="Forecasts per page")
class WeatherAnalytics(BaseModel):
"""Schema for weather analytics"""
location_id: str
period_start: datetime
period_end: datetime
avg_temperature: Optional[float] = None
min_temperature: Optional[float] = None
max_temperature: Optional[float] = None
total_precipitation: Optional[float] = None
avg_humidity: Optional[float] = None
avg_wind_speed: Optional[float] = None
avg_pressure: Optional[float] = None
weather_conditions: dict = Field(default_factory=dict)
rainy_days: int = 0
sunny_days: int = 0
class LocationRequest(BaseModel):
latitude: float
longitude: float
address: Optional[str] = None
class DateRangeRequest(BaseModel):
start_date: datetime
end_date: datetime
class HistoricalWeatherRequest(BaseModel):
latitude: float
longitude: float
start_date: datetime
end_date: datetime
class WeatherForecastRequest(BaseModel):
latitude: float
longitude: float
days: int
class HourlyForecastRequest(BaseModel):
latitude: float
longitude: float
hours: int = Field(default=48, ge=1, le=48, description="Number of hours to forecast (1-48)")
class HourlyForecastResponse(BaseModel):
forecast_datetime: datetime
generated_at: datetime
temperature: Optional[float]
precipitation: Optional[float]
humidity: Optional[float]
wind_speed: Optional[float]
description: Optional[str]
source: str
hour: int
class WeatherForecastAPIResponse(BaseModel):
"""Simplified schema for API weather forecast responses (without database fields)"""
forecast_date: datetime = Field(..., description="Date for forecast")
generated_at: datetime = Field(..., description="When forecast was generated")
temperature: Optional[float] = Field(None, ge=-50, le=60, description="Forecasted temperature")
precipitation: Optional[float] = Field(None, ge=0, description="Forecasted precipitation")
humidity: Optional[float] = Field(None, ge=0, le=100, description="Forecasted humidity")
wind_speed: Optional[float] = Field(None, ge=0, le=200, description="Forecasted wind speed")
description: Optional[str] = Field(None, max_length=200, description="Forecast description")
source: str = Field("aemet", max_length=50, description="Data source")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}