Initial commit - production deployment
This commit is contained in:
46
services/external/app/models/__init__.py
vendored
Normal file
46
services/external/app/models/__init__.py
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
External Service Models Package
|
||||
|
||||
Import all models to ensure they are registered with SQLAlchemy Base.
|
||||
"""
|
||||
|
||||
# Import AuditLog model for this service
|
||||
from shared.security import create_audit_log_model
|
||||
from shared.database.base import Base
|
||||
|
||||
# Create audit log model for this service
|
||||
AuditLog = create_audit_log_model(Base)
|
||||
|
||||
# Import all models to register them with the Base metadata
|
||||
from .traffic import (
|
||||
TrafficData,
|
||||
TrafficMeasurementPoint,
|
||||
TrafficDataBackgroundJob,
|
||||
)
|
||||
|
||||
from .weather import (
|
||||
WeatherData,
|
||||
WeatherForecast,
|
||||
)
|
||||
|
||||
from .city_weather import CityWeatherData
|
||||
from .city_traffic import CityTrafficData
|
||||
from .calendar import SchoolCalendar, TenantLocationContext
|
||||
|
||||
# List all models for easier access
|
||||
__all__ = [
|
||||
# Traffic models
|
||||
"TrafficData",
|
||||
"TrafficMeasurementPoint",
|
||||
"TrafficDataBackgroundJob",
|
||||
# Weather models
|
||||
"WeatherData",
|
||||
"WeatherForecast",
|
||||
# City-based models (new)
|
||||
"CityWeatherData",
|
||||
"CityTrafficData",
|
||||
# Calendar models (hyperlocal)
|
||||
"SchoolCalendar",
|
||||
"TenantLocationContext",
|
||||
"AuditLog",
|
||||
]
|
||||
86
services/external/app/models/calendar.py
vendored
Normal file
86
services/external/app/models/calendar.py
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
# services/external/app/models/calendar.py
|
||||
"""
|
||||
School Calendar and Tenant Location Context Models
|
||||
Hyperlocal data for demand forecasting
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, Index, Boolean
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class SchoolCalendar(Base):
|
||||
"""City-based school calendar data for forecasting"""
|
||||
|
||||
__tablename__ = "school_calendars"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
city_id = Column(String(50), nullable=False, index=True)
|
||||
calendar_name = Column(String(100), nullable=False)
|
||||
school_type = Column(String(20), nullable=False) # primary, secondary, university
|
||||
academic_year = Column(String(10), nullable=False) # e.g., "2024-2025"
|
||||
|
||||
# Holiday periods as array of date ranges
|
||||
# Example: [
|
||||
# {"name": "Christmas", "start": "2024-12-20", "end": "2025-01-08"},
|
||||
# {"name": "Easter", "start": "2025-04-10", "end": "2025-04-21"},
|
||||
# {"name": "Summer", "start": "2025-06-23", "end": "2025-09-09"}
|
||||
# ]
|
||||
holiday_periods = Column(JSONB, nullable=False, default=list)
|
||||
|
||||
# School hours configuration
|
||||
# Example: {
|
||||
# "morning_start": "09:00",
|
||||
# "morning_end": "14:00",
|
||||
# "afternoon_start": "15:00", # if applicable
|
||||
# "afternoon_end": "17:00",
|
||||
# "has_afternoon_session": false
|
||||
# }
|
||||
school_hours = Column(JSONB, nullable=False, default=dict)
|
||||
|
||||
# Metadata
|
||||
source = Column(String(100), nullable=True) # e.g., "madrid_education_dept"
|
||||
enabled = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_school_calendar_city_year', 'city_id', 'academic_year'),
|
||||
Index('idx_school_calendar_city_type', 'city_id', 'school_type'),
|
||||
)
|
||||
|
||||
|
||||
class TenantLocationContext(Base):
|
||||
"""Tenant-specific location context for hyperlocal forecasting"""
|
||||
|
||||
__tablename__ = "tenant_location_contexts"
|
||||
|
||||
tenant_id = Column(UUID(as_uuid=True), primary_key=True)
|
||||
city_id = Column(String(50), nullable=False, index=True)
|
||||
|
||||
# School calendar assignment
|
||||
school_calendar_id = Column(UUID(as_uuid=True), nullable=True, index=True)
|
||||
|
||||
# Hyperlocal context
|
||||
neighborhood = Column(String(100), nullable=True)
|
||||
|
||||
# Custom local events specific to this tenant's location
|
||||
# Example: [
|
||||
# {"name": "Neighborhood Festival", "date": "2025-06-15", "impact": "high"},
|
||||
# {"name": "Local Market Day", "date": "2025-05-20", "impact": "medium"}
|
||||
# ]
|
||||
local_events = Column(JSONB, nullable=True, default=list)
|
||||
|
||||
# Additional metadata
|
||||
notes = Column(String(500), nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_tenant_location_calendar', 'school_calendar_id'),
|
||||
)
|
||||
36
services/external/app/models/city_traffic.py
vendored
Normal file
36
services/external/app/models/city_traffic.py
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# services/external/app/models/city_traffic.py
|
||||
"""
|
||||
City Traffic Data Model - Shared city-based traffic storage
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, Integer, Float, DateTime, Text, Index
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class CityTrafficData(Base):
|
||||
"""City-based historical traffic data"""
|
||||
|
||||
__tablename__ = "city_traffic_data"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
city_id = Column(String(50), nullable=False, index=True)
|
||||
date = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
|
||||
traffic_volume = Column(Integer, nullable=True)
|
||||
pedestrian_count = Column(Integer, nullable=True)
|
||||
congestion_level = Column(String(20), nullable=True)
|
||||
average_speed = Column(Float, nullable=True)
|
||||
|
||||
source = Column(String(50), nullable=False)
|
||||
raw_data = Column(JSONB, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_city_traffic_lookup', 'city_id', 'date'),
|
||||
)
|
||||
38
services/external/app/models/city_weather.py
vendored
Normal file
38
services/external/app/models/city_weather.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# services/external/app/models/city_weather.py
|
||||
"""
|
||||
City Weather Data Model - Shared city-based weather storage
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, Float, DateTime, Text, Index
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class CityWeatherData(Base):
|
||||
"""City-based historical weather data"""
|
||||
|
||||
__tablename__ = "city_weather_data"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
city_id = Column(String(50), nullable=False, index=True)
|
||||
date = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
|
||||
temperature = Column(Float, nullable=True)
|
||||
precipitation = Column(Float, nullable=True)
|
||||
humidity = Column(Float, nullable=True)
|
||||
wind_speed = Column(Float, nullable=True)
|
||||
pressure = Column(Float, nullable=True)
|
||||
description = Column(String(200), nullable=True)
|
||||
|
||||
source = Column(String(50), nullable=False)
|
||||
raw_data = Column(JSONB, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_city_weather_lookup', 'city_id', 'date'),
|
||||
)
|
||||
123
services/external/app/models/poi_context.py
vendored
Normal file
123
services/external/app/models/poi_context.py
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
POI Context Model
|
||||
|
||||
Stores Point of Interest detection results and ML features for bakery locations.
|
||||
Used for location-based demand forecasting with contextual features.
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, Float, Index, Integer
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import uuid
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class TenantPOIContext(Base):
|
||||
"""
|
||||
POI (Point of Interest) context for bakery location.
|
||||
|
||||
Stores detected POIs around bakery and calculated ML features
|
||||
for demand forecasting with location-specific context.
|
||||
"""
|
||||
|
||||
__tablename__ = "tenant_poi_contexts"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, unique=True, index=True)
|
||||
|
||||
# Location (denormalized for quick reference and spatial queries)
|
||||
latitude = Column(Float, nullable=False)
|
||||
longitude = Column(Float, nullable=False)
|
||||
|
||||
# POI Detection Results (full raw data)
|
||||
# Structure: {
|
||||
# "schools": {
|
||||
# "pois": [{"osm_id": "...", "name": "...", "lat": ..., "lon": ...}],
|
||||
# "features": {"proximity_score": 3.45, "count_0_100m": 2, ...},
|
||||
# "count": 5
|
||||
# },
|
||||
# "offices": {...},
|
||||
# ...
|
||||
# }
|
||||
poi_detection_results = Column(JSONB, nullable=False, default=dict)
|
||||
|
||||
# ML Features (flat structure for easy model ingestion)
|
||||
# Structure: {
|
||||
# "poi_schools_proximity_score": 3.45,
|
||||
# "poi_schools_weighted_proximity_score": 5.18,
|
||||
# "poi_schools_count_0_100m": 2,
|
||||
# "poi_offices_proximity_score": 1.23,
|
||||
# ...
|
||||
# }
|
||||
ml_features = Column(JSONB, nullable=False, default=dict)
|
||||
|
||||
# Summary Statistics
|
||||
total_pois_detected = Column(Integer, default=0)
|
||||
high_impact_categories = Column(JSONB, default=list) # Categories with significant POI presence
|
||||
relevant_categories = Column(JSONB, default=list) # Categories that passed relevance thresholds
|
||||
|
||||
# Detection Metadata
|
||||
detection_timestamp = Column(DateTime(timezone=True), nullable=False)
|
||||
detection_source = Column(String(50), default="overpass_api")
|
||||
detection_status = Column(String(20), default="completed") # completed, failed, partial
|
||||
detection_error = Column(String(500), nullable=True) # Error message if detection failed
|
||||
|
||||
# Data Freshness Strategy
|
||||
# POIs don't change frequently, refresh every 6 months
|
||||
next_refresh_date = Column(DateTime(timezone=True), nullable=True)
|
||||
refresh_interval_days = Column(Integer, default=180) # 6 months default
|
||||
last_refreshed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_tenant_poi_location', 'latitude', 'longitude'),
|
||||
Index('idx_tenant_poi_refresh', 'next_refresh_date'),
|
||||
Index('idx_tenant_poi_status', 'detection_status'),
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary for API responses"""
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"tenant_id": str(self.tenant_id),
|
||||
"location": {
|
||||
"latitude": self.latitude,
|
||||
"longitude": self.longitude
|
||||
},
|
||||
"poi_detection_results": self.poi_detection_results,
|
||||
"ml_features": self.ml_features,
|
||||
"total_pois_detected": self.total_pois_detected,
|
||||
"high_impact_categories": self.high_impact_categories,
|
||||
"relevant_categories": self.relevant_categories,
|
||||
"detection_timestamp": self.detection_timestamp.isoformat() if self.detection_timestamp else None,
|
||||
"detection_source": self.detection_source,
|
||||
"detection_status": self.detection_status,
|
||||
"detection_error": self.detection_error,
|
||||
"next_refresh_date": self.next_refresh_date.isoformat() if self.next_refresh_date else None,
|
||||
"last_refreshed_at": self.last_refreshed_at.isoformat() if self.last_refreshed_at else None,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
def is_stale(self) -> bool:
|
||||
"""Check if POI data needs refresh"""
|
||||
if not self.next_refresh_date:
|
||||
return True
|
||||
return datetime.now(timezone.utc) > self.next_refresh_date
|
||||
|
||||
def calculate_next_refresh(self) -> datetime:
|
||||
"""Calculate next refresh date based on interval"""
|
||||
return datetime.now(timezone.utc) + timedelta(days=self.refresh_interval_days)
|
||||
|
||||
def mark_refreshed(self):
|
||||
"""Mark as refreshed and calculate next refresh date"""
|
||||
self.last_refreshed_at = datetime.now(timezone.utc)
|
||||
self.next_refresh_date = self.calculate_next_refresh()
|
||||
154
services/external/app/models/poi_refresh_job.py
vendored
Normal file
154
services/external/app/models/poi_refresh_job.py
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
POI Refresh Job Model
|
||||
|
||||
Tracks background jobs for periodic POI context refresh.
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, Integer, Boolean, Text, Float
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from datetime import datetime, timezone
|
||||
import uuid
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class POIRefreshJob(Base):
|
||||
"""
|
||||
POI Refresh Background Job Model
|
||||
|
||||
Tracks periodic POI context refresh jobs for all tenants.
|
||||
Jobs run on a configurable schedule (default: 180 days).
|
||||
"""
|
||||
|
||||
__tablename__ = "poi_refresh_jobs"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Job scheduling
|
||||
scheduled_at = Column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="When this job was scheduled"
|
||||
)
|
||||
started_at = Column(
|
||||
DateTime(timezone=True),
|
||||
nullable=True,
|
||||
comment="When job execution started"
|
||||
)
|
||||
completed_at = Column(
|
||||
DateTime(timezone=True),
|
||||
nullable=True,
|
||||
comment="When job execution completed"
|
||||
)
|
||||
|
||||
# Job status
|
||||
status = Column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
default="pending",
|
||||
index=True,
|
||||
comment="Job status: pending, running, completed, failed"
|
||||
)
|
||||
|
||||
# Job execution details
|
||||
attempt_count = Column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
default=0,
|
||||
comment="Number of execution attempts"
|
||||
)
|
||||
max_attempts = Column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
default=3,
|
||||
comment="Maximum number of retry attempts"
|
||||
)
|
||||
|
||||
# Location data (cached for job execution)
|
||||
latitude = Column(
|
||||
Float,
|
||||
nullable=False,
|
||||
comment="Bakery latitude for POI detection"
|
||||
)
|
||||
longitude = Column(
|
||||
Float,
|
||||
nullable=False,
|
||||
comment="Bakery longitude for POI detection"
|
||||
)
|
||||
|
||||
# Results
|
||||
pois_detected = Column(
|
||||
Integer,
|
||||
nullable=True,
|
||||
comment="Number of POIs detected in this refresh"
|
||||
)
|
||||
changes_detected = Column(
|
||||
Boolean,
|
||||
default=False,
|
||||
comment="Whether significant changes were detected"
|
||||
)
|
||||
change_summary = Column(
|
||||
JSONB,
|
||||
nullable=True,
|
||||
comment="Summary of changes detected"
|
||||
)
|
||||
|
||||
# Error handling
|
||||
error_message = Column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="Error message if job failed"
|
||||
)
|
||||
error_details = Column(
|
||||
JSONB,
|
||||
nullable=True,
|
||||
comment="Detailed error information"
|
||||
)
|
||||
|
||||
# Next execution
|
||||
next_scheduled_at = Column(
|
||||
DateTime(timezone=True),
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment="When next refresh should be scheduled"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<POIRefreshJob(id={self.id}, tenant_id={self.tenant_id}, "
|
||||
f"status={self.status}, scheduled_at={self.scheduled_at})>"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_overdue(self) -> bool:
|
||||
"""Check if job is overdue for execution"""
|
||||
if self.status in ("completed", "running"):
|
||||
return False
|
||||
return datetime.now(timezone.utc) > self.scheduled_at
|
||||
|
||||
@property
|
||||
def can_retry(self) -> bool:
|
||||
"""Check if job can be retried"""
|
||||
return self.attempt_count < self.max_attempts
|
||||
|
||||
@property
|
||||
def duration_seconds(self) -> float | None:
|
||||
"""Calculate job duration in seconds"""
|
||||
if self.started_at and self.completed_at:
|
||||
return (self.completed_at - self.started_at).total_seconds()
|
||||
return None
|
||||
294
services/external/app/models/traffic.py
vendored
Normal file
294
services/external/app/models/traffic.py
vendored
Normal file
@@ -0,0 +1,294 @@
|
||||
# ================================================================
|
||||
# services/data/app/models/traffic.py - Enhanced for Multiple Cities
|
||||
# ================================================================
|
||||
"""
|
||||
Flexible traffic data models supporting multiple cities and extensible schemas
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, Float, Integer, Text, Index, Boolean, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from shared.database.base import Base
|
||||
|
||||
|
||||
class TrafficData(Base):
|
||||
"""
|
||||
Flexible traffic data model supporting multiple cities
|
||||
Designed to accommodate varying data structures across different cities
|
||||
"""
|
||||
__tablename__ = "traffic_data"
|
||||
|
||||
# Primary identification
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
# Location and temporal data
|
||||
location_id = Column(String(100), nullable=False, index=True) # "lat,lon" or city-specific ID
|
||||
city = Column(String(50), nullable=False, index=True) # madrid, barcelona, valencia, etc.
|
||||
date = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
|
||||
# Core standardized traffic metrics (common across all cities)
|
||||
traffic_volume = Column(Integer, nullable=True) # Vehicle count or intensity
|
||||
congestion_level = Column(String(20), nullable=True) # low, medium, high, blocked
|
||||
average_speed = Column(Float, nullable=True) # Average speed in km/h
|
||||
|
||||
# Enhanced metrics (may not be available for all cities)
|
||||
occupation_percentage = Column(Float, nullable=True) # Road occupation %
|
||||
load_percentage = Column(Float, nullable=True) # Traffic load %
|
||||
pedestrian_count = Column(Integer, nullable=True) # Estimated pedestrian count
|
||||
|
||||
# Measurement point information
|
||||
measurement_point_id = Column(String(100), nullable=True, index=True)
|
||||
measurement_point_name = Column(String(500), nullable=True)
|
||||
measurement_point_type = Column(String(50), nullable=True) # URB, M30, A, etc.
|
||||
|
||||
# Geographic data
|
||||
latitude = Column(Float, nullable=True)
|
||||
longitude = Column(Float, nullable=True)
|
||||
district = Column(String(100), nullable=True) # City district/area
|
||||
zone = Column(String(100), nullable=True) # Traffic zone or sector
|
||||
|
||||
# Data source and quality
|
||||
source = Column(String(50), nullable=False, default="unknown") # madrid_opendata, synthetic, etc.
|
||||
data_quality_score = Column(Float, nullable=True) # Quality score 0-100
|
||||
is_synthetic = Column(Boolean, default=False)
|
||||
has_pedestrian_inference = Column(Boolean, default=False)
|
||||
|
||||
# City-specific data (flexible JSON storage)
|
||||
city_specific_data = Column(JSON, nullable=True) # Store city-specific fields
|
||||
|
||||
# Raw data backup
|
||||
raw_data = Column(Text, nullable=True) # Original data for debugging
|
||||
|
||||
# Audit fields
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=True, index=True) # For multi-tenancy
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# Performance-optimized indexes
|
||||
__table_args__ = (
|
||||
# Core query patterns
|
||||
Index('idx_traffic_location_date', 'location_id', 'date'),
|
||||
Index('idx_traffic_city_date', 'city', 'date'),
|
||||
Index('idx_traffic_tenant_date', 'tenant_id', 'date'),
|
||||
|
||||
# Advanced query patterns
|
||||
Index('idx_traffic_city_location', 'city', 'location_id'),
|
||||
Index('idx_traffic_measurement_point', 'city', 'measurement_point_id'),
|
||||
Index('idx_traffic_district_date', 'city', 'district', 'date'),
|
||||
|
||||
# Training data queries
|
||||
Index('idx_traffic_training', 'tenant_id', 'city', 'date', 'is_synthetic'),
|
||||
Index('idx_traffic_quality', 'city', 'data_quality_score', 'date'),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert model to dictionary for API responses"""
|
||||
result = {
|
||||
'id': str(self.id),
|
||||
'location_id': self.location_id,
|
||||
'city': self.city,
|
||||
'date': self.date.isoformat() if self.date else None,
|
||||
'traffic_volume': self.traffic_volume,
|
||||
'congestion_level': self.congestion_level,
|
||||
'average_speed': self.average_speed,
|
||||
'occupation_percentage': self.occupation_percentage,
|
||||
'load_percentage': self.load_percentage,
|
||||
'pedestrian_count': self.pedestrian_count,
|
||||
'measurement_point_id': self.measurement_point_id,
|
||||
'measurement_point_name': self.measurement_point_name,
|
||||
'measurement_point_type': self.measurement_point_type,
|
||||
'latitude': self.latitude,
|
||||
'longitude': self.longitude,
|
||||
'district': self.district,
|
||||
'zone': self.zone,
|
||||
'source': self.source,
|
||||
'data_quality_score': self.data_quality_score,
|
||||
'is_synthetic': self.is_synthetic,
|
||||
'has_pedestrian_inference': self.has_pedestrian_inference,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
# Add city-specific data if present
|
||||
if self.city_specific_data:
|
||||
result['city_specific_data'] = self.city_specific_data
|
||||
|
||||
return result
|
||||
|
||||
def get_city_specific_field(self, field_name: str, default: Any = None) -> Any:
|
||||
"""Safely get city-specific field value"""
|
||||
if self.city_specific_data and isinstance(self.city_specific_data, dict):
|
||||
return self.city_specific_data.get(field_name, default)
|
||||
return default
|
||||
|
||||
def set_city_specific_field(self, field_name: str, value: Any) -> None:
|
||||
"""Set city-specific field value"""
|
||||
if not self.city_specific_data:
|
||||
self.city_specific_data = {}
|
||||
if not isinstance(self.city_specific_data, dict):
|
||||
self.city_specific_data = {}
|
||||
self.city_specific_data[field_name] = value
|
||||
|
||||
|
||||
class TrafficMeasurementPoint(Base):
|
||||
"""
|
||||
Registry of traffic measurement points across all cities
|
||||
Supports different city-specific measurement point schemas
|
||||
"""
|
||||
__tablename__ = "traffic_measurement_points"
|
||||
|
||||
# Primary identification
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
# Location and identification
|
||||
city = Column(String(50), nullable=False, index=True)
|
||||
measurement_point_id = Column(String(100), nullable=False, index=True) # City-specific ID
|
||||
name = Column(String(500), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# Geographic information
|
||||
latitude = Column(Float, nullable=False)
|
||||
longitude = Column(Float, nullable=False)
|
||||
district = Column(String(100), nullable=True)
|
||||
zone = Column(String(100), nullable=True)
|
||||
|
||||
# Classification
|
||||
road_type = Column(String(50), nullable=True) # URB, M30, A, etc.
|
||||
measurement_type = Column(String(50), nullable=True) # intensity, speed, etc.
|
||||
point_category = Column(String(50), nullable=True) # urban, highway, ring_road
|
||||
|
||||
# Status and metadata
|
||||
is_active = Column(Boolean, default=True)
|
||||
installation_date = Column(DateTime(timezone=True), nullable=True)
|
||||
last_data_received = Column(DateTime(timezone=True), nullable=True)
|
||||
data_quality_rating = Column(Float, nullable=True) # Average quality 0-100
|
||||
|
||||
# City-specific point data
|
||||
city_specific_metadata = Column(JSON, nullable=True)
|
||||
|
||||
# Audit fields
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc))
|
||||
|
||||
__table_args__ = (
|
||||
# Ensure unique measurement points per city
|
||||
Index('idx_unique_city_point', 'city', 'measurement_point_id', unique=True),
|
||||
|
||||
# Geographic queries
|
||||
Index('idx_points_city_location', 'city', 'latitude', 'longitude'),
|
||||
Index('idx_points_district', 'city', 'district'),
|
||||
Index('idx_points_road_type', 'city', 'road_type'),
|
||||
|
||||
# Status queries
|
||||
Index('idx_points_active', 'city', 'is_active', 'last_data_received'),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert measurement point to dictionary"""
|
||||
return {
|
||||
'id': str(self.id),
|
||||
'city': self.city,
|
||||
'measurement_point_id': self.measurement_point_id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'latitude': self.latitude,
|
||||
'longitude': self.longitude,
|
||||
'district': self.district,
|
||||
'zone': self.zone,
|
||||
'road_type': self.road_type,
|
||||
'measurement_type': self.measurement_type,
|
||||
'point_category': self.point_category,
|
||||
'is_active': self.is_active,
|
||||
'installation_date': self.installation_date.isoformat() if self.installation_date else None,
|
||||
'last_data_received': self.last_data_received.isoformat() if self.last_data_received else None,
|
||||
'data_quality_rating': self.data_quality_rating,
|
||||
'city_specific_metadata': self.city_specific_metadata,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
|
||||
class TrafficDataBackgroundJob(Base):
|
||||
"""
|
||||
Track background data collection jobs for multiple cities
|
||||
Supports scheduling and monitoring of data fetching processes
|
||||
"""
|
||||
__tablename__ = "traffic_background_jobs"
|
||||
|
||||
# Primary identification
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
# Job configuration
|
||||
job_type = Column(String(50), nullable=False) # historical_fetch, cleanup, etc.
|
||||
city = Column(String(50), nullable=False, index=True)
|
||||
location_pattern = Column(String(200), nullable=True) # Location pattern or specific coords
|
||||
|
||||
# Scheduling
|
||||
scheduled_at = Column(DateTime(timezone=True), nullable=False)
|
||||
started_at = Column(DateTime(timezone=True), nullable=True)
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Status tracking
|
||||
status = Column(String(20), nullable=False, default='pending') # pending, running, completed, failed
|
||||
progress_percentage = Column(Float, default=0.0)
|
||||
records_processed = Column(Integer, default=0)
|
||||
records_stored = Column(Integer, default=0)
|
||||
|
||||
# Date range for data jobs
|
||||
data_start_date = Column(DateTime(timezone=True), nullable=True)
|
||||
data_end_date = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Results and error handling
|
||||
success_count = Column(Integer, default=0)
|
||||
error_count = Column(Integer, default=0)
|
||||
error_message = Column(Text, nullable=True)
|
||||
job_metadata = Column(JSON, nullable=True) # Additional job-specific data
|
||||
|
||||
# Tenant association
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=True, index=True)
|
||||
|
||||
# Audit fields
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc))
|
||||
|
||||
__table_args__ = (
|
||||
# Job monitoring
|
||||
Index('idx_jobs_city_status', 'city', 'status', 'scheduled_at'),
|
||||
Index('idx_jobs_tenant_status', 'tenant_id', 'status', 'scheduled_at'),
|
||||
Index('idx_jobs_type_city', 'job_type', 'city', 'scheduled_at'),
|
||||
|
||||
# Cleanup queries
|
||||
Index('idx_jobs_completed', 'status', 'completed_at'),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert job to dictionary"""
|
||||
return {
|
||||
'id': str(self.id),
|
||||
'job_type': self.job_type,
|
||||
'city': self.city,
|
||||
'location_pattern': self.location_pattern,
|
||||
'scheduled_at': self.scheduled_at.isoformat() if self.scheduled_at else None,
|
||||
'started_at': self.started_at.isoformat() if self.started_at else None,
|
||||
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
|
||||
'status': self.status,
|
||||
'progress_percentage': self.progress_percentage,
|
||||
'records_processed': self.records_processed,
|
||||
'records_stored': self.records_stored,
|
||||
'data_start_date': self.data_start_date.isoformat() if self.data_start_date else None,
|
||||
'data_end_date': self.data_end_date.isoformat() if self.data_end_date else None,
|
||||
'success_count': self.success_count,
|
||||
'error_count': self.error_count,
|
||||
'error_message': self.error_message,
|
||||
'job_metadata': self.job_metadata,
|
||||
'tenant_id': str(self.tenant_id) if self.tenant_id else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
74
services/external/app/models/weather.py
vendored
Normal file
74
services/external/app/models/weather.py
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# ================================================================
|
||||
# services/data/app/models/weather.py
|
||||
# ================================================================
|
||||
"""Weather data models"""
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, Float, Integer, Text, Index, Boolean
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSON
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from shared.database.base import Base
|
||||
|
||||
class WeatherData(Base):
|
||||
__tablename__ = "weather_data"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
location_id = Column(String(100), nullable=False, index=True)
|
||||
city = Column(String(50), nullable=False)
|
||||
station_name = Column(String(200), nullable=True)
|
||||
latitude = Column(Float, nullable=True)
|
||||
longitude = Column(Float, nullable=True)
|
||||
date = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
forecast_date = Column(DateTime(timezone=True), nullable=True)
|
||||
temperature = Column(Float, nullable=True) # Celsius
|
||||
temperature_min = Column(Float, nullable=True)
|
||||
temperature_max = Column(Float, nullable=True)
|
||||
feels_like = Column(Float, nullable=True)
|
||||
precipitation = Column(Float, nullable=True) # mm
|
||||
precipitation_probability = Column(Float, nullable=True)
|
||||
humidity = Column(Float, nullable=True) # percentage
|
||||
wind_speed = Column(Float, nullable=True) # km/h
|
||||
wind_direction = Column(Float, nullable=True)
|
||||
wind_gust = Column(Float, nullable=True)
|
||||
pressure = Column(Float, nullable=True) # hPa
|
||||
visibility = Column(Float, nullable=True)
|
||||
uv_index = Column(Float, nullable=True)
|
||||
cloud_cover = Column(Float, nullable=True)
|
||||
condition = Column(String(100), nullable=True)
|
||||
description = Column(String(200), nullable=True)
|
||||
weather_code = Column(String(20), nullable=True)
|
||||
source = Column(String(50), nullable=False, default="aemet")
|
||||
data_type = Column(String(20), nullable=False)
|
||||
is_forecast = Column(Boolean, nullable=True)
|
||||
data_quality_score = Column(Float, nullable=True)
|
||||
raw_data = Column(JSON, nullable=True)
|
||||
processed_data = Column(JSON, nullable=True)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=True, index=True)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_weather_location_date', 'location_id', 'date'),
|
||||
)
|
||||
|
||||
class WeatherForecast(Base):
|
||||
__tablename__ = "weather_forecasts"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
location_id = Column(String(100), nullable=False, index=True)
|
||||
forecast_date = Column(DateTime(timezone=True), nullable=False)
|
||||
generated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
temperature = Column(Float, nullable=True)
|
||||
precipitation = Column(Float, nullable=True)
|
||||
humidity = Column(Float, nullable=True)
|
||||
wind_speed = Column(Float, nullable=True)
|
||||
description = Column(String(200), nullable=True)
|
||||
source = Column(String(50), nullable=False, default="aemet")
|
||||
raw_data = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_forecast_location_date', 'location_id', 'forecast_date'),
|
||||
)
|
||||
Reference in New Issue
Block a user