Skip to content

Weather Module (kadi.weather)

The kadi.weather module provides a unified interface to acquire, cache, and analyze agricultural weather data. Designed specifically for the Beninese context, it relies on two complementary data sources: Open-Meteo for short-term forecasts and temperature series, and CHIRPS for long-term historical rainfall.

The main entry point is the Weather facade, which initializes each internal component on demand (lazy loading).


Architecture

kadi.weather
├── Weather          - Main facade (session.py)
├── Location         - GPS location and agro-climatic zone (location.py)
├── WeatherLoader    - Raw data acquisition and caching (data.py)
├── Phenology        - Seasons, GDD, onset/cessation (phenology.py)
├── Hydrology        - Water balance, FAO-56 ET0 (hydrology.py)
└── Risk             - SPI, Markov chains, rain probability (risk.py)

The Phenology, Hydrology, and Risk components are only instantiated when a method requiring them is called for the first time.


Data Sources

Source Usage Availability
Open-Meteo Forecasts (up to 16 days) and historical temperatures Free API
CHIRPS Long historical rainfall (since 1981, ~15-day latency) CHC API
SQLite Cache Local storage and offline fallback ~/.kadi/cache.db

The rainfall source for historical queries is configurable via the source parameter of historical(): 'openmeteo', 'chirps', or 'both'. If source is omitted, the default value defined in CONFIG is applied.

In 'both' mode, CHIRPS covers historical periods and Open-Meteo fills recent dates not yet published in CHIRPS. The data_source column in the SQLite cache records the provenance of each observation.


Agro-Climatic Zones

The zone is automatically detected from latitude by Location.

Zone Latitude Rainfall Regime
'Sud' lat < 7.5 Bimodal (two rainy seasons)
'Centre' 7.5 <= lat < 9.0 Bimodal
'Nord' lat >= 9.0 Unimodal (one rainy season)

The zone is stored in attribute location.zone and the regime in location.regime.


Importing

import kadi as kd

# Main Facade
weather = kd.Weather(lat=9.3333, lon=2.6333, name="Parakou")

# Direct access to classes if needed
from kadi.weather import Location, WeatherLoader

Weather (Main Facade)

Initialization

weather = kd.Weather(lat=9.3333, lon=2.6333, name="Parakou")
Parameter Type Required Description
lat float Yes Latitude in decimal degrees
lon float Yes Longitude in decimal degrees
name str No Locality name
cache str No Cache directory path (retained for signature compatibility)

lat and lon are mandatory: a TypeError is raised if either is omitted.

Attributes Instantiated at Initialization:

Attribute Type Description
location Location Associated location instance
cache str or None Cache directory (retained parameter)
loader WeatherLoader Weather data loader instance
phenology Phenology or None Initialized on demand
hydrology Hydrology or None Initialized on demand
risk Risk or None Initialized on demand

forecast(days=None)

Retrieves short-term weather forecast from Open-Meteo (or valid local cache).

forecast_data = weather.forecast(days=7)

print(forecast_data["location"]["name"])
for day in forecast_data["data"]:
    print(day["date"], day["precipitation"])
Parameter Type Default Description
days int or None Value of CONFIG["weather"]["forecast_days_default"] Number of forecast days

The number of days is capped at CONFIG["weather"]["max_forecast_days"].

Returns: dict containing keys: - 'location': dict with 'name', 'lat', 'lon' - 'data': list of daily dictionaries - 'source': data source ('open-meteo' or 'cached') - 'last_updated': ISO response timestamp


historical(metric='all', months=120, source=None, months_back=None)

Returns historical weather time series as a DataFrame.

# 24 months, all metrics
df = weather.historical(months=24)

# Rainfall only
df_rain = weather.historical(metric="precipitation", months=12)

# Force CHIRPS for rainfall
df_chirps = weather.historical(months=36, source="chirps")
Parameter Type Default Description
metric str 'all' Column filter: 'temperature', 'precipitation', 'humidity', or 'all'
months int 120 Number of historical months
source str or None CONFIG default Rainfall source: 'chirps', 'openmeteo', or 'both'
months_back int or None None Legacy alias for months (deprecated)

Returns: pd.DataFrame indexed by DatetimeIndex.


gdd(crop, start, end=None)

Calculates accumulated Growing Degree Days (GDD) from planting date start to end (or today if omitted).

result = weather.gdd(crop="maize", start="2026-05-15")
print(result["gdd_accumulated"])
print(result["phenology_stage"])
Parameter Type Description
crop str Target crop ('maize', 'rice', etc.)
start str or pd.Timestamp Planting date (YYYY-MM-DD format)
end str, pd.Timestamp, or None End date. None defaults to today

Returns: dict (see Phenology documentation).


onset()

Detects rainy season onset date based on zone climate regime.

season_start = weather.onset()
print(season_start["onset_date"])
print(season_start["method"])

Returns: dict (see Phenology).


cessation()

Determines effective rainy season end date.

season_end = weather.cessation()
print(season_end["cessation_date"])

Returns: dict (see Phenology).


drought(method='spi', window=3, window_months=None)

Calculates drought index for the location.

drought_info = weather.drought(method="spi", window=3)
print(drought_info["spi_3month"])
print(drought_info["drought_severity"])
Parameter Type Default Description
method str 'spi' Calculation method: 'spi', 'markov', 'hurst', 'combined'
window int 3 Time window in months
window_months int or None None Legacy alias for window (deprecated)

Returns: dict (see Risk).


rain_prob(days=1, min_mm=1.0, days_ahead=None, min_rainfall_mm=None)

Forecasts rain probability over the next days.

prob = weather.rain_prob(days=3, min_mm=1.0)
print(prob["tomorrow"])          # tomorrow rain probability (float 0.0 to 1.0)
print(prob["recommendation"])    # agronomic recommendation
Parameter Type Default Description
days int 1 Number of future days
min_mm float 1.0 Minimum rainfall threshold in mm
days_ahead int or None None Legacy alias for days (deprecated)
min_rainfall_mm float or None None Legacy alias for min_mm (deprecated)

Returns: dict (see Risk).


water_balance(crop='maize', soil_type='ferrugineux')

Simulates daily water balance using the FAO-56 methodology.

wb = weather.water_balance(crop="maize", soil_type="ferrugineux")
print(wb[["precipitation", "ET0", "deficit_eau", "reserve_utile"]].tail(14))
Parameter Type Default Description
crop str 'maize' Crop type
soil_type str 'ferrugineux' Soil type

Returns: pd.DataFrame (see Hydrology).


et0_hargreaves(tmin, tmax, day_of_year)

Calculates reference evapotranspiration (ET0) using the Hargreaves-Samani method.

et0 = weather.et0_hargreaves(tmin=22.0, tmax=35.0, day_of_year=180)
print(f"ET0 : {et0:.2f} mm/day")
Parameter Type Description
tmin float Minimum temperature in degC
tmax float Maximum temperature in degC
day_of_year int Day of year (1 to 365)

Returns: float (ET0 in mm/day).


Location

Represents a geographic position in Benin with automatic detection of agro-climatic zone and rainfall regime.

Initialization

from kadi.weather import Location

loc = Location(lat=9.3333, lon=2.6333, name="Parakou")
print(loc.zone)    # 'Nord'
print(loc.regime)  # 'unimodal'
Parameter Type Description
lat float Latitude in decimal degrees
lon float Longitude in decimal degrees
name str Optional locality name

Coordinates are validated against the Benin GPS bounding box defined in CONFIG["weather"]["gps_validation_bbox"]. A LocationError is raised if coordinates fall outside this region.

If name is omitted, the attribute defaults to 'Point(lat, lon)'.

Attributes:

Attribute Type Description
lat float Latitude
lon float Longitude
name str Locality name
zone str Detected zone: 'Sud', 'Centre', or 'Nord'
regime str Detected regime: 'bimodal' or 'unimodal'

climate()

Returns default climate parameters for the zone.

params = loc.climate()
# {'Tbase': 10, 'onset_method': 'sivakumar'}

Returns: dict containing keys 'Tbase' and 'onset_method'.

Zone onset_method
'Sud' 'walter_anyadike'
'Centre' 'hybrid'
'Nord' 'sivakumar'

to_dict()

Serializes location for caching.

loc.to_dict()
# {'name': 'Parakou', 'lat': 9.3333, 'lon': 2.6333, 'zone': 'Nord', 'regime': 'unimodal'}

Returns: dict.


WeatherLoader

Manages API data acquisition, normalization, and persistence in the local SQLite cache (~/.kadi/cache.db).

In practice, WeatherLoader is used internally by Weather via weather.loader. It is rarely instantiated directly.

Initialization

from kadi.weather import Location, WeatherLoader

loc = Location(lat=9.3333, lon=2.6333)
loader = WeatherLoader(loc)

cache is optional. If provided, it overrides the cache directory passed by the Weather facade.

Attributes:

Attribute Type Description
location Location Associated location instance
forecast pd.DataFrame or None In-memory forecast DataFrame
historical pd.DataFrame or None In-memory historical DataFrame
source str Source identifier of last loaded dataset

get_forecast(days=7, refresh=False)

Fetches weather forecast checking SQLite cache first.

Parameter Type Default Description
days int 7 Number of forecast days
refresh bool False If True, bypasses cache and forces refresh

Cache TTL for forecasts is configured via CONFIG["weather"]["cache_ttl_forecast_hours"]. On API failure, cache is returned if valid, otherwise an OfflineError is raised.

Returns: pd.DataFrame indexed by date.


get_historical(months=120, refresh=False, source=None)

Fetches historical weather data combining CHIRPS and Open-Meteo according to source.

Parameter Type Default Description
months int 120 Number of historical months
refresh bool False If True, bypasses cache
source str or None CONFIG default 'chirps', 'openmeteo', or 'both'

A 5-day row count tolerance is allowed before marking cache incomplete. Cache TTL is configured via CONFIG["weather"]["cache_ttl_historical_days"].

Returns: pd.DataFrame indexed by date.


Data Normalization Pipeline

WeatherLoader automatically normalizes each DataFrame via _normalize():

  1. Converts 'date' column to DatetimeIndex.
  2. Filters temperature outliers outside [-5, 55] degC.
  3. Enforces non-negative precipitation values.
  4. Computes 'data_quality' column (ratio of populated critical columns).
  5. Applies linear interpolation on short gaps (up to 3 consecutive days).
  6. Fills residual missing precipitation values with 0.0.
  7. Ensures presence of 'temperature_mean' column via _unify_temp(): calculated as (temperature_min + temperature_max) / 2, or aliased from temperature_avg if base columns are missing.

Offline Cache

All downloaded data is stored in the KadiPy SQLite database (~/.kadi/cache.db, table weather_data). If network is offline, the module automatically uses cached data without raising errors (unless cache is empty, in which case OfflineError is raised).

The data_source column in the cache table records the provenance of each observation: 'chirps', 'open-meteo', or a hybrid combination.


Complete Example

import kadi as kd

# Initialization for Cotonou (Sud zone, bimodal regime)
weather = kd.Weather(lat=6.3654, lon=2.4183, name="Cotonou")

# 5-day forecast
forecast_data = weather.forecast(days=5)
for day in forecast_data["data"]:
    print(day["date"], day["precipitation"], day["temperature_min"])

# 12-month history (hybrid source by default)
df = weather.historical(months=12)
print(df.columns.tolist())

# Season onset date
start = weather.onset()
print(start["onset_date"], start["method"])

# GDD calculation for maize
gdd = weather.gdd(crop="maize", start="2026-05-15")
print(gdd["gdd_accumulated"], gdd["phenology_stage"])

# Daily water balance
wb = weather.water_balance(crop="maize", soil_type="ferrugineux")
print(wb[["precipitation", "ET0", "deficit_eau"]].tail(7))

# SPI-3 drought index
drought_info = weather.drought(method="spi", window=3)
print(drought_info["spi_3month"], drought_info["drought_severity"])

# Tomorrow rain probability
prob = weather.rain_prob(days=1, min_mm=1.0)
print(prob["tomorrow"], prob["recommendation"])

Backward Compatibility

Legacy class and parameter names emit a DeprecationWarning and continue to function until KadiPy v2.0.

Classes:

Legacy Name New Name
WeatherSession Weather
WeatherData WeatherLoader
RiskIndicators Risk

Weather.__init__() Parameters:

Legacy Parameter New Parameter
latitude lat
longitude lon
cache_dir cache

Weather Methods:

Legacy Method New Method
growing_degree_days(crop, start_date, end_date) gdd(crop, start, end)
drought_index(method, window_months) drought(method, window)
rain_probability(days_ahead, min_rainfall_mm) rain_prob(days, min_mm)

Weather Attributes:

Legacy Attribute New Attribute
cache_dir cache
weather_data loader
risk_indicators risk

Weather.historical() Parameters:

Legacy Parameter New Parameter
months_back months

Sub-modules

Weather

Façade météorologique : point d'entrée principal pour l'utilisateur.

Orchestre les composants Location, WeatherLoader, Phenology, Hydrology et Risk pour exposer une API simple et unifiée.

Attributs

location (Location): Localisation associée à la façade météo. cache (str): Répertoire de cache (conservé pour la compatibilité). loader (WeatherLoader): Gestionnaire de données météorologiques. phenology (Phenology): Composant phénologique (chargé à la demande). hydrology (Hydrology): Composant hydrologique (chargé à la demande). risk (Risk): Composant des indicateurs de risque (chargé à la demande).

Source code in kadi/weather/session.py
 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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
class Weather:
    """
    Façade météorologique : point d'entrée principal pour l'utilisateur.

    Orchestre les composants Location, WeatherLoader, Phenology, Hydrology et
    Risk pour exposer une API simple et unifiée.

    Attributs:
        location (Location): Localisation associée à la façade météo.
        cache (str): Répertoire de cache (conservé pour la compatibilité).
        loader (WeatherLoader): Gestionnaire de données météorologiques.
        phenology (Phenology): Composant phénologique (chargé à la demande).
        hydrology (Hydrology): Composant hydrologique (chargé à la demande).
        risk (Risk): Composant des indicateurs de risque (chargé à la demande).
    """

    def __init__(
        self,
        lat: float = None,
        lon: float = None,
        name: str = None,
        cache: str = None,
        latitude: float = None,
        longitude: float = None,
        cache_dir: str = None,
    ):
        """
        Initialise une nouvelle façade météo pour une localisation.

        Args:
            lat (float): Latitude en degrés décimaux.
            lon (float): Longitude en degrés décimaux.
            name (str): Nom de la localité (optionnel).
            cache (str): Répertoire de cache (optionnel).
            latitude (float): Ancien nom du paramètre lat.
            longitude (float): Ancien nom du paramètre lon.
            cache_dir (str): Ancien nom du paramètre cache.
        """
        # Prise en charge des anciens noms de paramètres d'initialisation
        if latitude is not None:
            warnings.warn(
                "Le paramètre 'latitude' est obsolète et sera supprimé dans "
                "KadiPy v2.0. Utilisez 'lat' à la place.",
                category=DeprecationWarning,
                stacklevel=2,
            )
            lat = latitude

        if longitude is not None:
            warnings.warn(
                "Le paramètre 'longitude' est obsolète et sera supprimé dans "
                "KadiPy v2.0. Utilisez 'lon' à la place.",
                category=DeprecationWarning,
                stacklevel=2,
            )
            lon = longitude

        if cache_dir is not None:
            warnings.warn(
                "Le paramètre 'cache_dir' est obsolète et sera supprimé dans "
                "KadiPy v2.0. Utilisez 'cache' à la place.",
                category=DeprecationWarning,
                stacklevel=2,
            )
            cache = cache_dir

        if lat is None or lon is None:
            raise TypeError("Les coordonnées 'lat' et 'lon' sont obligatoires.")

        self.location = Location(lat, lon, name)
        # Conservé pour la compatibilité de signature
        self.cache = cache

        # Initialisation du gestionnaire de données
        self.loader = WeatherLoader(self.location, cache)

        # Composants métiers (initialisés à la demande)
        self.phenology: Optional[Phenology] = None
        self.hydrology: Optional[Hydrology] = None
        self.risk: Optional[Risk] = None

    def _load_data(
        self,
        forecast: bool = False,
        historical: bool = False,
    ) -> None:
        """
        S'assure que les données nécessaires sont chargées en mémoire.

        Args:
            forecast (bool): Si True, charge les prévisions si absentes.
            historical (bool): Si True, charge l'historique si absent.
        """
        if forecast and self.loader.forecast is None:
            self.loader.get_forecast()

        if historical and self.loader.historical is None:
            self.loader.get_historical()

    def _ensure_data(
        self,
        require_forecast: bool = False,
        require_historical: bool = False,
    ) -> None:
        """
        Ancien nom de ``_load_data``. Déprécié depuis v1.2.0.

        Args:
            require_forecast (bool): Ancien nom de forecast.
            require_historical (bool): Ancien nom de historical.
        """
        warnings.warn(
            "Weather._ensure_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Weather._load_data à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self._load_data(forecast=require_forecast, historical=require_historical)

    def _init_component(self, component: str) -> None:
        """
        S'assure que le composant demandé est initialisé.

        Args:
            component (str): Identifiant du composant parmi 'phenology',
                'hydrology' ou 'risk'.
        """
        if component == "phenology" and self.phenology is None:
            self._load_data(historical=True)
            hist = self.loader.historical
            self.phenology = Phenology(
                self.location,
                hist["precipitation"],
                hist[["temperature_min", "temperature_max"]],
            )

        elif component == "hydrology" and self.hydrology is None:
            self._load_data(historical=True)
            hist = self.loader.historical
            self.hydrology = Hydrology(
                self.location,
                hist["precipitation"],
                hist[["temperature_min", "temperature_max"]],
            )

        elif component == "risk" and self.risk is None:
            self._load_data(forecast=True, historical=True)
            self.risk = Risk(
                self.location,
                self.loader.historical["precipitation"],
                self.loader.forecast,
            )

    def _ensure_components(self, component: str) -> None:
        """
        Ancien nom de ``_init_component``. Déprécié depuis v1.2.0.

        Args:
            component (str): Identifiant du composant.
        """
        warnings.warn(
            "Weather._ensure_components est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Weather._init_component à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self._init_component(component)

    def forecast(self, days: int = None) -> dict:
        """
        Récupère la prévision météorologique court-terme.

        Args:
            days (int): Nombre de jours de prévision (défaut depuis CONFIG).

        Returns:
            dict: Dictionnaire des prévisions avec la localisation, les données
                brutes, la source et l'horodatage de mise à jour.
        """
        if days is None:
            days = CONFIG["weather"]["forecast_days_default"]

        # Plafonnement au maximum autorisé par la configuration
        if days > CONFIG["weather"]["max_forecast_days"]:
            days = CONFIG["weather"]["max_forecast_days"]

        df = self.loader.get_forecast(days=days)

        return {
            "location": {
                "name": self.location.name,
                "lat": self.location.lat,
                "lon": self.location.lon,
            },
            "data": df.reset_index().to_dict(orient="records"),
            "source": self.loader.source,
            "last_updated": pd.Timestamp.now().isoformat(),
        }

    def historical(
        self,
        metric: str = "all",
        months: int = 120,
        source: str = None,
        months_back: Optional[int] = None,
    ) -> pd.DataFrame:
        """
        Retourne les séries historiques météorologiques.

        Args:
            metric (str): Filtre de colonne : 'temperature', 'precipitation',
                'humidity' ou 'all' (défaut, toutes les colonnes).
            months (int): Nombre de mois d'historique à récupérer.
            source (str): Source des données de précipitation. Valeurs acceptées :
                'chirps', 'openmeteo', 'both'. Si None, utilise la valeur de CONFIG.
            months_back (int, optionnel): Ancien nom du paramètre ``months``.

        Returns:
            pd.DataFrame: DataFrame historique avec DatetimeIndex.
        """
        if months_back is not None:
            warnings.warn(
                "Le paramètre 'months_back' est obsolète et sera supprimé dans "
                "KadiPy v2.0. Utilisez 'months' à la place.",
                category=DeprecationWarning,
                stacklevel=2,
            )
            months = months_back

        df = self.loader.get_historical(months=months, source=source)

        # Filtre optionnel sur les colonnes
        if metric != "all":
            cols = [c for c in df.columns if metric in c]
            if cols:
                return df[cols]

        return df

    def gdd(
        self,
        crop: str,
        start: Union[str, pd.Timestamp],
        end: Union[str, pd.Timestamp] = None,
    ) -> dict:
        """
        Calcule l'accumulation des degrés-jours de croissance (GDD).

        Args:
            crop (str): Nom de la culture ('maize', 'rice', etc.).
            start (str ou pd.Timestamp): Date de semis (YYYY-MM-DD).
            end (str ou pd.Timestamp): Date de fin (optionnel).

        Returns:
            dict: Résultat du cumul GDD.
        """
        self._init_component("phenology")
        return self.phenology.gdd(crop, start, end)

    def growing_degree_days(
        self,
        crop: str,
        start_date: str,
        end_date: str = None,
    ) -> dict:
        """
        Ancienne méthode publique. Dépréciée depuis v1.2.0.

        Utilisez ``gdd(crop, start, end)`` à la place.

        Args:
            crop (str): Nom de la culture.
            start_date (str): Ancien nom de start.
            end_date (str): Ancien nom de end.

        Returns:
            dict: Résultat du cumul GDD.
        """
        warnings.warn(
            "Weather.growing_degree_days() est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez gdd() à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.gdd(crop, start_date, end_date)

    def onset(self) -> dict:
        """
        Détecte la date de démarrage de la saison agricole.

        Returns:
            dict: Résultat de l'onset.
        """
        self._init_component("phenology")
        return self.phenology.onset()

    def cessation(self) -> dict:
        """
        Détermine la date de fin des pluies utiles.

        Returns:
            dict: Résultat de la cessation.
        """
        self._init_component("phenology")
        return self.phenology.cessation()

    def drought(
        self,
        method: str = "spi",
        window: int = 3,
        window_months: Optional[int] = None,
    ) -> dict:
        """
        Calcule l'indice de sécheresse.

        Args:
            method (str): 'spi', 'markov', 'hurst', 'combined'. Par défaut 'spi'.
            window (int): Fenêtre temporelle en mois. Par défaut 3.
            window_months (int, optionnel): Ancien nom du paramètre window.

        Returns:
            dict: Indicateurs de sécheresse.
        """
        self._init_component("risk")
        return self.risk.drought(method=method, window=window, window_months=window_months)

    def drought_index(self, method: str = "spi", window_months: int = 3) -> dict:
        """
        Ancienne méthode publique. Dépréciée depuis v1.2.0.

        Utilisez ``drought(method, window)`` à la place.

        Args:
            method (str): Méthode de calcul.
            window_months (int): Ancien nom du paramètre window.

        Returns:
            dict: Indicateurs de sécheresse.
        """
        warnings.warn(
            "Weather.drought_index() est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez drought() à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.drought(method=method, window=window_months)

    def rain_prob(
        self,
        days: int = 1,
        min_mm: float = 1.0,
        days_ahead: Optional[int] = None,
        min_rainfall_mm: Optional[float] = None,
    ) -> dict:
        """
        Prévoit la probabilité de pluie.

        Args:
            days (int): Nombre de jours futurs.
            min_mm (float): Seuil minimum en mm.
            days_ahead (int, optionnel): Ancien nom du paramètre days.
            min_rainfall_mm (float, optionnel): Ancien nom du paramètre min_mm.

        Returns:
            dict: Probabilité et recommandations agronomiques.
        """
        if days_ahead is not None:
            warnings.warn(
                "Le paramètre 'days_ahead' est obsolète et sera supprimé dans "
                "KadiPy v2.0. Utilisez 'days' à la place.",
                category=DeprecationWarning,
                stacklevel=2,
            )
            days = days_ahead

        if min_rainfall_mm is not None:
            warnings.warn(
                "Le paramètre 'min_rainfall_mm' est obsolète et sera supprimé dans "
                "KadiPy v2.0. Utilisez 'min_mm' à la place.",
                category=DeprecationWarning,
                stacklevel=2,
            )
            min_mm = min_rainfall_mm

        self._init_component("risk")
        return self.risk.rain_prob(days=days, min_mm=min_mm)

    def rain_probability(self, days_ahead: int = 1, min_rainfall_mm: float = 1.0) -> dict:
        """
        Ancienne méthode publique. Dépréciée depuis v1.2.0.

        Utilisez ``rain_prob(days, min_mm)`` à la place.

        Args:
            days_ahead (int): Ancien nom de days.
            min_rainfall_mm (float): Ancien nom de min_mm.

        Returns:
            dict: Probabilité et recommandations agronomiques.
        """
        warnings.warn(
            "Weather.rain_probability() est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez rain_prob() à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.rain_prob(days=days_ahead, min_mm=min_rainfall_mm)

    def water_balance(
        self,
        crop: str = "maize",
        soil_type: str = "ferrugineux",
    ) -> pd.DataFrame:
        """
        Simule le bilan hydrique quotidien (FAO-56).

        Args:
            crop (str): Type de culture.
            soil_type (str): Type de sol.

        Returns:
            pd.DataFrame: DataFrame avec le bilan hydrique.
        """
        self._init_component("hydrology")
        # Mise à jour des paramètres de culture et de sol avant le calcul
        self.hydrology.crop = crop
        self.hydrology.soil_type = soil_type
        self.hydrology.soil = self.hydrology.soil_params(soil_type)
        return self.hydrology.water_balance()

    def et0_hargreaves(
        self,
        tmin: float,
        tmax: float,
        day_of_year: int,
    ) -> float:
        """
        Calcule l'ETo par Hargreaves-Samani.

        Args:
            tmin (float): Température minimale (degC).
            tmax (float): Température maximale (degC).
            day_of_year (int): Jour de l'année (1 à 365).

        Returns:
            float: ETo en mm/jour.
        """
        self._init_component("hydrology")
        return self.hydrology.et0_hargreaves(tmin, tmax, day_of_year)

    def _setup(self) -> None:
        """
        Initialise toutes les classes composantes de la façade météo.
        """
        self._init_component("phenology")
        self._init_component("hydrology")
        self._init_component("risk")

    def _init_all_components(self) -> None:
        """
        Ancien nom de ``_setup``. Déprécié depuis v1.2.0.
        """
        warnings.warn(
            "Weather._init_all_components est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Weather._setup à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self._setup()

    # ----------------------------------------------------------------
    # Attributs et propriétés dépréciés (anciens noms)
    # ----------------------------------------------------------------

    @property
    def cache_dir(self) -> Optional[str]:
        """Ancien nom de ``cache``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Weather.cache_dir est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Weather.cache à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.cache

    @cache_dir.setter
    def cache_dir(self, value: Optional[str]) -> None:
        """Ancien setter de ``cache``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Weather.cache_dir est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Weather.cache à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.cache = value

    @property
    def weather_data(self) -> WeatherLoader:
        """Ancien nom de ``loader``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Weather.weather_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Weather.loader à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.loader

    @weather_data.setter
    def weather_data(self, value: WeatherLoader) -> None:
        """Ancien setter de ``loader``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Weather.weather_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Weather.loader à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.loader = value

    @property
    def risk_indicators(self) -> Optional[Risk]:
        """Ancien nom de ``risk``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Weather.risk_indicators est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Weather.risk à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.risk

    @risk_indicators.setter
    def risk_indicators(self, value: Optional[Risk]) -> None:
        """Ancien setter de ``risk``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Weather.risk_indicators est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Weather.risk à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.risk = value

cache_dir property writable

cache_dir: Optional[str]

Ancien nom de cache. Déprécié depuis v1.2.0.

weather_data property writable

weather_data: WeatherLoader

Ancien nom de loader. Déprécié depuis v1.2.0.

risk_indicators property writable

risk_indicators: Optional[Risk]

Ancien nom de risk. Déprécié depuis v1.2.0.

__init__

__init__(lat: float = None, lon: float = None, name: str = None, cache: str = None, latitude: float = None, longitude: float = None, cache_dir: str = None)

Initialise une nouvelle façade météo pour une localisation.

Parameters:

Name Type Description Default
lat float

Latitude en degrés décimaux.

None
lon float

Longitude en degrés décimaux.

None
name str

Nom de la localité (optionnel).

None
cache str

Répertoire de cache (optionnel).

None
latitude float

Ancien nom du paramètre lat.

None
longitude float

Ancien nom du paramètre lon.

None
cache_dir str

Ancien nom du paramètre cache.

None
Source code in kadi/weather/session.py
def __init__(
    self,
    lat: float = None,
    lon: float = None,
    name: str = None,
    cache: str = None,
    latitude: float = None,
    longitude: float = None,
    cache_dir: str = None,
):
    """
    Initialise une nouvelle façade météo pour une localisation.

    Args:
        lat (float): Latitude en degrés décimaux.
        lon (float): Longitude en degrés décimaux.
        name (str): Nom de la localité (optionnel).
        cache (str): Répertoire de cache (optionnel).
        latitude (float): Ancien nom du paramètre lat.
        longitude (float): Ancien nom du paramètre lon.
        cache_dir (str): Ancien nom du paramètre cache.
    """
    # Prise en charge des anciens noms de paramètres d'initialisation
    if latitude is not None:
        warnings.warn(
            "Le paramètre 'latitude' est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez 'lat' à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        lat = latitude

    if longitude is not None:
        warnings.warn(
            "Le paramètre 'longitude' est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez 'lon' à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        lon = longitude

    if cache_dir is not None:
        warnings.warn(
            "Le paramètre 'cache_dir' est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez 'cache' à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        cache = cache_dir

    if lat is None or lon is None:
        raise TypeError("Les coordonnées 'lat' et 'lon' sont obligatoires.")

    self.location = Location(lat, lon, name)
    # Conservé pour la compatibilité de signature
    self.cache = cache

    # Initialisation du gestionnaire de données
    self.loader = WeatherLoader(self.location, cache)

    # Composants métiers (initialisés à la demande)
    self.phenology: Optional[Phenology] = None
    self.hydrology: Optional[Hydrology] = None
    self.risk: Optional[Risk] = None

forecast

forecast(days: int = None) -> dict

Récupère la prévision météorologique court-terme.

Parameters:

Name Type Description Default
days int

Nombre de jours de prévision (défaut depuis CONFIG).

None

Returns:

Name Type Description
dict dict

Dictionnaire des prévisions avec la localisation, les données brutes, la source et l'horodatage de mise à jour.

Source code in kadi/weather/session.py
def forecast(self, days: int = None) -> dict:
    """
    Récupère la prévision météorologique court-terme.

    Args:
        days (int): Nombre de jours de prévision (défaut depuis CONFIG).

    Returns:
        dict: Dictionnaire des prévisions avec la localisation, les données
            brutes, la source et l'horodatage de mise à jour.
    """
    if days is None:
        days = CONFIG["weather"]["forecast_days_default"]

    # Plafonnement au maximum autorisé par la configuration
    if days > CONFIG["weather"]["max_forecast_days"]:
        days = CONFIG["weather"]["max_forecast_days"]

    df = self.loader.get_forecast(days=days)

    return {
        "location": {
            "name": self.location.name,
            "lat": self.location.lat,
            "lon": self.location.lon,
        },
        "data": df.reset_index().to_dict(orient="records"),
        "source": self.loader.source,
        "last_updated": pd.Timestamp.now().isoformat(),
    }

historical

historical(metric: str = 'all', months: int = 120, source: str = None, months_back: Optional[int] = None) -> pd.DataFrame

Retourne les séries historiques météorologiques.

Parameters:

Name Type Description Default
metric str

Filtre de colonne : 'temperature', 'precipitation', 'humidity' ou 'all' (défaut, toutes les colonnes).

'all'
months int

Nombre de mois d'historique à récupérer.

120
source str

Source des données de précipitation. Valeurs acceptées : 'chirps', 'openmeteo', 'both'. Si None, utilise la valeur de CONFIG.

None
months_back (int, optionnel)

Ancien nom du paramètre months.

None

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame historique avec DatetimeIndex.

Source code in kadi/weather/session.py
def historical(
    self,
    metric: str = "all",
    months: int = 120,
    source: str = None,
    months_back: Optional[int] = None,
) -> pd.DataFrame:
    """
    Retourne les séries historiques météorologiques.

    Args:
        metric (str): Filtre de colonne : 'temperature', 'precipitation',
            'humidity' ou 'all' (défaut, toutes les colonnes).
        months (int): Nombre de mois d'historique à récupérer.
        source (str): Source des données de précipitation. Valeurs acceptées :
            'chirps', 'openmeteo', 'both'. Si None, utilise la valeur de CONFIG.
        months_back (int, optionnel): Ancien nom du paramètre ``months``.

    Returns:
        pd.DataFrame: DataFrame historique avec DatetimeIndex.
    """
    if months_back is not None:
        warnings.warn(
            "Le paramètre 'months_back' est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez 'months' à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        months = months_back

    df = self.loader.get_historical(months=months, source=source)

    # Filtre optionnel sur les colonnes
    if metric != "all":
        cols = [c for c in df.columns if metric in c]
        if cols:
            return df[cols]

    return df

gdd

gdd(crop: str, start: Union[str, Timestamp], end: Union[str, Timestamp] = None) -> dict

Calcule l'accumulation des degrés-jours de croissance (GDD).

Parameters:

Name Type Description Default
crop str

Nom de la culture ('maize', 'rice', etc.).

required
start str ou pd.Timestamp

Date de semis (YYYY-MM-DD).

required
end str ou pd.Timestamp

Date de fin (optionnel).

None

Returns:

Name Type Description
dict dict

Résultat du cumul GDD.

Source code in kadi/weather/session.py
def gdd(
    self,
    crop: str,
    start: Union[str, pd.Timestamp],
    end: Union[str, pd.Timestamp] = None,
) -> dict:
    """
    Calcule l'accumulation des degrés-jours de croissance (GDD).

    Args:
        crop (str): Nom de la culture ('maize', 'rice', etc.).
        start (str ou pd.Timestamp): Date de semis (YYYY-MM-DD).
        end (str ou pd.Timestamp): Date de fin (optionnel).

    Returns:
        dict: Résultat du cumul GDD.
    """
    self._init_component("phenology")
    return self.phenology.gdd(crop, start, end)

growing_degree_days

growing_degree_days(crop: str, start_date: str, end_date: str = None) -> dict

Ancienne méthode publique. Dépréciée depuis v1.2.0.

Utilisez gdd(crop, start, end) à la place.

Parameters:

Name Type Description Default
crop str

Nom de la culture.

required
start_date str

Ancien nom de start.

required
end_date str

Ancien nom de end.

None

Returns:

Name Type Description
dict dict

Résultat du cumul GDD.

Source code in kadi/weather/session.py
def growing_degree_days(
    self,
    crop: str,
    start_date: str,
    end_date: str = None,
) -> dict:
    """
    Ancienne méthode publique. Dépréciée depuis v1.2.0.

    Utilisez ``gdd(crop, start, end)`` à la place.

    Args:
        crop (str): Nom de la culture.
        start_date (str): Ancien nom de start.
        end_date (str): Ancien nom de end.

    Returns:
        dict: Résultat du cumul GDD.
    """
    warnings.warn(
        "Weather.growing_degree_days() est obsolète et sera supprimé dans "
        "KadiPy v2.0. Utilisez gdd() à la place.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return self.gdd(crop, start_date, end_date)

onset

onset() -> dict

Détecte la date de démarrage de la saison agricole.

Returns:

Name Type Description
dict dict

Résultat de l'onset.

Source code in kadi/weather/session.py
def onset(self) -> dict:
    """
    Détecte la date de démarrage de la saison agricole.

    Returns:
        dict: Résultat de l'onset.
    """
    self._init_component("phenology")
    return self.phenology.onset()

cessation

cessation() -> dict

Détermine la date de fin des pluies utiles.

Returns:

Name Type Description
dict dict

Résultat de la cessation.

Source code in kadi/weather/session.py
def cessation(self) -> dict:
    """
    Détermine la date de fin des pluies utiles.

    Returns:
        dict: Résultat de la cessation.
    """
    self._init_component("phenology")
    return self.phenology.cessation()

drought

drought(method: str = 'spi', window: int = 3, window_months: Optional[int] = None) -> dict

Calcule l'indice de sécheresse.

Parameters:

Name Type Description Default
method str

'spi', 'markov', 'hurst', 'combined'. Par défaut 'spi'.

'spi'
window int

Fenêtre temporelle en mois. Par défaut 3.

3
window_months (int, optionnel)

Ancien nom du paramètre window.

None

Returns:

Name Type Description
dict dict

Indicateurs de sécheresse.

Source code in kadi/weather/session.py
def drought(
    self,
    method: str = "spi",
    window: int = 3,
    window_months: Optional[int] = None,
) -> dict:
    """
    Calcule l'indice de sécheresse.

    Args:
        method (str): 'spi', 'markov', 'hurst', 'combined'. Par défaut 'spi'.
        window (int): Fenêtre temporelle en mois. Par défaut 3.
        window_months (int, optionnel): Ancien nom du paramètre window.

    Returns:
        dict: Indicateurs de sécheresse.
    """
    self._init_component("risk")
    return self.risk.drought(method=method, window=window, window_months=window_months)

drought_index

drought_index(method: str = 'spi', window_months: int = 3) -> dict

Ancienne méthode publique. Dépréciée depuis v1.2.0.

Utilisez drought(method, window) à la place.

Parameters:

Name Type Description Default
method str

Méthode de calcul.

'spi'
window_months int

Ancien nom du paramètre window.

3

Returns:

Name Type Description
dict dict

Indicateurs de sécheresse.

Source code in kadi/weather/session.py
def drought_index(self, method: str = "spi", window_months: int = 3) -> dict:
    """
    Ancienne méthode publique. Dépréciée depuis v1.2.0.

    Utilisez ``drought(method, window)`` à la place.

    Args:
        method (str): Méthode de calcul.
        window_months (int): Ancien nom du paramètre window.

    Returns:
        dict: Indicateurs de sécheresse.
    """
    warnings.warn(
        "Weather.drought_index() est obsolète et sera supprimé dans "
        "KadiPy v2.0. Utilisez drought() à la place.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return self.drought(method=method, window=window_months)

rain_prob

rain_prob(days: int = 1, min_mm: float = 1.0, days_ahead: Optional[int] = None, min_rainfall_mm: Optional[float] = None) -> dict

Prévoit la probabilité de pluie.

Parameters:

Name Type Description Default
days int

Nombre de jours futurs.

1
min_mm float

Seuil minimum en mm.

1.0
days_ahead (int, optionnel)

Ancien nom du paramètre days.

None
min_rainfall_mm (float, optionnel)

Ancien nom du paramètre min_mm.

None

Returns:

Name Type Description
dict dict

Probabilité et recommandations agronomiques.

Source code in kadi/weather/session.py
def rain_prob(
    self,
    days: int = 1,
    min_mm: float = 1.0,
    days_ahead: Optional[int] = None,
    min_rainfall_mm: Optional[float] = None,
) -> dict:
    """
    Prévoit la probabilité de pluie.

    Args:
        days (int): Nombre de jours futurs.
        min_mm (float): Seuil minimum en mm.
        days_ahead (int, optionnel): Ancien nom du paramètre days.
        min_rainfall_mm (float, optionnel): Ancien nom du paramètre min_mm.

    Returns:
        dict: Probabilité et recommandations agronomiques.
    """
    if days_ahead is not None:
        warnings.warn(
            "Le paramètre 'days_ahead' est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez 'days' à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        days = days_ahead

    if min_rainfall_mm is not None:
        warnings.warn(
            "Le paramètre 'min_rainfall_mm' est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez 'min_mm' à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        min_mm = min_rainfall_mm

    self._init_component("risk")
    return self.risk.rain_prob(days=days, min_mm=min_mm)

rain_probability

rain_probability(days_ahead: int = 1, min_rainfall_mm: float = 1.0) -> dict

Ancienne méthode publique. Dépréciée depuis v1.2.0.

Utilisez rain_prob(days, min_mm) à la place.

Parameters:

Name Type Description Default
days_ahead int

Ancien nom de days.

1
min_rainfall_mm float

Ancien nom de min_mm.

1.0

Returns:

Name Type Description
dict dict

Probabilité et recommandations agronomiques.

Source code in kadi/weather/session.py
def rain_probability(self, days_ahead: int = 1, min_rainfall_mm: float = 1.0) -> dict:
    """
    Ancienne méthode publique. Dépréciée depuis v1.2.0.

    Utilisez ``rain_prob(days, min_mm)`` à la place.

    Args:
        days_ahead (int): Ancien nom de days.
        min_rainfall_mm (float): Ancien nom de min_mm.

    Returns:
        dict: Probabilité et recommandations agronomiques.
    """
    warnings.warn(
        "Weather.rain_probability() est obsolète et sera supprimé dans "
        "KadiPy v2.0. Utilisez rain_prob() à la place.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return self.rain_prob(days=days_ahead, min_mm=min_rainfall_mm)

water_balance

water_balance(crop: str = 'maize', soil_type: str = 'ferrugineux') -> pd.DataFrame

Simule le bilan hydrique quotidien (FAO-56).

Parameters:

Name Type Description Default
crop str

Type de culture.

'maize'
soil_type str

Type de sol.

'ferrugineux'

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame avec le bilan hydrique.

Source code in kadi/weather/session.py
def water_balance(
    self,
    crop: str = "maize",
    soil_type: str = "ferrugineux",
) -> pd.DataFrame:
    """
    Simule le bilan hydrique quotidien (FAO-56).

    Args:
        crop (str): Type de culture.
        soil_type (str): Type de sol.

    Returns:
        pd.DataFrame: DataFrame avec le bilan hydrique.
    """
    self._init_component("hydrology")
    # Mise à jour des paramètres de culture et de sol avant le calcul
    self.hydrology.crop = crop
    self.hydrology.soil_type = soil_type
    self.hydrology.soil = self.hydrology.soil_params(soil_type)
    return self.hydrology.water_balance()

et0_hargreaves

et0_hargreaves(tmin: float, tmax: float, day_of_year: int) -> float

Calcule l'ETo par Hargreaves-Samani.

Parameters:

Name Type Description Default
tmin float

Température minimale (degC).

required
tmax float

Température maximale (degC).

required
day_of_year int

Jour de l'année (1 à 365).

required

Returns:

Name Type Description
float float

ETo en mm/jour.

Source code in kadi/weather/session.py
def et0_hargreaves(
    self,
    tmin: float,
    tmax: float,
    day_of_year: int,
) -> float:
    """
    Calcule l'ETo par Hargreaves-Samani.

    Args:
        tmin (float): Température minimale (degC).
        tmax (float): Température maximale (degC).
        day_of_year (int): Jour de l'année (1 à 365).

    Returns:
        float: ETo en mm/jour.
    """
    self._init_component("hydrology")
    return self.hydrology.et0_hargreaves(tmin, tmax, day_of_year)

Location

Représente une localisation géographique avec détection automatique de la zone climatique béninoise.

Attributs

lat (float): Latitude en degrés décimaux. lon (float): Longitude en degrés décimaux. name (str): Nom de la localité. zone (str): Zone climatique détectée ('Sud', 'Centre' ou 'Nord'). regime (str): Régime climatique ('bimodal' ou 'unimodal').

Source code in kadi/weather/location.py
class Location:
    """
    Représente une localisation géographique avec détection automatique
    de la zone climatique béninoise.

    Attributs:
        lat (float): Latitude en degrés décimaux.
        lon (float): Longitude en degrés décimaux.
        name (str): Nom de la localité.
        zone (str): Zone climatique détectée ('Sud', 'Centre' ou 'Nord').
        regime (str): Régime climatique ('bimodal' ou 'unimodal').
    """

    def __init__(
        self,
        lat: float = None,
        lon: float = None,
        name: str = None,
        *,
        # Anciens paramètres conservés pour la rétrocompatibilité
        latitude: float = None,
        longitude: float = None,
    ):
        """
        Initialise une nouvelle localisation.

        Les paramètres ``lat`` et ``lon`` sont les noms recommandés.
        Les anciens noms ``latitude`` et ``longitude`` restent acceptés
        mais émettent un DeprecationWarning.

        Args:
            lat (float): Latitude en degrés décimaux.
            lon (float): Longitude en degrés décimaux.
            name (str): Nom de la localité (optionnel).
            latitude (float): Ancien nom de lat. Déprécié depuis v1.2.0.
            longitude (float): Ancien nom de lon. Déprécié depuis v1.2.0.
        """
        # Gestion des anciens paramètres avec avertissement de déprécation
        if latitude is not None:
            warnings.warn(
                "Le paramètre 'latitude' est obsolète et sera supprimé dans "
                "KadiPy v2.0. Utilisez 'lat' à la place.",
                category=DeprecationWarning,
                stacklevel=2,
            )
            lat = latitude

        if longitude is not None:
            warnings.warn(
                "Le paramètre 'longitude' est obsolète et sera supprimé dans "
                "KadiPy v2.0. Utilisez 'lon' à la place.",
                category=DeprecationWarning,
                stacklevel=2,
            )
            lon = longitude

        # Validation : les coordonnées doivent être fournies
        if lat is None or lon is None:
            raise TypeError("Location() requiert les arguments 'lat' et 'lon'.")

        # Stockage des coordonnées
        self.lat = lat
        self.lon = lon

        # Validation par rapport à la zone d'étude V1 (Bénin)
        bbox = CONFIG["weather"]["gps_validation_bbox"]
        if not (
            bbox["min_lat"] <= lat <= bbox["max_lat"]
            and bbox["min_lon"] <= lon <= bbox["max_lon"]
        ):
            raise LocationError(
                f"Les coordonnées GPS ({lat}, {lon}) sont en dehors "
                "de la zone d'étude (Bénin V1)."
            )

        self.name = name if name else f"Point({lat}, {lon})"

        # Détection automatique de la zone et du régime (stockés comme attributs)
        self.zone = self._detect_zone()
        self.regime = self._regime()

    def _detect_zone(self) -> str:
        """
        Calcule la zone climatique à partir de la latitude.

        Méthode interne appelée à l'initialisation. Pour lire la zone depuis
        l'extérieur, utiliser l'attribut ``self.zone``.

        Returns:
            str: 'Sud', 'Centre' ou 'Nord'.
        """
        if self.lat < 7.5:
            return "Sud"
        elif 7.5 <= self.lat < 9.0:
            return "Centre"
        else:
            return "Nord"

    def _regime(self) -> str:
        """
        Déduit le régime climatique selon la zone.

        Le Sud et le Centre ont un régime bimodal (deux saisons de pluies).
        Le Nord a un régime unimodal (une saison de pluies).

        Returns:
            str: 'bimodal' ou 'unimodal'.
        """
        # Le Centre suit également un cycle bimodal
        if self.zone in ("Sud", "Centre"):
            return "bimodal"
        return "unimodal"

    def climate(self) -> dict:
        """
        Retourne les paramètres climatiques par défaut pour la zone.

        Returns:
            dict: Dictionnaire des paramètres climatiques de la zone.
        """
        params = {
            "Sud":    {"Tbase": 10, "onset_method": "walter_anyadike"},
            "Centre": {"Tbase": 10, "onset_method": "hybrid"},
            "Nord":   {"Tbase": 10, "onset_method": "sivakumar"},
        }
        return params.get(self.zone, {})

    def to_dict(self) -> dict:
        """
        Sérialise la localisation pour le cache.

        Returns:
            dict: Représentation sérialisable de l'objet Location.
        """
        return {
            "name":   self.name,
            "lat":    self.lat,
            "lon":    self.lon,
            "zone":   self.zone,
            "regime": self.regime,
        }

    # ----------------------------------------------------------------
    # Méthodes dépréciées (anciens noms publics)
    # ----------------------------------------------------------------

    def detect_zone(self) -> str:
        """
        Ancienne méthode publique. Dépréciée depuis v1.2.0.

        Utilisez l'attribut ``self.zone`` à la place.

        Returns:
            str: Zone climatique ('Sud', 'Centre' ou 'Nord').
        """
        warnings.warn(
            "Location.detect_zone() est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez l'attribut Location.zone à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.zone

    def get_climate_params(self) -> dict:
        """
        Ancienne méthode publique. Dépréciée depuis v1.2.0.

        Utilisez ``Location.climate()`` à la place.

        Returns:
            dict: Paramètres climatiques de la zone.
        """
        warnings.warn(
            "Location.get_climate_params() est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Location.climate() à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.climate()

    # ----------------------------------------------------------------
    # Propriétés de rétrocompatibilité (anciens noms d'attributs)
    # ----------------------------------------------------------------

    @property
    def latitude(self) -> float:
        """Ancien nom de ``lat``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Location.latitude est obsolète et sera supprimé dans KadiPy v2.0. "
            "Utilisez Location.lat à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.lat

    @latitude.setter
    def latitude(self, value: float) -> None:
        """Ancien setter de ``lat``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Location.latitude est obsolète et sera supprimé dans KadiPy v2.0. "
            "Utilisez Location.lat à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.lat = value

    @property
    def longitude(self) -> float:
        """Ancien nom de ``lon``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Location.longitude est obsolète et sera supprimé dans KadiPy v2.0. "
            "Utilisez Location.lon à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.lon

    @longitude.setter
    def longitude(self, value: float) -> None:
        """Ancien setter de ``lon``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Location.longitude est obsolète et sera supprimé dans KadiPy v2.0. "
            "Utilisez Location.lon à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.lon = value

    @property
    def climate_regime(self) -> str:
        """Ancien nom de ``regime``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Location.climate_regime est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Location.regime à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.regime

    @climate_regime.setter
    def climate_regime(self, value: str) -> None:
        """Ancien setter de ``regime``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Location.climate_regime est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Location.regime à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.regime = value

latitude property writable

latitude: float

Ancien nom de lat. Déprécié depuis v1.2.0.

longitude property writable

longitude: float

Ancien nom de lon. Déprécié depuis v1.2.0.

climate_regime property writable

climate_regime: str

Ancien nom de regime. Déprécié depuis v1.2.0.

__init__

__init__(lat: float = None, lon: float = None, name: str = None, *, latitude: float = None, longitude: float = None)

Initialise une nouvelle localisation.

Les paramètres lat et lon sont les noms recommandés. Les anciens noms latitude et longitude restent acceptés mais émettent un DeprecationWarning.

Parameters:

Name Type Description Default
lat float

Latitude en degrés décimaux.

None
lon float

Longitude en degrés décimaux.

None
name str

Nom de la localité (optionnel).

None
latitude float

Ancien nom de lat. Déprécié depuis v1.2.0.

None
longitude float

Ancien nom de lon. Déprécié depuis v1.2.0.

None
Source code in kadi/weather/location.py
def __init__(
    self,
    lat: float = None,
    lon: float = None,
    name: str = None,
    *,
    # Anciens paramètres conservés pour la rétrocompatibilité
    latitude: float = None,
    longitude: float = None,
):
    """
    Initialise une nouvelle localisation.

    Les paramètres ``lat`` et ``lon`` sont les noms recommandés.
    Les anciens noms ``latitude`` et ``longitude`` restent acceptés
    mais émettent un DeprecationWarning.

    Args:
        lat (float): Latitude en degrés décimaux.
        lon (float): Longitude en degrés décimaux.
        name (str): Nom de la localité (optionnel).
        latitude (float): Ancien nom de lat. Déprécié depuis v1.2.0.
        longitude (float): Ancien nom de lon. Déprécié depuis v1.2.0.
    """
    # Gestion des anciens paramètres avec avertissement de déprécation
    if latitude is not None:
        warnings.warn(
            "Le paramètre 'latitude' est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez 'lat' à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        lat = latitude

    if longitude is not None:
        warnings.warn(
            "Le paramètre 'longitude' est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez 'lon' à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        lon = longitude

    # Validation : les coordonnées doivent être fournies
    if lat is None or lon is None:
        raise TypeError("Location() requiert les arguments 'lat' et 'lon'.")

    # Stockage des coordonnées
    self.lat = lat
    self.lon = lon

    # Validation par rapport à la zone d'étude V1 (Bénin)
    bbox = CONFIG["weather"]["gps_validation_bbox"]
    if not (
        bbox["min_lat"] <= lat <= bbox["max_lat"]
        and bbox["min_lon"] <= lon <= bbox["max_lon"]
    ):
        raise LocationError(
            f"Les coordonnées GPS ({lat}, {lon}) sont en dehors "
            "de la zone d'étude (Bénin V1)."
        )

    self.name = name if name else f"Point({lat}, {lon})"

    # Détection automatique de la zone et du régime (stockés comme attributs)
    self.zone = self._detect_zone()
    self.regime = self._regime()

climate

climate() -> dict

Retourne les paramètres climatiques par défaut pour la zone.

Returns:

Name Type Description
dict dict

Dictionnaire des paramètres climatiques de la zone.

Source code in kadi/weather/location.py
def climate(self) -> dict:
    """
    Retourne les paramètres climatiques par défaut pour la zone.

    Returns:
        dict: Dictionnaire des paramètres climatiques de la zone.
    """
    params = {
        "Sud":    {"Tbase": 10, "onset_method": "walter_anyadike"},
        "Centre": {"Tbase": 10, "onset_method": "hybrid"},
        "Nord":   {"Tbase": 10, "onset_method": "sivakumar"},
    }
    return params.get(self.zone, {})

to_dict

to_dict() -> dict

Sérialise la localisation pour le cache.

Returns:

Name Type Description
dict dict

Représentation sérialisable de l'objet Location.

Source code in kadi/weather/location.py
def to_dict(self) -> dict:
    """
    Sérialise la localisation pour le cache.

    Returns:
        dict: Représentation sérialisable de l'objet Location.
    """
    return {
        "name":   self.name,
        "lat":    self.lat,
        "lon":    self.lon,
        "zone":   self.zone,
        "regime": self.regime,
    }

detect_zone

detect_zone() -> str

Ancienne méthode publique. Dépréciée depuis v1.2.0.

Utilisez l'attribut self.zone à la place.

Returns:

Name Type Description
str str

Zone climatique ('Sud', 'Centre' ou 'Nord').

Source code in kadi/weather/location.py
def detect_zone(self) -> str:
    """
    Ancienne méthode publique. Dépréciée depuis v1.2.0.

    Utilisez l'attribut ``self.zone`` à la place.

    Returns:
        str: Zone climatique ('Sud', 'Centre' ou 'Nord').
    """
    warnings.warn(
        "Location.detect_zone() est obsolète et sera supprimé dans "
        "KadiPy v2.0. Utilisez l'attribut Location.zone à la place.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return self.zone

get_climate_params

get_climate_params() -> dict

Ancienne méthode publique. Dépréciée depuis v1.2.0.

Utilisez Location.climate() à la place.

Returns:

Name Type Description
dict dict

Paramètres climatiques de la zone.

Source code in kadi/weather/location.py
def get_climate_params(self) -> dict:
    """
    Ancienne méthode publique. Dépréciée depuis v1.2.0.

    Utilisez ``Location.climate()`` à la place.

    Returns:
        dict: Paramètres climatiques de la zone.
    """
    warnings.warn(
        "Location.get_climate_params() est obsolète et sera supprimé dans "
        "KadiPy v2.0. Utilisez Location.climate() à la place.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return self.climate()

WeatherLoader

Gère l'acquisition, le cache SQLite et la normalisation des données météorologiques pour une localisation donnée.

Attributs

location (Location): Localisation associée. forecast (pd.DataFrame): Prévisions en mémoire (None si non chargées). historical (pd.DataFrame): Historique en mémoire (None si non chargé). source (str): Source des dernières données chargées.

Source code in kadi/weather/data.py
 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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
class WeatherLoader:
    """
    Gère l'acquisition, le cache SQLite et la normalisation
    des données météorologiques pour une localisation donnée.

    Attributs:
        location (Location): Localisation associée.
        forecast (pd.DataFrame): Prévisions en mémoire (None si non chargées).
        historical (pd.DataFrame): Historique en mémoire (None si non chargé).
        source (str): Source des dernières données chargées.
    """

    @staticmethod
    def _unify_temp(df: pd.DataFrame) -> pd.DataFrame:
        """
        Garantit la présence de la colonne ``temperature_mean`` dans le DataFrame.

        Centralise la logique d'alias entre ``temperature_avg`` (nom SQLite du
        cache) et ``temperature_mean`` (nom utilisé par les algorithmes du module
        weather).

        Ordre de priorité :
        1. Calcul direct (temperature_min + temperature_max) / 2 si les deux
           colonnes de base sont disponibles.
        2. Alias depuis ``temperature_avg`` si le calcul est impossible (cas
           cache sans temperature_min/temperature_max).

        Args:
            df (pd.DataFrame): DataFrame météo brut ou issu du cache.

        Returns:
            pd.DataFrame: DataFrame avec ``temperature_mean`` garanti si les
                données sources sont présentes.
        """
        # Calcul direct prioritaire : (min + max) / 2
        if "temperature_min" in df.columns and "temperature_max" in df.columns:
            tmean_calc = (df["temperature_min"] + df["temperature_max"]) / 2.0
            if "temperature_mean" not in df.columns:
                # Création de la colonne depuis les bornes thermiques
                df["temperature_mean"] = tmean_calc
            else:
                # Comblement des NaN uniquement, sans écraser les valeurs existantes
                df["temperature_mean"] = df["temperature_mean"].fillna(tmean_calc)
        elif "temperature_avg" in df.columns and "temperature_mean" not in df.columns:
            # Alias de secours : temperature_avg provient du cache SQLite
            df["temperature_mean"] = df["temperature_avg"]
        return df

    def __init__(self, location: Location, cache_dir: str = None):
        """
        Initialise le gestionnaire de données pour une localisation donnée.

        Args:
            location (Location): Instance de la classe Location.
            cache_dir (str): Ignoré car on utilise kadi.cache (SQLite global).
        """
        self.location = location
        # Données en mémoire (chargées à la demande)
        self.forecast: Optional[pd.DataFrame] = None
        self.historical: Optional[pd.DataFrame] = None
        self.source: str = "none"

        # S'assure que la base de données et les tables existent
        init_db()

    def get_forecast(self, days: int = 7, refresh: bool = False) -> pd.DataFrame:
        """
        Récupère les prévisions météorologiques en vérifiant d'abord le cache.

        Args:
            days (int): Nombre de jours de prévision à récupérer.
            refresh (bool): Si True, ignore le cache et force le rechargement.

        Returns:
            pd.DataFrame: DataFrame indexé par date avec les prévisions.

        Raises:
            OfflineError: Si l'API est inaccessible et qu'aucun cache n'existe.
        """
        today = datetime.now().date()
        end_date = today + timedelta(days=days - 1)

        if not refresh:
            # Vérification du cache avant tout appel API
            cached_data = self._from_cache(today.isoformat(), end_date.isoformat())
            if not cached_data.empty and len(cached_data) >= days:
                fetched_at = pd.to_datetime(cached_data["fetched_at"]).max()
                cache_ttl = timedelta(
                    hours=CONFIG["weather"]["cache_ttl_forecast_hours"]
                )
                if datetime.now() - fetched_at < cache_ttl:
                    cached_data = cached_data.head(days)
                    self.forecast = cached_data
                    self.source = "cached"
                    return cached_data

        # Appel API
        try:
            df = self._fetch_forecast(days=days)
            self._to_cache(df, "forecast")
        except Exception as e:
            # Repli sur le cache si disponible
            if "cached_data" in locals() and not cached_data.empty:
                cached_data = cached_data.head(days)
                self.forecast = cached_data
                self.source = "cached_offline"
                return cached_data
            raise OfflineError(
                "Impossible de récupérer les prévisions et aucun cache "
                f"n'est disponible : {e}"
            )

        df = df.head(days)
        self.forecast = df
        self.source = "open-meteo"
        return df

    def get_historical(
        self,
        months: int = 120,
        refresh: bool = False,
        source: str = None,
    ) -> pd.DataFrame:
        """
        Récupère les données historiques en vérifiant d'abord le cache SQLite.

        Args:
            months (int): Nombre de mois d'historique à récupérer.
            refresh (bool): Si True, ignore le cache et force le rechargement.
            source (str): Source de précipitation. Valeurs acceptées :
                'chirps' (CHIRPS uniquement, repli Open-Meteo si indisponible),
                'openmeteo' (Open-Meteo uniquement, comportement V1.0),
                'both' (CHIRPS pour l'historique long, Open-Meteo pour le
                récent). Si None, utilise CONFIG.

        Returns:
            pd.DataFrame: DataFrame indexé par date avec l'historique météo.

        Raises:
            OfflineError: Si l'API est inaccessible et qu'aucun cache n'existe.
        """
        # Résolution de la source par défaut depuis la configuration
        if source is None:
            source = CONFIG["weather"]["chirps"]["source_default"]

        days = months * 30
        today = datetime.now().date()
        start_date = today - timedelta(days=days)

        if not refresh:
            # Vérification du cache (tolérance de 5 jours)
            cached_data = self._from_cache(start_date.isoformat(), today.isoformat())
            if not cached_data.empty and len(cached_data) >= (days - 5):
                fetched_at = pd.to_datetime(cached_data["fetched_at"]).max()
                cache_ttl = timedelta(
                    days=CONFIG["weather"]["cache_ttl_historical_days"]
                )
                if datetime.now() - fetched_at < cache_ttl:
                    self.historical = cached_data
                    self.source = "cached"
                    return cached_data

        # Appel API
        try:
            df = self._fetch_historical(days=days, source=source)
            self._to_cache(df, "historical")
        except Exception as e:
            if "cached_data" in locals() and not cached_data.empty:
                self.historical = cached_data
                self.source = "cached_offline"
                return cached_data
            raise OfflineError(
                "Impossible de récupérer l'historique météo et aucun cache "
                f"n'est disponible : {e}"
            )

        self.historical = df
        self.source = source
        return df

    def _from_cache(self, start: str, end: str) -> pd.DataFrame:
        """
        Récupère les données météo depuis le cache SQLite KadiPy.

        Args:
            start (str): Date de début au format ISO (YYYY-MM-DD).
            end (str): Date de fin au format ISO (YYYY-MM-DD).

        Returns:
            pd.DataFrame: DataFrame des données en cache, ou DataFrame vide.
        """
        with get_connection() as conn:
            query = """
                SELECT date, temperature_min, temperature_max, temperature_avg,
                       precipitation, humidity, data_type, data_source,
                       confidence, fetched_at
                FROM weather_data
                WHERE location_id = ? AND date >= ? AND date <= ?
                ORDER BY date ASC, fetched_at DESC
            """
            df = pd.read_sql_query(
                query, conn,
                params=(self.location.name, start, end),
            )

            if df.empty:
                return df

            # Déduplication : garder le fetch le plus récent par date
            df = df.drop_duplicates(subset=["date"], keep="first")

            # Normalisation de l'index
            df["date"] = pd.to_datetime(df["date"])
            df.set_index("date", inplace=True)

            # Garantit la présence de temperature_mean (logique centralisée)
            df = WeatherLoader._unify_temp(df)

            return df

    def _to_cache(self, data: pd.DataFrame, kind: str) -> None:
        """
        Insère ou met à jour les données météo dans le cache SQLite KadiPy.

        Args:
            data (pd.DataFrame): DataFrame contenant les données météo normalisées.
            kind (str): Type de données ('forecast' ou 'historical').

        Returns:
            None
        """
        # Vérification si le DataFrame est vide
        if data.empty:
            # Fin prématurée si aucune donnée à sauvegarder
            return

        # Ouverture de la connexion à la base de données SQLite KadiPy
        with get_connection() as conn:
            # Création du curseur d'exécution SQL
            cursor = conn.cursor()

            # Horodatage courant au format ISO
            now = datetime.now().isoformat()

            # Parcours de chaque ligne du DataFrame de données météo
            for date_idx, row in data.iterrows():
                # Formattage de la date en chaîne YYYY-MM-DD
                date_str = date_idx.strftime("%Y-%m-%d")

                # Extraction ou calcul de la température moyenne
                t_avg = row.get("temperature_mean", row.get("temperature_avg", None))
                if t_avg is None:
                    # Calcul par la moyenne min et max
                    t_avg = (row["temperature_min"] + row["temperature_max"]) / 2.0

                # Extraction de la source réelle de données pour la ligne courante
                valeur_source = row.get("data_source", None)
                if not valeur_source or pd.isna(valeur_source):
                    valeur_source = "open-meteo"

                # Insertion avec gestion du conflit d'unicité (ON CONFLICT UPDATE)
                cursor.execute(
                    """
                    INSERT INTO weather_data (
                        location_id, latitude, longitude, date, hour,
                        temperature_min, temperature_max, temperature_avg,
                        precipitation, humidity, data_type, data_source,
                        confidence, fetched_at
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    ON CONFLICT(location_id, date, hour) DO UPDATE SET
                        temperature_min=excluded.temperature_min,
                        temperature_max=excluded.temperature_max,
                        temperature_avg=excluded.temperature_avg,
                        precipitation=excluded.precipitation,
                        humidity=excluded.humidity,
                        data_type=excluded.data_type,
                        data_source=excluded.data_source,
                        confidence=excluded.confidence,
                        fetched_at=excluded.fetched_at
                    """,
                    (
                        self.location.name,
                        self.location.lat,
                        self.location.lon,
                        date_str,
                        -1,
                        row["temperature_min"],
                        row["temperature_max"],
                        t_avg,
                        row["precipitation"],
                        row.get("humidity", 0.0),
                        kind,
                        str(valeur_source),
                        1.0,
                        now,
                    ),
                )

            # Détermination de la source principale pour la table de métadonnées
            source_principale = "open-meteo"
            if "data_source" in data.columns:
                sources_uniques = [
                    str(s)
                    for s in data["data_source"].dropna().unique()
                    if s
                ]
                if sources_uniques:
                    if len(sources_uniques) > 1:
                        source_principale = "+".join(sorted(sources_uniques))
                    else:
                        source_principale = sources_uniques[0]

            # Mise à jour de la table des métadonnées du cache
            cursor.execute(
                """
                INSERT INTO cache_metadata (
                    module_name, table_name, data_source,
                    last_fetch, last_success, last_update
                ) VALUES ('weather', 'weather_data', ?, ?, ?, ?)
                ON CONFLICT(module_name, data_source) DO UPDATE SET
                    last_fetch=excluded.last_fetch,
                    last_success=excluded.last_success,
                    last_update=excluded.last_update
                """,
                (source_principale, now, now, now),
            )

            # Validation définitive des transactions en base
            conn.commit()

    def _fetch_forecast(self, days: int = 7) -> pd.DataFrame:
        """
        Récupère les prévisions via l'API Open-Meteo.

        Args:
            days (int): Nombre de jours de prévision.

        Returns:
            pd.DataFrame: DataFrame normalisé des prévisions.
        """
        from kadi._sources import fetch_forecast
        from kadi._utils.network import fetch_with_retry

        attempts = CONFIG["weather"]["retry_attempts"]
        backoff = CONFIG["weather"]["retry_backoff_sec"]

        data_list = fetch_with_retry(
            fetch_forecast, attempts, backoff,
            lat=self.location.lat, lon=self.location.lon, days=days,
        )

        df = pd.DataFrame(data_list)
        return self._normalize(df)

    def _fetch_historical(self, days: int = 7, source: str = "both") -> pd.DataFrame:
        """
        Récupère l'historique météo en combinant CHIRPS et/ou Open-Meteo.

        La logique de fusion est la suivante :
        - 'openmeteo' : comportement V1 inchangé, Open-Meteo fournit tout.
        - 'chirps' : CHIRPS fournit les précipitations, Open-Meteo les
          températures. Repli automatique sur Open-Meteo si CHIRPS échoue.
        - 'both' : CHIRPS couvre la période historique, Open-Meteo complète
          les dates récentes non encore disponibles dans CHIRPS.

        Args:
            days (int): Nombre de jours d'historique à récupérer.
            source (str): Source de précipitation à utiliser.

        Returns:
            pd.DataFrame: DataFrame normalisé avec les données historiques.
        """
        from datetime import date as date_type

        # Calcul des bornes de la plage demandée
        aujourd_hui = datetime.now().date()
        date_debut = aujourd_hui - timedelta(days=days)
        months = max(1, (days + 29) // 30)

        # Récupération des températures via Open-Meteo (toujours nécessaire)
        from kadi._sources.open_meteo import fetch_historical
        from kadi._utils.network import fetch_with_retry

        attempts = CONFIG["weather"]["retry_attempts"]
        backoff = CONFIG["weather"]["retry_backoff_sec"]

        om_list = fetch_with_retry(
            fetch_historical, attempts, backoff,
            lat=self.location.lat, lon=self.location.lon,
            months_back=months,
        )
        df_om = pd.DataFrame(om_list)
        df_om = self._normalize(df_om)
        # Marquage explicite de la source Open-Meteo pour la précipitation
        df_om["data_source"] = "open-meteo"

        # Mode Open-Meteo uniquement : comportement V1 inchangé
        if source == "openmeteo":
            return df_om

        # Récupération des précipitations CHIRPS
        from kadi._sources.chirps import fetch_historical_precipitation

        df_chirps = None
        try:
            df_chirps = fetch_historical_precipitation(
                lat=self.location.lat,
                lon=self.location.lon,
                start_date=date_debut.isoformat(),
                end_date=aujourd_hui.isoformat(),
            )
        except Exception as exc:
            # Repli global sur Open-Meteo avec message explicite
            import logging
            logging.getLogger(__name__).warning(
                "CHIRPS inaccessible pour toute la plage demandée. "
                "Les précipitations proviennent exclusivement d'Open-Meteo. "
                "Détail : %s",
                exc,
            )

        # Si CHIRPS n'a rien retourné : comportement identique à 'openmeteo'
        if df_chirps is None or df_chirps.empty:
            import logging
            logging.getLogger(__name__).warning(
                "Aucune donnée CHIRPS disponible (source='%s'). "
                "Repli automatique sur Open-Meteo.",
                source,
            )
            return df_om

        # Fusion CHIRPS (précipitations) + Open-Meteo (températures)
        df_chirps = df_chirps.set_index("date")
        df_chirps.index = pd.to_datetime(df_chirps.index)

        if source == "chirps":
            # CHIRPS fournit les précipitations, Open-Meteo les températures
            df_fusion = df_om.copy()
            dates_chirps = df_chirps.index
            masque = df_fusion.index.isin(dates_chirps)
            df_fusion.loc[masque, "precipitation"] = df_chirps.loc[
                df_chirps.index.isin(df_fusion.index), "precipitation"
            ].values
            df_fusion["data_source"] = "open-meteo"
            df_fusion.loc[masque, "data_source"] = "chirps"
            return df_fusion

        # Mode 'both' : CHIRPS pour la période couverte, Open-Meteo pour le reste
        df_fusion = df_om.copy()
        dates_chirps = df_chirps.index
        masque_chirps = df_fusion.index.isin(dates_chirps)
        df_fusion.loc[masque_chirps, "precipitation"] = df_chirps.loc[
            df_chirps.index.isin(df_fusion.index), "precipitation"
        ].values
        df_fusion.loc[masque_chirps, "data_source"] = "chirps"

        return df_fusion

    def _normalize(self, raw: pd.DataFrame) -> pd.DataFrame:
        """
        Normalise les données brutes retournées par une source météo.

        Applique dans l'ordre :
        1. Conversion de la colonne 'date' en index DatetimeIndex.
        2. Filtrage des valeurs aberrantes (températures hors [-5, 55] degC,
           pluie négative).
        3. Calcul de la colonne 'data_quality' (ratio de colonnes critiques
           renseignées).
        4. Interpolation linéaire sur les lacunes courtes (max 3 jours).
        5. Remplissage résiduel (précipitation à 0, temperature_mean).

        Args:
            raw (pd.DataFrame): DataFrame brut retourné par la source de données.

        Returns:
            pd.DataFrame: DataFrame normalisé avec l'index en date.
        """
        if raw.empty:
            return raw

        df = raw.copy()

        # 1. Conversion et indexation par date
        if "date" in df.columns:
            df["date"] = pd.to_datetime(df["date"])
            df.set_index("date", inplace=True)

        # Tri chronologique avant interpolation
        df = df.sort_index()

        # 2. Filtrage des valeurs aberrantes de température
        for col in ("temperature_min", "temperature_max"):
            if col in df.columns:
                masque_aberrant = (df[col] < -5.0) | (df[col] > 55.0)
                df.loc[masque_aberrant, col] = np.nan

        # Précipitation négative remise à zéro (impossible physiquement)
        if "precipitation" in df.columns:
            df.loc[df["precipitation"] < 0.0, "precipitation"] = 0.0

        # 3. Colonne data_quality : proportion de colonnes critiques renseignées
        cols_critiques = [
            c for c in ("temperature_min", "temperature_max", "precipitation")
            if c in df.columns
        ]
        if cols_critiques:
            ratio_manquant = df[cols_critiques].isna().mean(axis=1)
            df["data_quality"] = (1.0 - ratio_manquant).round(2)
        else:
            df["data_quality"] = 1.0

        # 4. Interpolation linéaire pour les lacunes courtes (max 3 jours)
        for col in cols_critiques:
            if df[col].isna().any():
                df[col] = df[col].interpolate(
                    method="linear", limit=3, limit_direction="both"
                )

        # 5. Remplissages résiduels après interpolation
        if "precipitation" in df.columns:
            # Toute lacune restante en pluie est supposée nulle (0 mm)
            df["precipitation"] = df["precipitation"].fillna(0.0)

        # Garantit la présence de temperature_mean (logique centralisée)
        df = WeatherLoader._unify_temp(df)

        return df

    # ----------------------------------------------------------------
    # Méthodes et attributs dépréciés (anciens noms)
    # ----------------------------------------------------------------

    def fetch_forecast(self, days: int = 7, force_refresh: bool = False) -> pd.DataFrame:
        """
        Ancienne méthode publique. Dépréciée depuis v1.2.0.

        Utilisez ``get_forecast(days, refresh)`` à la place.

        Args:
            days (int): Nombre de jours de prévision.
            force_refresh (bool): Ancien nom de ``refresh``.

        Returns:
            pd.DataFrame: DataFrame des prévisions.
        """
        warnings.warn(
            "WeatherLoader.fetch_forecast() est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez get_forecast() à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.get_forecast(days=days, refresh=force_refresh)

    def fetch_historical(
        self,
        months_back: int = 120,
        force_refresh: bool = False,
        source: str = None,
    ) -> pd.DataFrame:
        """
        Ancienne méthode publique. Dépréciée depuis v1.2.0.

        Utilisez ``get_historical(months, refresh, source)`` à la place.

        Args:
            months_back (int): Ancien nom de ``months``.
            force_refresh (bool): Ancien nom de ``refresh``.
            source (str): Source de données.

        Returns:
            pd.DataFrame: DataFrame historique.
        """
        warnings.warn(
            "WeatherLoader.fetch_historical() est obsolète et sera supprimé "
            "dans KadiPy v2.0. Utilisez get_historical() à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.get_historical(months=months_back, refresh=force_refresh, source=source)

    @property
    def forecast_data(self) -> Optional[pd.DataFrame]:
        """Ancien nom de ``forecast``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "WeatherLoader.forecast_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader.forecast à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.forecast

    @forecast_data.setter
    def forecast_data(self, value: Optional[pd.DataFrame]) -> None:
        """Ancien setter de ``forecast``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "WeatherLoader.forecast_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader.forecast à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.forecast = value

    @property
    def historical_data(self) -> Optional[pd.DataFrame]:
        """Ancien nom de ``historical``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "WeatherLoader.historical_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader.historical à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.historical

    @historical_data.setter
    def historical_data(self, value: Optional[pd.DataFrame]) -> None:
        """Ancien setter de ``historical``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "WeatherLoader.historical_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader.historical à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.historical = value

    @property
    def data_source(self) -> str:
        """Ancien nom de ``source``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "WeatherLoader.data_source est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader.source à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.source

    @data_source.setter
    def data_source(self, value: str) -> None:
        """Ancien setter de ``source``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "WeatherLoader.data_source est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader.source à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.source = value

    # ----------------------------------------------------------------
    # Méthodes privées dépréciées (anciens noms internes)
    # ----------------------------------------------------------------

    def _normalize_data(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Ancien nom de ``_normalize``. Déprécié depuis v1.2.0.

        Args:
            df (pd.DataFrame): DataFrame à normaliser.

        Returns:
            pd.DataFrame: DataFrame normalisé.
        """
        warnings.warn(
            "WeatherLoader._normalize_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader._normalize à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self._normalize(df)

    def _get_from_cache(self, start: str, end: str) -> pd.DataFrame:
        """
        Ancien nom de ``_from_cache``. Déprécié depuis v1.2.0.

        Args:
            start (str): Date de début ISO.
            end (str): Date de fin ISO.

        Returns:
            pd.DataFrame: Données en cache.
        """
        warnings.warn(
            "WeatherLoader._get_from_cache est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader._from_cache à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self._from_cache(start, end)

    def _save_to_cache(self, data: pd.DataFrame, kind: str) -> None:
        """
        Ancien nom de ``_to_cache``. Déprécié depuis v1.2.0.

        Args:
            data (pd.DataFrame): Données à sauvegarder.
            kind (str): Type de données.
        """
        warnings.warn(
            "WeatherLoader._save_to_cache est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader._to_cache à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self._to_cache(data, kind)

    def _fetch_forecast_data(self, days: int = 7) -> pd.DataFrame:
        """
        Ancien nom de ``_fetch_forecast``. Déprécié depuis v1.2.0.

        Args:
            days (int): Jours de prévision.

        Returns:
            pd.DataFrame: Données prévues.
        """
        warnings.warn(
            "WeatherLoader._fetch_forecast_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader._fetch_forecast à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self._fetch_forecast(days=days)

    def _fetch_historical_data(self, days: int = 7, source: str = "both") -> pd.DataFrame:
        """
        Ancien nom de ``_fetch_historical``. Déprécié depuis v1.2.0.

        Args:
            days (int): Jours d'historique.
            source (str): Source de données.

        Returns:
            pd.DataFrame: Données historiques.
        """
        warnings.warn(
            "WeatherLoader._fetch_historical_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez WeatherLoader._fetch_historical à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self._fetch_historical(days=days, source=source)

forecast_data property writable

forecast_data: Optional[DataFrame]

Ancien nom de forecast. Déprécié depuis v1.2.0.

historical_data property writable

historical_data: Optional[DataFrame]

Ancien nom de historical. Déprécié depuis v1.2.0.

data_source property writable

data_source: str

Ancien nom de source. Déprécié depuis v1.2.0.

__init__

__init__(location: Location, cache_dir: str = None)

Initialise le gestionnaire de données pour une localisation donnée.

Parameters:

Name Type Description Default
location Location

Instance de la classe Location.

required
cache_dir str

Ignoré car on utilise kadi.cache (SQLite global).

None
Source code in kadi/weather/data.py
def __init__(self, location: Location, cache_dir: str = None):
    """
    Initialise le gestionnaire de données pour une localisation donnée.

    Args:
        location (Location): Instance de la classe Location.
        cache_dir (str): Ignoré car on utilise kadi.cache (SQLite global).
    """
    self.location = location
    # Données en mémoire (chargées à la demande)
    self.forecast: Optional[pd.DataFrame] = None
    self.historical: Optional[pd.DataFrame] = None
    self.source: str = "none"

    # S'assure que la base de données et les tables existent
    init_db()

get_forecast

get_forecast(days: int = 7, refresh: bool = False) -> pd.DataFrame

Récupère les prévisions météorologiques en vérifiant d'abord le cache.

Parameters:

Name Type Description Default
days int

Nombre de jours de prévision à récupérer.

7
refresh bool

Si True, ignore le cache et force le rechargement.

False

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame indexé par date avec les prévisions.

Raises:

Type Description
OfflineError

Si l'API est inaccessible et qu'aucun cache n'existe.

Source code in kadi/weather/data.py
def get_forecast(self, days: int = 7, refresh: bool = False) -> pd.DataFrame:
    """
    Récupère les prévisions météorologiques en vérifiant d'abord le cache.

    Args:
        days (int): Nombre de jours de prévision à récupérer.
        refresh (bool): Si True, ignore le cache et force le rechargement.

    Returns:
        pd.DataFrame: DataFrame indexé par date avec les prévisions.

    Raises:
        OfflineError: Si l'API est inaccessible et qu'aucun cache n'existe.
    """
    today = datetime.now().date()
    end_date = today + timedelta(days=days - 1)

    if not refresh:
        # Vérification du cache avant tout appel API
        cached_data = self._from_cache(today.isoformat(), end_date.isoformat())
        if not cached_data.empty and len(cached_data) >= days:
            fetched_at = pd.to_datetime(cached_data["fetched_at"]).max()
            cache_ttl = timedelta(
                hours=CONFIG["weather"]["cache_ttl_forecast_hours"]
            )
            if datetime.now() - fetched_at < cache_ttl:
                cached_data = cached_data.head(days)
                self.forecast = cached_data
                self.source = "cached"
                return cached_data

    # Appel API
    try:
        df = self._fetch_forecast(days=days)
        self._to_cache(df, "forecast")
    except Exception as e:
        # Repli sur le cache si disponible
        if "cached_data" in locals() and not cached_data.empty:
            cached_data = cached_data.head(days)
            self.forecast = cached_data
            self.source = "cached_offline"
            return cached_data
        raise OfflineError(
            "Impossible de récupérer les prévisions et aucun cache "
            f"n'est disponible : {e}"
        )

    df = df.head(days)
    self.forecast = df
    self.source = "open-meteo"
    return df

get_historical

get_historical(months: int = 120, refresh: bool = False, source: str = None) -> pd.DataFrame

Récupère les données historiques en vérifiant d'abord le cache SQLite.

Parameters:

Name Type Description Default
months int

Nombre de mois d'historique à récupérer.

120
refresh bool

Si True, ignore le cache et force le rechargement.

False
source str

Source de précipitation. Valeurs acceptées : 'chirps' (CHIRPS uniquement, repli Open-Meteo si indisponible), 'openmeteo' (Open-Meteo uniquement, comportement V1.0), 'both' (CHIRPS pour l'historique long, Open-Meteo pour le récent). Si None, utilise CONFIG.

None

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame indexé par date avec l'historique météo.

Raises:

Type Description
OfflineError

Si l'API est inaccessible et qu'aucun cache n'existe.

Source code in kadi/weather/data.py
def get_historical(
    self,
    months: int = 120,
    refresh: bool = False,
    source: str = None,
) -> pd.DataFrame:
    """
    Récupère les données historiques en vérifiant d'abord le cache SQLite.

    Args:
        months (int): Nombre de mois d'historique à récupérer.
        refresh (bool): Si True, ignore le cache et force le rechargement.
        source (str): Source de précipitation. Valeurs acceptées :
            'chirps' (CHIRPS uniquement, repli Open-Meteo si indisponible),
            'openmeteo' (Open-Meteo uniquement, comportement V1.0),
            'both' (CHIRPS pour l'historique long, Open-Meteo pour le
            récent). Si None, utilise CONFIG.

    Returns:
        pd.DataFrame: DataFrame indexé par date avec l'historique météo.

    Raises:
        OfflineError: Si l'API est inaccessible et qu'aucun cache n'existe.
    """
    # Résolution de la source par défaut depuis la configuration
    if source is None:
        source = CONFIG["weather"]["chirps"]["source_default"]

    days = months * 30
    today = datetime.now().date()
    start_date = today - timedelta(days=days)

    if not refresh:
        # Vérification du cache (tolérance de 5 jours)
        cached_data = self._from_cache(start_date.isoformat(), today.isoformat())
        if not cached_data.empty and len(cached_data) >= (days - 5):
            fetched_at = pd.to_datetime(cached_data["fetched_at"]).max()
            cache_ttl = timedelta(
                days=CONFIG["weather"]["cache_ttl_historical_days"]
            )
            if datetime.now() - fetched_at < cache_ttl:
                self.historical = cached_data
                self.source = "cached"
                return cached_data

    # Appel API
    try:
        df = self._fetch_historical(days=days, source=source)
        self._to_cache(df, "historical")
    except Exception as e:
        if "cached_data" in locals() and not cached_data.empty:
            self.historical = cached_data
            self.source = "cached_offline"
            return cached_data
        raise OfflineError(
            "Impossible de récupérer l'historique météo et aucun cache "
            f"n'est disponible : {e}"
        )

    self.historical = df
    self.source = source
    return df

fetch_forecast

fetch_forecast(days: int = 7, force_refresh: bool = False) -> pd.DataFrame

Ancienne méthode publique. Dépréciée depuis v1.2.0.

Utilisez get_forecast(days, refresh) à la place.

Parameters:

Name Type Description Default
days int

Nombre de jours de prévision.

7
force_refresh bool

Ancien nom de refresh.

False

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame des prévisions.

Source code in kadi/weather/data.py
def fetch_forecast(self, days: int = 7, force_refresh: bool = False) -> pd.DataFrame:
    """
    Ancienne méthode publique. Dépréciée depuis v1.2.0.

    Utilisez ``get_forecast(days, refresh)`` à la place.

    Args:
        days (int): Nombre de jours de prévision.
        force_refresh (bool): Ancien nom de ``refresh``.

    Returns:
        pd.DataFrame: DataFrame des prévisions.
    """
    warnings.warn(
        "WeatherLoader.fetch_forecast() est obsolète et sera supprimé dans "
        "KadiPy v2.0. Utilisez get_forecast() à la place.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return self.get_forecast(days=days, refresh=force_refresh)

fetch_historical

fetch_historical(months_back: int = 120, force_refresh: bool = False, source: str = None) -> pd.DataFrame

Ancienne méthode publique. Dépréciée depuis v1.2.0.

Utilisez get_historical(months, refresh, source) à la place.

Parameters:

Name Type Description Default
months_back int

Ancien nom de months.

120
force_refresh bool

Ancien nom de refresh.

False
source str

Source de données.

None

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame historique.

Source code in kadi/weather/data.py
def fetch_historical(
    self,
    months_back: int = 120,
    force_refresh: bool = False,
    source: str = None,
) -> pd.DataFrame:
    """
    Ancienne méthode publique. Dépréciée depuis v1.2.0.

    Utilisez ``get_historical(months, refresh, source)`` à la place.

    Args:
        months_back (int): Ancien nom de ``months``.
        force_refresh (bool): Ancien nom de ``refresh``.
        source (str): Source de données.

    Returns:
        pd.DataFrame: DataFrame historique.
    """
    warnings.warn(
        "WeatherLoader.fetch_historical() est obsolète et sera supprimé "
        "dans KadiPy v2.0. Utilisez get_historical() à la place.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return self.get_historical(months=months_back, refresh=force_refresh, source=source)