""" Observation Models - Layer 6 of IOM Pydantic models for observations, findings, and media """ from datetime import datetime from typing import List, Optional, Dict, Any from enum import Enum from pydantic import BaseModel, Field class MediaType(str, Enum): """Types of media attachments""" IMAGE = "image" VIDEO = "video" DEPTH_MAP = "depth_map" THERMAL = "thermal" LIDAR = "lidar" class FindingType(str, Enum): """Types of findings/observations""" DIRT_ACCUMULATION = "dirt_accumulation" COLOR_CHANGE = "color_change" SURFACE_DAMAGE = "surface_damage" GRAFFITI = "graffiti" CRACK = "crack" DEFORMATION = "deformation" MATERIAL_LOSS = "material_loss" MISSING_PART = "missing_part" BROKEN_PART = "broken_part" LOOSE_PART = "loose_part" PHYSICAL_BLOCK = "physical_block" VEGETATION = "vegetation" WATER_DAMAGE = "water_damage" ICE_DAMAGE = "ice_damage" CORROSION = "corrosion" OTHER = "other" class GeoTag(BaseModel): """Geographic location tag""" lat: float = Field(..., ge=-90, le=90, description="Latitude") lng: float = Field(..., ge=-180, le=180, description="Longitude") elevation: Optional[float] = Field(None, description="Elevation in meters") accuracy: Optional[float] = Field(None, description="GPS accuracy in meters") class Config: json_schema_extra = { "example": { "lat": 59.3293, "lng": 18.0686, "elevation": 12.5, "accuracy": 3.2 } } class Media(BaseModel): """Media attachment (image, video, etc.)""" type: MediaType url: str = Field(..., description="URL to media file") timestamp: datetime = Field(default_factory=datetime.utcnow) geotag: Optional[GeoTag] = None class Config: json_schema_extra = { "example": { "type": "image", "url": "https://storage.quixzoom.com/obs/img_2847.jpg", "timestamp": "2026-06-26T09:15:03Z", "geotag": { "lat": 59.3293, "lng": 18.0686, "accuracy": 2.1 } } } class Finding(BaseModel): """Individual finding within an observation""" type: FindingType code: str = Field(..., pattern=r"^[0-9]{4}$", description="Defect code from Layer 7") description: str = Field(..., min_length=1, max_length=500) measurement: Optional[str] = Field(None, description="Measurement (e.g., '3 mm', '15% coverage')") location: Optional[str] = Field(None, description="Location on object (e.g., 'northeast_corner')") confidence: float = Field(..., ge=0.0, le=1.0, description="AI confidence score") class Config: json_schema_extra = { "example": { "type": "dirt_accumulation", "code": "2100", "description": "Smuts på fönster", "measurement": "30% coverage", "confidence": 0.94 } } class AIAnalysis(BaseModel): """AI-generated analysis of observation""" model: str = Field(..., description="AI model version") overall_condition: int = Field(..., ge=1, le=5, description="Overall condition 1-5") recommended_action: Optional[str] = Field(None, description="Recommended action") next_observation_due: Optional[datetime] = None class Config: json_schema_extra = { "example": { "model": "infrastructure-v3.2", "overall_condition": 3, "recommended_action": "schedule_cleaning", "next_observation_due": "2026-09-26" } } class Weather(BaseModel): """Weather conditions during observation""" temperature: Optional[float] = Field(None, description="Temperature in Celsius") conditions: Optional[str] = Field(None, description="Weather conditions") wind_speed: Optional[float] = Field(None, description="Wind speed in m/s") precipitation: Optional[str] = Field(None, description="Precipitation type") class Config: json_schema_extra = { "example": { "temperature": 22, "conditions": "sunny", "wind_speed": 3.5 } } class Observation(BaseModel): """ Main observation model - Layer 6 of IOM Core principle: The object never changes. Only observations change. """ id: Optional[str] = Field(None, pattern=r"^OBS-[0-9]{4}-[0-9]{7}$") timestamp: datetime = Field(default_factory=datetime.utcnow) object_goid: str = Field(..., pattern=r"^[A-Z]{3}(-[A-Z]{3})+-[0-9]+$", description="GOID of observed object") observer: str = Field(..., description="Observer ID (zoomer:id or sensor:id)") # Findings findings: List[Finding] = Field(default_factory=list) # Media media: List[Media] = Field(default_factory=list) # Context weather: Optional[Weather] = None # AI Analysis ai_analysis: Optional[AIAnalysis] = None # Metadata created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: Optional[datetime] = None class Config: json_schema_extra = { "example": { "id": "OBS-2026-0012847", "timestamp": "2026-06-26T09:15:00Z", "object_goid": "BYG-FAC-WIN-GLA-2847", "observer": "zoomer:anna_k", "findings": [ { "type": "dirt_accumulation", "code": "2100", "description": "Smuts på fönster", "measurement": "30% coverage", "confidence": 0.94 } ], "media": [ { "type": "image", "url": "https://storage.quixzoom.com/obs/img_2847.jpg", "timestamp": "2026-06-26T09:15:03Z", "geotag": { "lat": 59.3293, "lng": 18.0686, "accuracy": 2.1 } } ], "ai_analysis": { "model": "infrastructure-v3.2", "overall_condition": 3, "recommended_action": "schedule_cleaning" } } } class ObservationSummary(BaseModel): """Summary of observations for an object""" object_goid: str observation_count: int latest_condition: Optional[int] = None latest_observation_date: Optional[datetime] = None finding_types: List[str] = Field(default_factory=list) risk_trend: Optional[str] = None # "improving", "stable", "degrading" class Config: json_schema_extra = { "example": { "object_goid": "BYG-FAC-WIN-GLA-2847", "observation_count": 5, "latest_condition": 3, "latest_observation_date": "2026-06-26T09:15:00Z", "finding_types": ["dirt_accumulation", "surface_damage"], "risk_trend": "stable" } } class ObservationTrend(BaseModel): """Trend analysis for an object over time""" object_goid: str period_months: int data_points: List[Dict[str, Any]] class Config: json_schema_extra = { "example": { "object_goid": "BYG-FAC-WIN-GLA-2847", "period_months": 6, "data_points": [ {"month": "2026-01", "condition": 2, "observations": 1}, {"month": "2026-02", "condition": 2, "observations": 0}, {"month": "2026-03", "condition": 3, "observations": 1}, {"month": "2026-04", "condition": 3, "observations": 0}, {"month": "2026-05", "condition": 3, "observations": 1}, {"month": "2026-06", "condition": 3, "observations": 2} ] } } if __name__ == '__main__': # Example usage obs = Observation( id="OBS-2026-0012847", timestamp=datetime(2026, 6, 26, 9, 15, 0), object_goid="BYG-FAC-WIN-GLA-2847", observer="zoomer:anna_k", findings=[ Finding( type=FindingType.DIRT_ACCUMULATION, code="2100", description="Smuts på fönster", measurement="30% coverage", confidence=0.94 ) ], media=[ Media( type=MediaType.IMAGE, url="https://storage.quixzoom.com/obs/img_2847.jpg", geotag=GeoTag(lat=59.3293, lng=18.0686, accuracy=2.1) ) ], ai_analysis=AIAnalysis( model="infrastructure-v3.2", overall_condition=3, recommended_action="schedule_cleaning" ) ) print("Observation created:") print(obs.model_dump_json(indent=2))