meteostat 1.7.6__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. meteostat/__init__.py +32 -19
  2. meteostat/api/daily.py +76 -0
  3. meteostat/api/hourly.py +80 -0
  4. meteostat/api/interpolate.py +240 -0
  5. meteostat/api/inventory.py +59 -0
  6. meteostat/api/merge.py +103 -0
  7. meteostat/api/monthly.py +73 -0
  8. meteostat/api/normals.py +144 -0
  9. meteostat/api/point.py +30 -0
  10. meteostat/api/stations.py +234 -0
  11. meteostat/api/timeseries.py +334 -0
  12. meteostat/core/cache.py +212 -59
  13. meteostat/core/config.py +158 -0
  14. meteostat/core/data.py +199 -0
  15. meteostat/core/logger.py +9 -0
  16. meteostat/core/network.py +82 -0
  17. meteostat/core/parameters.py +112 -0
  18. meteostat/core/providers.py +184 -0
  19. meteostat/core/schema.py +170 -0
  20. meteostat/core/validator.py +38 -0
  21. meteostat/enumerations.py +149 -0
  22. meteostat/interpolation/idw.py +120 -0
  23. meteostat/interpolation/lapserate.py +91 -0
  24. meteostat/interpolation/nearest.py +31 -0
  25. meteostat/parameters.py +354 -0
  26. meteostat/providers/dwd/climat.py +166 -0
  27. meteostat/providers/dwd/daily.py +144 -0
  28. meteostat/providers/dwd/hourly.py +218 -0
  29. meteostat/providers/dwd/monthly.py +138 -0
  30. meteostat/providers/dwd/mosmix.py +351 -0
  31. meteostat/providers/dwd/poi.py +117 -0
  32. meteostat/providers/dwd/shared.py +155 -0
  33. meteostat/providers/eccc/daily.py +87 -0
  34. meteostat/providers/eccc/hourly.py +104 -0
  35. meteostat/providers/eccc/monthly.py +66 -0
  36. meteostat/providers/eccc/shared.py +45 -0
  37. meteostat/providers/index.py +496 -0
  38. meteostat/providers/meteostat/daily.py +65 -0
  39. meteostat/providers/meteostat/daily_derived.py +110 -0
  40. meteostat/providers/meteostat/hourly.py +66 -0
  41. meteostat/providers/meteostat/monthly.py +45 -0
  42. meteostat/providers/meteostat/monthly_derived.py +106 -0
  43. meteostat/providers/meteostat/shared.py +93 -0
  44. meteostat/providers/metno/forecast.py +186 -0
  45. meteostat/providers/noaa/ghcnd.py +228 -0
  46. meteostat/providers/noaa/isd_lite.py +142 -0
  47. meteostat/providers/noaa/metar.py +163 -0
  48. meteostat/typing.py +113 -0
  49. meteostat/utils/conversions.py +231 -0
  50. meteostat/utils/data.py +194 -0
  51. meteostat/utils/geo.py +28 -0
  52. meteostat/utils/parsers.py +168 -0
  53. meteostat/utils/types.py +113 -0
  54. meteostat/utils/validators.py +31 -0
  55. meteostat-2.0.0.dist-info/METADATA +134 -0
  56. meteostat-2.0.0.dist-info/RECORD +63 -0
  57. {meteostat-1.7.6.dist-info → meteostat-2.0.0.dist-info}/WHEEL +1 -2
  58. meteostat/core/loader.py +0 -103
  59. meteostat/core/warn.py +0 -34
  60. meteostat/enumerations/granularity.py +0 -22
  61. meteostat/interface/base.py +0 -39
  62. meteostat/interface/daily.py +0 -118
  63. meteostat/interface/hourly.py +0 -154
  64. meteostat/interface/meteodata.py +0 -210
  65. meteostat/interface/monthly.py +0 -109
  66. meteostat/interface/normals.py +0 -245
  67. meteostat/interface/point.py +0 -143
  68. meteostat/interface/stations.py +0 -252
  69. meteostat/interface/timeseries.py +0 -237
  70. meteostat/series/aggregate.py +0 -48
  71. meteostat/series/convert.py +0 -28
  72. meteostat/series/count.py +0 -17
  73. meteostat/series/coverage.py +0 -20
  74. meteostat/series/fetch.py +0 -28
  75. meteostat/series/interpolate.py +0 -47
  76. meteostat/series/normalize.py +0 -76
  77. meteostat/series/stations.py +0 -22
  78. meteostat/units.py +0 -149
  79. meteostat/utilities/__init__.py +0 -0
  80. meteostat/utilities/aggregations.py +0 -37
  81. meteostat/utilities/endpoint.py +0 -33
  82. meteostat/utilities/helpers.py +0 -70
  83. meteostat/utilities/mutations.py +0 -89
  84. meteostat/utilities/validations.py +0 -30
  85. meteostat-1.7.6.dist-info/METADATA +0 -112
  86. meteostat-1.7.6.dist-info/RECORD +0 -39
  87. meteostat-1.7.6.dist-info/top_level.txt +0 -1
  88. /meteostat/{core → api}/__init__.py +0 -0
  89. /meteostat/{enumerations → interpolation}/__init__.py +0 -0
  90. /meteostat/{interface → providers}/__init__.py +0 -0
  91. /meteostat/{interface/interpolate.py → py.typed} +0 -0
  92. /meteostat/{series → utils}/__init__.py +0 -0
  93. {meteostat-1.7.6.dist-info → meteostat-2.0.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,91 @@
1
+ from itertools import combinations
2
+ from statistics import mean
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ from meteostat.api.timeseries import TimeSeries
8
+ from meteostat.core.config import config
9
+ from meteostat.enumerations import Parameter
10
+
11
+
12
+ def lapse_rate(ts: TimeSeries, parameter: Parameter = Parameter.TEMP) -> float | None:
13
+ """
14
+ Calculate the lapse rate (temperature gradient) in degrees Celsius per kilometer
15
+ based on temperature and elevation data from multiple stations.
16
+
17
+ Parameters
18
+ ----------
19
+ df : pd.DataFrame
20
+ DataFrame containing temperature and elevation data for multiple stations.
21
+
22
+ Returns
23
+ -------
24
+ float
25
+ Calculated lapse rate in degrees Celsius per kilometer.
26
+ """
27
+ df = ts.fetch(location=True)
28
+
29
+ if df is None or "elevation" not in df.columns or parameter not in df.columns:
30
+ return None
31
+
32
+ elev_by_station = df["elevation"].groupby(level="station").first()
33
+ temp_by_station = df[parameter].groupby(level="station").mean()
34
+
35
+ if len(elev_by_station) < 2 or len(temp_by_station) < 2:
36
+ return None
37
+
38
+ lapse_rates = []
39
+
40
+ for a, b in combinations(elev_by_station.index, 2):
41
+ if (
42
+ pd.isna(elev_by_station[a])
43
+ or pd.isna(elev_by_station[b])
44
+ or pd.isna(temp_by_station[a])
45
+ or pd.isna(temp_by_station[b])
46
+ or elev_by_station[a] == elev_by_station[b]
47
+ ):
48
+ continue
49
+
50
+ temp_diff = temp_by_station[a] - temp_by_station[b]
51
+ elev_diff = elev_by_station[a] - elev_by_station[b]
52
+
53
+ # multiply by -1 to get positive lapse rate for decreasing temp
54
+ # with increasing elevation
55
+ lapse_rate = (temp_diff / elev_diff) * 1000 * -1
56
+ lapse_rates.append(lapse_rate)
57
+
58
+ if not lapse_rates:
59
+ return None
60
+
61
+ return mean(lapse_rates)
62
+
63
+
64
+ def apply_lapse_rate(
65
+ df: pd.DataFrame, elevation: int, lapse_rate: float
66
+ ) -> pd.DataFrame:
67
+ """
68
+ Calculate approximate temperature at target elevation
69
+ using a given lapse rate.
70
+
71
+ Parameters
72
+ ----------
73
+ df : pd.DataFrame
74
+ DataFrame containing the data to be adjusted.
75
+ elevation : int
76
+ Target elevation in meters.
77
+ lapse_rate : float
78
+ Lapse rate (temperature gradient) in degrees Celsius per kilometer.
79
+
80
+ Returns
81
+ -------
82
+ pd.DataFrame
83
+ DataFrame with adjusted temperature values.
84
+ """
85
+ for col in config.lapse_rate_parameters:
86
+ if col in df.columns:
87
+ df.loc[df[col] != np.nan, col] = round(
88
+ df[col] + ((lapse_rate / 1000) * (df["elevation"] - elevation)), 1
89
+ )
90
+
91
+ return df
@@ -0,0 +1,31 @@
1
+ import pandas as pd
2
+
3
+ from meteostat.api.point import Point
4
+ from meteostat.api.timeseries import TimeSeries
5
+
6
+
7
+ def nearest_neighbor(df: pd.DataFrame, ts: TimeSeries, _point: Point) -> pd.DataFrame:
8
+ """
9
+ Get nearest neighbor value for each record in a DataFrame.
10
+
11
+ Parameters
12
+ ----------
13
+ df : pd.DataFrame
14
+ DataFrame containing the data to be adjusted.
15
+ ts : TimeSeries
16
+ TimeSeries object containing the target data.
17
+ _point : Point
18
+ Point object representing the target location.
19
+
20
+ Returns
21
+ -------
22
+ pd.DataFrame
23
+ DataFrame with nearest neighbor values for each record.
24
+ """
25
+ df = (
26
+ df.sort_values("distance")
27
+ .groupby(pd.Grouper(level="time", freq=ts.freq))
28
+ .agg("first")
29
+ )
30
+
31
+ return df
@@ -0,0 +1,354 @@
1
+ """
2
+ Meteostat Parameter Definitions
3
+ """
4
+
5
+ from meteostat.enumerations import Granularity, Parameter, Unit
6
+ from meteostat.typing import ParameterSpec
7
+ from meteostat.utils.validators import maximum, minimum
8
+
9
+
10
+ DEFAULT_PARAMETERS = [
11
+ ParameterSpec(
12
+ id=Parameter.TEMP,
13
+ name="Air Temperature",
14
+ granularity=Granularity.HOURLY,
15
+ unit=Unit.CELSIUS,
16
+ dtype="Float64",
17
+ validators=[minimum(-100), maximum(65)],
18
+ ),
19
+ ParameterSpec(
20
+ id=Parameter.TEMP,
21
+ name="Mean Air Temperature",
22
+ granularity=Granularity.DAILY,
23
+ unit=Unit.CELSIUS,
24
+ dtype="Float64",
25
+ validators=[minimum(-100), maximum(65)],
26
+ ),
27
+ ParameterSpec(
28
+ id=Parameter.TEMP,
29
+ name="Mean Air Temperature",
30
+ granularity=Granularity.MONTHLY,
31
+ unit=Unit.CELSIUS,
32
+ dtype="Float64",
33
+ validators=[minimum(-100), maximum(65)],
34
+ ),
35
+ ParameterSpec(
36
+ id=Parameter.TEMP,
37
+ name="Mean Air Temperature",
38
+ granularity=Granularity.NORMALS,
39
+ unit=Unit.CELSIUS,
40
+ dtype="Float64",
41
+ validators=[minimum(-100), maximum(65)],
42
+ ),
43
+ ParameterSpec(
44
+ id=Parameter.TMIN,
45
+ name="Minimum Air Temperature",
46
+ granularity=Granularity.DAILY,
47
+ unit=Unit.CELSIUS,
48
+ dtype="Float64",
49
+ validators=[minimum(-100), maximum(65)],
50
+ ),
51
+ ParameterSpec(
52
+ id=Parameter.TMIN,
53
+ name="Mean Daily Minimum Air Temperature",
54
+ granularity=Granularity.MONTHLY,
55
+ unit=Unit.CELSIUS,
56
+ dtype="Float64",
57
+ validators=[minimum(-100), maximum(65)],
58
+ ),
59
+ ParameterSpec(
60
+ id=Parameter.TMIN,
61
+ name="Mean Daily Minimum Air Temperature",
62
+ granularity=Granularity.NORMALS,
63
+ unit=Unit.CELSIUS,
64
+ dtype="Float64",
65
+ validators=[minimum(-100), maximum(65)],
66
+ ),
67
+ ParameterSpec(
68
+ id=Parameter.TMAX,
69
+ name="Maximum Air Temperature",
70
+ granularity=Granularity.DAILY,
71
+ unit=Unit.CELSIUS,
72
+ dtype="Float64",
73
+ validators=[minimum(-100), maximum(65)],
74
+ ),
75
+ ParameterSpec(
76
+ id=Parameter.TMAX,
77
+ name="Mean Daily Maximum Air Temperature",
78
+ granularity=Granularity.MONTHLY,
79
+ unit=Unit.CELSIUS,
80
+ dtype="Float64",
81
+ validators=[minimum(-100), maximum(65)],
82
+ ),
83
+ ParameterSpec(
84
+ id=Parameter.TMAX,
85
+ name="Mean Daily Maximum Air Temperature",
86
+ granularity=Granularity.NORMALS,
87
+ unit=Unit.CELSIUS,
88
+ dtype="Float64",
89
+ validators=[minimum(-100), maximum(65)],
90
+ ),
91
+ ParameterSpec(
92
+ id=Parameter.TXMN,
93
+ name="Absolute Minimum Air Temperature",
94
+ granularity=Granularity.MONTHLY,
95
+ unit=Unit.CELSIUS,
96
+ dtype="Float64",
97
+ validators=[minimum(-100), maximum(65)],
98
+ ),
99
+ ParameterSpec(
100
+ id=Parameter.TXMN,
101
+ name="Absolute Minimum Air Temperature",
102
+ granularity=Granularity.NORMALS,
103
+ unit=Unit.CELSIUS,
104
+ dtype="Float64",
105
+ validators=[minimum(-100), maximum(65)],
106
+ ),
107
+ ParameterSpec(
108
+ id=Parameter.TXMX,
109
+ name="Absolute Maximum Air Temperature",
110
+ granularity=Granularity.MONTHLY,
111
+ unit=Unit.CELSIUS,
112
+ dtype="Float64",
113
+ validators=[minimum(-100), maximum(65)],
114
+ ),
115
+ ParameterSpec(
116
+ id=Parameter.TXMX,
117
+ name="Absolute Maximum Air Temperature",
118
+ granularity=Granularity.NORMALS,
119
+ unit=Unit.CELSIUS,
120
+ dtype="Float64",
121
+ validators=[minimum(-100), maximum(65)],
122
+ ),
123
+ ParameterSpec(
124
+ id=Parameter.DWPT,
125
+ name="Dew Point",
126
+ granularity=Granularity.HOURLY,
127
+ unit=Unit.CELSIUS,
128
+ dtype="Float64",
129
+ validators=[minimum(-100), maximum(65)],
130
+ ),
131
+ ParameterSpec(
132
+ id=Parameter.RHUM,
133
+ name="Relative Humidity",
134
+ granularity=Granularity.HOURLY,
135
+ unit=Unit.PERCENTAGE,
136
+ dtype="UInt8",
137
+ validators=[minimum(0), maximum(100)],
138
+ ),
139
+ ParameterSpec(
140
+ id=Parameter.RHUM,
141
+ name="Relative Humidity",
142
+ granularity=Granularity.DAILY,
143
+ unit=Unit.PERCENTAGE,
144
+ dtype="UInt8",
145
+ validators=[minimum(0), maximum(100)],
146
+ ),
147
+ ParameterSpec(
148
+ id=Parameter.RHUM,
149
+ name="Relative Humidity",
150
+ granularity=Granularity.MONTHLY,
151
+ unit=Unit.PERCENTAGE,
152
+ dtype="UInt8",
153
+ validators=[minimum(0), maximum(100)],
154
+ ),
155
+ ParameterSpec(
156
+ id=Parameter.RHUM,
157
+ name="Relative Humidity",
158
+ granularity=Granularity.NORMALS,
159
+ unit=Unit.PERCENTAGE,
160
+ dtype="UInt8",
161
+ validators=[minimum(0), maximum(100)],
162
+ ),
163
+ ParameterSpec(
164
+ id=Parameter.PRCP,
165
+ name="Total Precipitation",
166
+ granularity=Granularity.HOURLY,
167
+ unit=Unit.MILLIMETERS,
168
+ dtype="Float64",
169
+ validators=[minimum(0), maximum(350)],
170
+ ),
171
+ ParameterSpec(
172
+ id=Parameter.PRCP,
173
+ name="Total Precipitation",
174
+ granularity=Granularity.DAILY,
175
+ unit=Unit.MILLIMETERS,
176
+ dtype="Float64",
177
+ validators=[minimum(0), maximum(2000)],
178
+ ),
179
+ ParameterSpec(
180
+ id=Parameter.PRCP,
181
+ name="Total Precipitation",
182
+ granularity=Granularity.MONTHLY,
183
+ unit=Unit.MILLIMETERS,
184
+ dtype="Float64",
185
+ validators=[minimum(0), maximum(10000)],
186
+ ),
187
+ ParameterSpec(
188
+ id=Parameter.PRCP,
189
+ name="Total Precipitation",
190
+ granularity=Granularity.NORMALS,
191
+ unit=Unit.MILLIMETERS,
192
+ dtype="Float64",
193
+ validators=[minimum(0), maximum(10000)],
194
+ ),
195
+ ParameterSpec(
196
+ id=Parameter.SNOW,
197
+ name="Snowfall",
198
+ granularity=Granularity.HOURLY,
199
+ unit=Unit.CENTIMETERS,
200
+ dtype="UInt8",
201
+ validators=[minimum(0), maximum(10000)],
202
+ ),
203
+ ParameterSpec(
204
+ id=Parameter.SNWD,
205
+ name="Snow Depth",
206
+ granularity=Granularity.HOURLY,
207
+ unit=Unit.CENTIMETERS,
208
+ dtype="UInt16",
209
+ validators=[minimum(0), maximum(1200)],
210
+ ),
211
+ ParameterSpec(
212
+ id=Parameter.SNWD,
213
+ name="Snow Depth",
214
+ granularity=Granularity.DAILY,
215
+ unit=Unit.CENTIMETERS,
216
+ dtype="UInt16",
217
+ validators=[minimum(0), maximum(1200)],
218
+ ),
219
+ ParameterSpec(
220
+ id=Parameter.WDIR,
221
+ name="Wind Direction",
222
+ granularity=Granularity.HOURLY,
223
+ unit=Unit.DEGREES,
224
+ dtype="UInt16",
225
+ validators=[minimum(0), maximum(360)],
226
+ ),
227
+ ParameterSpec(
228
+ id=Parameter.WSPD,
229
+ name="Wind Speed",
230
+ granularity=Granularity.HOURLY,
231
+ unit=Unit.KMH,
232
+ dtype="Float64",
233
+ validators=[minimum(0), maximum(250)],
234
+ ),
235
+ ParameterSpec(
236
+ id=Parameter.WSPD,
237
+ name="Average Wind Speed",
238
+ granularity=Granularity.DAILY,
239
+ unit=Unit.KMH,
240
+ dtype="Float64",
241
+ validators=[minimum(0), maximum(150)],
242
+ ),
243
+ ParameterSpec(
244
+ id=Parameter.WPGT,
245
+ name="Peak Wind Gust",
246
+ granularity=Granularity.HOURLY,
247
+ unit=Unit.KMH,
248
+ dtype="Float64",
249
+ validators=[minimum(0), maximum(500)],
250
+ ),
251
+ ParameterSpec(
252
+ id=Parameter.WPGT,
253
+ name="Peak Wind Gust",
254
+ granularity=Granularity.DAILY,
255
+ unit=Unit.KMH,
256
+ dtype="Float64",
257
+ validators=[minimum(0), maximum(500)],
258
+ ),
259
+ ParameterSpec(
260
+ id=Parameter.PRES,
261
+ name="Air Pressure (MSL)",
262
+ granularity=Granularity.HOURLY,
263
+ unit=Unit.HPA,
264
+ dtype="Float64",
265
+ validators=[minimum(850), maximum(1090)],
266
+ ),
267
+ ParameterSpec(
268
+ id=Parameter.PRES,
269
+ name="Average Air Pressure (MSL)",
270
+ granularity=Granularity.DAILY,
271
+ unit=Unit.HPA,
272
+ dtype="Float64",
273
+ validators=[minimum(850), maximum(1090)],
274
+ ),
275
+ ParameterSpec(
276
+ id=Parameter.PRES,
277
+ name="Average Air Pressure (MSL)",
278
+ granularity=Granularity.MONTHLY,
279
+ unit=Unit.HPA,
280
+ dtype="Float64",
281
+ validators=[minimum(850), maximum(1090)],
282
+ ),
283
+ ParameterSpec(
284
+ id=Parameter.PRES,
285
+ name="Average Air Pressure (MSL)",
286
+ granularity=Granularity.NORMALS,
287
+ unit=Unit.HPA,
288
+ dtype="Float64",
289
+ validators=[minimum(850), maximum(1090)],
290
+ ),
291
+ ParameterSpec(
292
+ id=Parameter.TSUN,
293
+ name="Sunshine Duration",
294
+ granularity=Granularity.HOURLY,
295
+ unit=Unit.MINUTES,
296
+ dtype="UInt8",
297
+ validators=[minimum(0), maximum(60)],
298
+ ),
299
+ ParameterSpec(
300
+ id=Parameter.TSUN,
301
+ name="Sunshine Duration",
302
+ granularity=Granularity.DAILY,
303
+ unit=Unit.MINUTES,
304
+ dtype="UInt16",
305
+ validators=[minimum(0), maximum(1440)],
306
+ ),
307
+ ParameterSpec(
308
+ id=Parameter.TSUN,
309
+ name="Sunshine Duration",
310
+ granularity=Granularity.MONTHLY,
311
+ unit=Unit.MINUTES,
312
+ dtype="UInt16",
313
+ validators=[minimum(0), maximum(44640)],
314
+ ),
315
+ ParameterSpec(
316
+ id=Parameter.TSUN,
317
+ name="Sunshine Duration",
318
+ granularity=Granularity.NORMALS,
319
+ unit=Unit.MINUTES,
320
+ dtype="UInt16",
321
+ validators=[minimum(0), maximum(44640)],
322
+ ),
323
+ ParameterSpec(
324
+ id=Parameter.CLDC,
325
+ name="Cloud Cover",
326
+ granularity=Granularity.HOURLY,
327
+ unit=Unit.OKTAS,
328
+ dtype="UInt8",
329
+ validators=[minimum(0), maximum(8)],
330
+ ),
331
+ ParameterSpec(
332
+ id=Parameter.CLDC,
333
+ name="Average Cloud Cover",
334
+ granularity=Granularity.DAILY,
335
+ unit=Unit.OKTAS,
336
+ dtype="UInt8",
337
+ validators=[minimum(0), maximum(8)],
338
+ ),
339
+ ParameterSpec(
340
+ id=Parameter.VSBY,
341
+ name="Visibility",
342
+ granularity=Granularity.HOURLY,
343
+ unit=Unit.METERS,
344
+ dtype="UInt16",
345
+ validators=[minimum(0)],
346
+ ),
347
+ ParameterSpec(
348
+ id=Parameter.COCO,
349
+ name="Weather Condition Code",
350
+ granularity=Granularity.HOURLY,
351
+ dtype="UInt8",
352
+ validators=[minimum(0), maximum(27)],
353
+ ),
354
+ ]
@@ -0,0 +1,166 @@
1
+ """
2
+ DWD Global CLIMAT Data
3
+ """
4
+
5
+ from datetime import datetime
6
+ from ftplib import FTP
7
+ from io import BytesIO
8
+ from typing import List, Optional
9
+
10
+ import pandas as pd
11
+
12
+ from meteostat.core.logger import logger
13
+ from meteostat.core.config import config
14
+ from meteostat.enumerations import TTL, Parameter
15
+ from meteostat.typing import ProviderRequest, Station
16
+ from meteostat.core.cache import cache_service
17
+ from meteostat.providers.dwd.shared import get_ftp_connection
18
+
19
+ # Constants
20
+ BASE_DIR = "/climate_environment/CDC/observations_global/CLIMAT/monthly/qc/"
21
+
22
+ # Monthly column stubnames mapping template
23
+ MONTHS_MAP = {
24
+ "jan": 1,
25
+ "feb": 2,
26
+ "mrz": 3,
27
+ "apr": 4,
28
+ "mai": 5,
29
+ "jun": 6,
30
+ "jul": 7,
31
+ "aug": 8,
32
+ "sep": 9,
33
+ "okt": 10,
34
+ "nov": 11,
35
+ "dez": 12,
36
+ }
37
+
38
+ # Parameter directory configurations
39
+ PARAMETERS = [
40
+ ("precipitation_total", Parameter.PRCP),
41
+ ("air_temperature_mean", Parameter.TEMP),
42
+ ("air_temperature_mean_of_daily_max", Parameter.TMAX),
43
+ ("air_temperature_mean_of_daily_min", Parameter.TMIN),
44
+ ("air_temperature_absolute_max", Parameter.TXMX),
45
+ ("air_temperature_absolute_min", Parameter.TXMN),
46
+ ("mean_sea_level_pressure", Parameter.PRES),
47
+ ("sunshine_duration", Parameter.TSUN),
48
+ ]
49
+
50
+ # Precomputed parameter configs with stubnames
51
+ PARAMETER_CONFIGS = {
52
+ param: {
53
+ "dir": dir_name,
54
+ "stubnames": {
55
+ "jahr": "year",
56
+ **{
57
+ month: f"{param}{i + 1}"
58
+ for i, (month, _) in enumerate(MONTHS_MAP.items())
59
+ },
60
+ },
61
+ }
62
+ for dir_name, param in PARAMETERS
63
+ }
64
+
65
+
66
+ def find_file(ftp: FTP, mode: str, directory: str, search_term: str) -> Optional[str]:
67
+ """
68
+ Find a file in the FTP directory matching a pattern.
69
+ """
70
+ try:
71
+ ftp.cwd(f"{BASE_DIR}{directory}/{mode}")
72
+ matches = [f for f in ftp.nlst() if search_term in f]
73
+ return sorted(matches)[-1] if matches else None
74
+ except Exception:
75
+ logger.debug("Error while searching for file", exc_info=True)
76
+ return None
77
+
78
+
79
+ @cache_service.cache(TTL.WEEK, "pickle")
80
+ def get_df(parameter: str, mode: str, station_code: str) -> Optional[pd.DataFrame]:
81
+ """
82
+ Download and parse a CLIMAT dataset from DWD FTP.
83
+ """
84
+ param_config = PARAMETER_CONFIGS.get(parameter)
85
+ if not param_config:
86
+ logger.debug(f"Unknown parameter '{parameter}'")
87
+ return None
88
+
89
+ ftp = get_ftp_connection()
90
+ search_term = station_code if mode == "recent" else f"{station_code}_"
91
+ remote_file = find_file(ftp, mode, param_config["dir"], search_term)
92
+
93
+ if not remote_file:
94
+ logger.debug(
95
+ f"No file found for parameter '{parameter}', mode '{mode}', station '{station_code}'"
96
+ )
97
+ return None
98
+
99
+ buffer = BytesIO()
100
+ ftp.retrbinary(f"RETR {remote_file}", buffer.write)
101
+ ftp.close()
102
+
103
+ buffer.seek(0)
104
+ df = pd.read_csv(buffer, sep=";").rename(columns=lambda col: col.strip().lower())
105
+ df.rename(columns=param_config["stubnames"], inplace=True)
106
+
107
+ # Convert wide to long format
108
+ df = pd.wide_to_long(
109
+ df, stubnames=parameter, i="year", j="month", sep="", suffix="\\d+"
110
+ ).reset_index()
111
+
112
+ if parameter == Parameter.TSUN:
113
+ df[Parameter.TSUN] *= 60 # convert hours to minutes
114
+
115
+ # Create datetime index
116
+ df["time"] = pd.to_datetime(
117
+ df["year"].astype(str) + "-" + df["month"].astype(str).str.zfill(2) + "-01"
118
+ )
119
+
120
+ return df.drop(columns=["year", "month"]).set_index("time")
121
+
122
+
123
+ def get_parameter(
124
+ parameter: str, modes: List[str], station: Station
125
+ ) -> Optional[pd.DataFrame]:
126
+ """
127
+ Fetch and merge data for a parameter over multiple modes (e.g., recent, historical).
128
+ """
129
+ try:
130
+ station_code = station.identifiers.get("wmo")
131
+ if not station_code:
132
+ return None
133
+
134
+ datasets = [get_df(parameter, mode, station_code) for mode in modes]
135
+ datasets = [df for df in datasets if df is not None]
136
+
137
+ if not datasets:
138
+ return None
139
+
140
+ return pd.concat(datasets).loc[lambda df: ~df.index.duplicated(keep="first")]
141
+ except Exception as e:
142
+ logger.warning(
143
+ f"Failed to fetch data for parameter '{parameter}': {e}", exc_info=True
144
+ )
145
+ return None
146
+
147
+
148
+ def fetch(req: ProviderRequest) -> pd.DataFrame:
149
+ """
150
+ Entry point to fetch all requested parameters for a station query.
151
+ """
152
+ station_code = req.station.identifiers.get("wmo")
153
+ if not station_code:
154
+ return pd.DataFrame()
155
+
156
+ modes = ["historical"]
157
+ if (datetime.now() - req.end).days < 5 * 365:
158
+ modes.append("recent")
159
+
160
+ data_frames = [
161
+ get_parameter(param.value, config.dwd_climat_modes or modes, req.station)
162
+ for param in req.parameters
163
+ if param in PARAMETER_CONFIGS
164
+ ]
165
+
166
+ return pd.concat([df for df in data_frames if df is not None], axis=1)