# meteo/export.py from __future__ import annotations from pathlib import Path from typing import Iterable, Literal import pandas as pd from influxdb_client import InfluxDBClient from .station_config import SensorDefinition, StationConfig def _build_sensor_flux_query( bucket: str, sensor: SensorDefinition, start: str, stop: str | None, ) -> str: """ Construit une requête Flux pour un capteur donné (measurement + entity_id). On ne récupère que le champ `value`, qui contient la valeur physique, et on laisse Influx nous renvoyer tous les points bruts (chez vous : 1 point / minute). """ stop_clause = f", stop: {stop}" if stop is not None else "" return f""" from(bucket: "{bucket}") |> range(start: {start}{stop_clause}) |> filter(fn: (r) => r._measurement == "{sensor.measurement}") |> filter(fn: (r) => r["entity_id"] == "{sensor.entity_id}") |> filter(fn: (r) => r._field == "value") |> keep(columns: ["_time", "_value"]) |> sort(columns: ["_time"]) """ def fetch_sensor_series( client: InfluxDBClient, bucket: str, sensor: SensorDefinition, *, start: str, stop: str | None = None, ) -> pd.Series: """ Récupère la série temporelle d'un capteur unique sous forme de Series pandas. Paramètres ---------- client : Client InfluxDB déjà configuré. bucket : Nom du bucket. sensor : Définition du capteur (measurement + entity_id). start, stop : Intervalle temporel au format Flux (ex: "-7d", "2025-01-01T00:00:00Z"). Retour ------ Series pandas indexée par le temps, nommée `sensor.name`. """ query_api = client.query_api() flux_query = _build_sensor_flux_query(bucket, sensor, start=start, stop=stop) result = query_api.query_data_frame(flux_query) if isinstance(result, list): df = pd.concat(result, ignore_index=True) else: df = result if df.empty: # Série vide : on renvoie une Series vide avec le bon nom. return pd.Series(name=sensor.name, dtype="float64") df = df.rename(columns={"_time": "time", "_value": sensor.name}) df["time"] = pd.to_datetime(df["time"]) df = df.set_index("time").sort_index() # On ne retourne qu'une seule colonne en tant que Series series = df[sensor.name] series.name = sensor.name return series def export_station_data( client: InfluxDBClient, bucket: str, config: StationConfig, *, start: str, stop: str | None = None, output_path: str | Path = "data/weather_raw.csv", file_format: Literal["csv", "parquet"] = "csv", ) -> Path: """ Exporte les données de la station météo vers un fichier (CSV ou Parquet). Pour chaque capteur de `config.sensors`, cette fonction récupère la série temporelle correspondante, puis assemble le tout dans un DataFrame unique. Paramètres ---------- client : Client InfluxDB. bucket : Nom du bucket à interroger (ex: "weather"). config : Configuration de la station (liste de capteurs). start, stop : Intervalle temporel en syntaxe Flux (ex: "-30d", "2024-01-01T00:00:00Z"). Si `stop` est None, Influx utilisera `now()`. output_path : Chemin du fichier de sortie. file_format : "csv" ou "parquet". Retour ------ Path : Chemin absolu du fichier écrit. """ output_path = Path(output_path) output_path.parent.mkdir(parents=True, exist_ok=True) series_list: list[pd.Series] = [] for sensor in config.sensors: series = fetch_sensor_series( client, bucket, sensor, start=start, stop=stop, ) if not series.empty: series_list.append(series) else: # On pourrait logger un warning ici si vous le souhaitez. pass if not series_list: raise RuntimeError( "Aucune donnée récupérée pour la station sur l'intervalle demandé." ) # Assemblage des séries : jointure externe sur l'index temps. df = pd.concat(series_list, axis=1).sort_index() if file_format == "csv": df.to_csv(output_path, index_label="time") elif file_format == "parquet": df.to_parquet(output_path) else: raise ValueError(f"Format de fichier non supporté : {file_format!r}") return output_path.resolve()