from datetime import datetime from pathlib import Path from typing import Self import yaml from pydantic import BaseModel, Field class _YamlModel(BaseModel): def to_yaml(self, path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) data = self.model_dump(by_alias=True, mode="json", exclude_defaults=False) path.write_text( yaml.safe_dump(data, sort_keys=False, allow_unicode=True, width=120) ) @classmethod def from_yaml(cls, path: Path) -> Self: raw = yaml.safe_load(path.read_text()) return cls.model_validate(raw) class ColumnPhysical(BaseModel): type: str nullable: bool = True pk: bool = False default: str | None = None comment: str = "" examples: list[str] = [] is_enum: bool = False eligible: bool = True eligibility_reason: str = "" class ForeignKey(BaseModel): columns: list[str] ref_table: str ref_columns: list[str] name: str = "" class Index(BaseModel): name: str columns: list[str] unique: bool = False primary: bool = False type: str = "btree" class TablePhysical(BaseModel): comment: str = "" row_count: int = 0 # stima da pg_class.reltuples columns: dict[str, ColumnPhysical] = {} foreign_keys: list[ForeignKey] = [] indexes: list[Index] = [] class PhysicalSchema(_YamlModel): database: str db_schema: str = Field(alias="schema") introspected_at: datetime tables: dict[str, TablePhysical] = {} model_config = {"populate_by_name": True} class ColumnAnnotation(BaseModel): description: str = "" synonyms: list[str] = [] concepts: list[str] = [] evidence: list[str] = [] notes: str = "" eligible: bool | None = None class TableAnnotation(BaseModel): description: str = "" concepts: list[str] = [] notes: str = "" columns: dict[str, ColumnAnnotation] = {} # FK "logiche" curate a mano: il DWH non dichiara vincoli, quindi i join # noti (es. data_time_key -> dim_time.day_key) vivono qui e vengono fusi # con le FK fisiche in tutte le viste renderizzate. foreign_keys: list[ForeignKey] = [] class Annotations(_YamlModel): tables: dict[str, TableAnnotation] = {} @classmethod def from_yaml(cls, path: Path) -> "Annotations": if not path.exists(): return cls() return super().from_yaml(path)