File size: 8,644 Bytes
bad6218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
"""Pydantic models for the Indicateurs Territoriaux API responses."""

from typing import Any

from pydantic import BaseModel, Field


class IndicatorMetadata(BaseModel):
    """Metadata for a territorial indicator."""

    id: int = Field(..., description="Unique identifier of the indicator")
    libelle: str = Field(..., description="Human-readable name of the indicator")
    unite: str | None = Field(None, description="Unit of measurement")
    description: str | None = Field(None, description="Detailed description")
    methode_calcul: str | None = Field(None, description="Calculation method")
    fonction_calcul: str | None = Field(None, description="Calculation function")
    date_debut: int | None = Field(None, description="First available year")
    date_fin: int | None = Field(None, description="Last available year")
    annees_disponibles: str | None = Field(
        None, description="Available years (comma-separated)"
    )
    annees_manquantes: str | None = Field(
        None, description="Missing years (comma-separated)"
    )
    mailles_disponibles: str | None = Field(
        None, description="Available geographic levels (e.g., 'region,departement,epci')"
    )
    maille_mini_disponible: str | None = Field(
        None, description="Finest available geographic level"
    )
    couverture_geographique: str | None = Field(
        None, description="Geographic coverage (France métro, DOM, etc.)"
    )
    liste_drom: str | None = Field(None, description="Covered DROM territories")
    completion_region: float | None = Field(
        None, description="Completion percentage at region level"
    )
    completion_departement: float | None = Field(
        None, description="Completion percentage at department level"
    )
    completion_epci: float | None = Field(
        None, description="Completion percentage at EPCI level"
    )
    completion_commune: float | None = Field(
        None, description="Completion percentage at commune level"
    )
    compte_region: int | None = Field(
        None, description="Number of regions with data"
    )
    compte_departement: int | None = Field(
        None, description="Number of departments with data"
    )
    compte_epci: int | None = Field(None, description="Number of EPCIs with data")
    compte_commune: int | None = Field(
        None, description="Number of communes with data"
    )
    thematique_fnv: str | None = Field(
        None, description="France Nation Verte thematic"
    )
    secteur_fnv: str | None = Field(None, description="FNV sector")
    enjeux_fnv: str | None = Field(None, description="FNV challenges")
    levier_fnv: str | None = Field(None, description="FNV lever")
    projets_associes: str | None = Field(None, description="Associated projects")
    valeur_axes: str | None = Field(
        None, description="Breakdown axes (JSON stringified)"
    )

    @classmethod
    def from_api_response(cls, data: dict[str, Any]) -> "IndicatorMetadata":
        """Create an IndicatorMetadata from a Cube.js API response row.
        
        The API returns dimension names prefixed with the cube name.
        This method strips the prefix.
        """
        # Strip the cube name prefix from keys
        prefix = "indicateur_metadata."
        cleaned = {}
        for key, value in data.items():
            clean_key = key.replace(prefix, "")
            cleaned[clean_key] = value
        return cls(**cleaned)

    def has_geographic_level(self, level: str) -> bool:
        """Check if the indicator has data at the specified geographic level."""
        if not self.mailles_disponibles:
            return False
        return level.lower() in self.mailles_disponibles.lower()

    def get_completion_for_level(self, level: str) -> float | None:
        """Get the completion percentage for a geographic level."""
        level_map = {
            "region": self.completion_region,
            "departement": self.completion_departement,
            "epci": self.completion_epci,
            "commune": self.completion_commune,
        }
        return level_map.get(level.lower())


class SourceMetadata(BaseModel):
    """Metadata for a data source associated with an indicator."""

    id_indicateur: int = Field(..., description="ID of the related indicator")
    nom_source: str | None = Field(None, description="Source identifier")
    libelle: str | None = Field(None, description="Human-readable source name")
    description: str | None = Field(None, description="Source description")
    producteur_source: str | None = Field(None, description="Data producer")
    distributeur_source: str | None = Field(None, description="Data distributor")
    license_source: str | None = Field(None, description="Data license")
    lien_page: str | None = Field(None, description="Source URL")
    annees_disponibles_source: str | None = Field(
        None, description="Available years from this source"
    )
    annees_manquantes_source: str | None = Field(
        None, description="Missing years from this source"
    )
    maille_mini_disponible: str | None = Field(
        None, description="Finest geographic level"
    )
    couverture_geographique: str | None = Field(
        None, description="Geographic coverage"
    )
    date_derniere_extraction: str | None = Field(
        None, description="Last extraction date"
    )

    @classmethod
    def from_api_response(cls, data: dict[str, Any]) -> "SourceMetadata":
        """Create a SourceMetadata from a Cube.js API response row."""
        prefix = "indicateur_x_source_metadata."
        cleaned = {}
        for key, value in data.items():
            clean_key = key.replace(prefix, "")
            cleaned[clean_key] = value
        return cls(**cleaned)


class IndicatorListItem(BaseModel):
    """Simplified indicator info for list responses."""

    id: int
    libelle: str
    unite: str | None = None
    mailles_disponibles: str | None = None
    thematique_fnv: str | None = None


class IndicatorDetails(BaseModel):
    """Complete indicator details with sources."""

    metadata: IndicatorMetadata
    sources: list[SourceMetadata] = Field(default_factory=list)


class GeographicDataPoint(BaseModel):
    """A single data point with geographic information."""

    geocode: str = Field(..., description="INSEE code of the territory")
    libelle: str | None = Field(None, description="Name of the territory")
    valeur: float | str | None = Field(None, description="Indicator value")
    annee: str | None = Field(None, description="Year of the data")
    unite: str | None = Field(None, description="Unit of measurement")


class QueryResult(BaseModel):
    """Result of a data query."""

    indicator_id: int
    indicator_name: str
    geographic_level: str
    data: list[GeographicDataPoint]
    total_count: int = 0
    query_info: dict[str, Any] = Field(default_factory=dict)


class SearchResult(BaseModel):
    """Result of an indicator search."""

    indicators: list[IndicatorListItem]
    query: str
    total_count: int


class CubeInfo(BaseModel):
    """Information about a data cube."""
    
    name: str = Field(..., description="Cube name (e.g., 'conso_enaf_com')")
    maille: str = Field(..., description="Geographic level (commune, epci, departement, region)")
    indicator_ids: list[int] = Field(default_factory=list, description="Indicator IDs in this cube")


# Geographic level constants
GEOGRAPHIC_LEVELS = ["region", "departement", "epci", "commune"]

# Maille suffix mapping for cube names
MAILLE_SUFFIX_MAP = {
    "commune": "_com",
    "epci": "_epci",
    "departement": "_dpt",
    "region": "_reg",
}

# Dimension patterns for each geographic level (validated by API tests)
# Format: geocode_{maille} and libelle_{maille}
GEO_DIMENSION_PATTERNS = {
    "region": {
        "geocode": "geocode_region",
        "libelle": "libelle_region",
    },
    "departement": {
        "geocode": "geocode_departement",
        "libelle": "libelle_departement",
    },
    "epci": {
        "geocode": "geocode_epci",
        "libelle": "libelle_epci",
    },
    "commune": {
        "geocode": "geocode_commune",
        "libelle": "libelle_commune",
    },
}

# Region code reference
REGION_CODES = {
    "11": "Île-de-France",
    "24": "Centre-Val de Loire",
    "27": "Bourgogne-Franche-Comté",
    "28": "Normandie",
    "32": "Hauts-de-France",
    "44": "Grand Est",
    "52": "Pays de la Loire",
    "53": "Bretagne",
    "75": "Nouvelle-Aquitaine",
    "76": "Occitanie",
    "84": "Auvergne-Rhône-Alpes",
    "93": "Provence-Alpes-Côte d'Azur",
    "94": "Corse",
}