""" Pydantic models for Odoo Directory data structures. """ from datetime import datetime from enum import Enum from typing import Optional, List, Dict, Any from uuid import UUID from pydantic import BaseModel, Field, HttpUrl class TagCategory(str, Enum): """Tag category types.""" SERVICE = "service" INDUSTRY = "industry" MODULE = "module" PARTNER_LEVEL = "partner_level" class StepStatus(str, Enum): """Execution step status.""" PENDING = "pending" RUNNING = "running" COMPLETED = "completed" FAILED = "failed" SKIPPED = "skipped" class Region(BaseModel): """Geographic region model.""" id: Optional[UUID] = None name: str slug: str created_at: Optional[datetime] = None class Config: from_attributes = True class Country(BaseModel): """Country model.""" id: Optional[UUID] = None region_id: Optional[UUID] = None name: str name_local: Optional[str] = None code: str # ISO 3166-1 alpha-2 slug: str language: str # Primary language code created_at: Optional[datetime] = None class Config: from_attributes = True class City(BaseModel): """City model.""" id: Optional[UUID] = None country_id: Optional[UUID] = None name: str name_local: Optional[str] = None slug: str state: Optional[str] = None population: Optional[int] = None latitude: Optional[float] = None longitude: Optional[float] = None created_at: Optional[datetime] = None class Config: from_attributes = True class Tag(BaseModel): """Tag model for services, industries, modules, partner levels.""" id: Optional[UUID] = None category: TagCategory name: str slug: str description: Optional[str] = None created_at: Optional[datetime] = None class Config: from_attributes = True class Company(BaseModel): """Odoo partner company model.""" id: Optional[UUID] = None city_id: Optional[UUID] = None # Basic info (from Google Maps) name: str slug: str google_place_id: Optional[str] = None address: Optional[str] = None phone: Optional[str] = None website: Optional[str] = None email: Optional[str] = None # Google Maps data rating: Optional[float] = None review_count: Optional[int] = None latitude: Optional[float] = None longitude: Optional[float] = None # Enriched data (from Firecrawl) website_markdown: Optional[str] = None logo_url: Optional[str] = None # Generated content (from Haiku) description: Optional[str] = None content_html: Optional[str] = None meta_title: Optional[str] = None meta_description: Optional[str] = None # Status tracking status: str = "scraped" # scraped, enriched, content_generated, exported last_scraped_at: Optional[datetime] = None last_enriched_at: Optional[datetime] = None last_content_at: Optional[datetime] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None class Config: from_attributes = True class CompanyTag(BaseModel): """Junction table for company-tag relationship.""" company_id: UUID tag_id: UUID confidence: Optional[float] = None # AI confidence score created_at: Optional[datetime] = None class Config: from_attributes = True class CityTag(BaseModel): """Aggregated tag counts per city.""" city_id: UUID tag_id: UUID company_count: int = 0 class Config: from_attributes = True class CountryTag(BaseModel): """Aggregated tag counts per country.""" country_id: UUID tag_id: UUID company_count: int = 0 city_count: int = 0 class Config: from_attributes = True class PlanStep(BaseModel): """Individual step in an execution plan.""" id: Optional[UUID] = None plan_id: Optional[UUID] = None step_name: str # scrape, enrich, content, export city_id: Optional[UUID] = None status: StepStatus = StepStatus.PENDING started_at: Optional[datetime] = None completed_at: Optional[datetime] = None error_message: Optional[str] = None items_processed: int = 0 items_total: int = 0 class Config: from_attributes = True class ExecutionPlan(BaseModel): """Execution plan for processing a country/region.""" id: Optional[UUID] = None name: str country_code: str cities: List[str] # List of city slugs status: StepStatus = StepStatus.PENDING created_at: Optional[datetime] = None started_at: Optional[datetime] = None completed_at: Optional[datetime] = None # Related steps steps: List[PlanStep] = [] class Config: from_attributes = True # API Response models class ApifyGoogleMapsResult(BaseModel): """Result from Apify Google Maps scraper.""" title: str address: Optional[str] = None phone: Optional[str] = None website: Optional[str] = None rating: Optional[float] = None reviewsCount: Optional[int] = None placeId: Optional[str] = None url: Optional[str] = None location: Optional[Dict[str, float]] = None class Config: extra = "ignore" class FirecrawlResult(BaseModel): """Result from Firecrawl website scraper.""" markdown: Optional[str] = None metadata: Optional[Dict[str, Any]] = None images: Optional[List[str]] = None class Config: extra = "ignore" class HaikuTagsResponse(BaseModel): """Response from Haiku tag extraction.""" services: List[str] = [] industries: List[str] = [] modules: List[str] = [] partner_level: Optional[str] = None confidence: float = 0.0 class Config: extra = "ignore" class HaikuContentResponse(BaseModel): """Response from Haiku content generation.""" description: str content_html: str meta_title: str meta_description: str class Config: extra = "ignore"