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,351 @@
1
+ """
2
+ DWD MOSMIX data provider
3
+
4
+ Parameters: https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_parameteruebersicht.pdf?__blob=publicationFile&v=3
5
+ """
6
+
7
+ import re
8
+ from io import BytesIO
9
+ from typing import Optional
10
+ from datetime import datetime
11
+ from zipfile import ZipFile
12
+ from lxml import etree # type: ignore
13
+
14
+ import pandas as pd
15
+
16
+ from meteostat.core.cache import cache_service
17
+ from meteostat.enumerations import TTL, Parameter
18
+ from meteostat.typing import ProviderRequest
19
+ from meteostat.utils.conversions import (
20
+ kelvin_to_celsius,
21
+ ms_to_kmh,
22
+ percentage_to_okta,
23
+ temp_dwpt_to_rhum,
24
+ )
25
+ from meteostat.core.network import network_service
26
+
27
+ ENDPOINT = "https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_L/single_stations/{station}/kml/MOSMIX_L_LATEST_{station}.kmz"
28
+ COCO_MAP = {
29
+ "0": 1,
30
+ "1": 2,
31
+ "2": 3,
32
+ "3": 4,
33
+ "45": 5,
34
+ "49": 5,
35
+ "61": 7,
36
+ "63": 8,
37
+ "65": 9,
38
+ "51": 7,
39
+ "53": 8,
40
+ "55": 9,
41
+ "68": 12,
42
+ "69": 13,
43
+ "71": 14,
44
+ "73": 15,
45
+ "75": 16,
46
+ "80": 17,
47
+ "81": 18,
48
+ "82": 18,
49
+ "83": 19,
50
+ "84": 20,
51
+ "85": 21,
52
+ "86": 22,
53
+ "66": 10,
54
+ "67": 11,
55
+ "56": 10,
56
+ "57": 11,
57
+ "95": 25,
58
+ }
59
+
60
+
61
+ def get_coco(code: str | int) -> Optional[int]:
62
+ """
63
+ Map DWD MOSMIX weather condition codes to Meteostat condicodes
64
+ """
65
+ return COCO_MAP.get(str(code))
66
+
67
+
68
+ @cache_service.cache(TTL.HOUR, "pickle")
69
+ def get_df(station: str) -> Optional[pd.DataFrame]:
70
+ # Fetch the KMZ file data in memory
71
+ response = network_service.get(ENDPOINT.format(station=station))
72
+ kmz_data = BytesIO(response.content)
73
+
74
+ # KMZ -> KML in memory
75
+ with ZipFile(kmz_data, "r") as kmz:
76
+ with kmz.open(kmz.infolist()[0].filename, "r") as raw:
77
+ kml = raw.read()
78
+
79
+ # Parse KML
80
+ tree = etree.fromstring(kml)
81
+
82
+ # Skip stale forecasts
83
+ issue_time = datetime.strptime(
84
+ tree.xpath(
85
+ "//kml:kml/kml:Document/kml:ExtendedData/"
86
+ + "dwd:ProductDefinition/dwd:IssueTime",
87
+ namespaces=tree.nsmap,
88
+ )[0].text,
89
+ "%Y-%m-%dT%H:%M:%S.%fZ",
90
+ )
91
+ if (datetime.now() - issue_time).total_seconds() > 25200:
92
+ return None
93
+
94
+ # Collect all time steps
95
+ timesteps = []
96
+ for step in tree.xpath(
97
+ "//kml:kml/kml:Document/kml:ExtendedData/dwd:ProductDefinition/"
98
+ + "dwd:ForecastTimeSteps/dwd:TimeStep",
99
+ namespaces=tree.nsmap,
100
+ ):
101
+ timesteps.append(step.text)
102
+
103
+ # COLLECT WEATHER DATA
104
+ # Each parameter is processed individually
105
+ data = {
106
+ "time": timesteps,
107
+ Parameter.TEMP: [],
108
+ Parameter.DWPT: [],
109
+ Parameter.PRCP: [],
110
+ Parameter.WDIR: [],
111
+ Parameter.WSPD: [],
112
+ Parameter.WPGT: [],
113
+ Parameter.TSUN: [],
114
+ Parameter.PRES: [],
115
+ Parameter.CLDC: [],
116
+ Parameter.VSBY: [],
117
+ Parameter.COCO: [],
118
+ }
119
+ placemark = tree.xpath(
120
+ "//kml:kml/kml:Document/kml:Placemark", namespaces=tree.nsmap
121
+ )[0]
122
+
123
+ # Pressure
124
+ for value in (
125
+ re.sub(
126
+ r"/\s+/",
127
+ " ",
128
+ placemark.xpath(
129
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="PPPP"]/dwd:value',
130
+ namespaces=tree.nsmap,
131
+ )[0].text,
132
+ )
133
+ .strip()
134
+ .split()
135
+ ):
136
+ data[Parameter.PRES].append(
137
+ float(value) / 100
138
+ if value.lstrip("-").replace(".", "", 1).isdigit()
139
+ else None
140
+ )
141
+
142
+ # Air temperature
143
+ for value in (
144
+ re.sub(
145
+ r"/\s+/",
146
+ " ",
147
+ placemark.xpath(
148
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="TTT"]/dwd:value',
149
+ namespaces=tree.nsmap,
150
+ )[0].text,
151
+ )
152
+ .strip()
153
+ .split()
154
+ ):
155
+ data[Parameter.TEMP].append(
156
+ kelvin_to_celsius(float(value))
157
+ if value.lstrip("-").replace(".", "", 1).isdigit()
158
+ else None
159
+ )
160
+
161
+ # Dew point
162
+ for value in (
163
+ re.sub(
164
+ r"/\s+/",
165
+ " ",
166
+ placemark.xpath(
167
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="Td"]/dwd:value',
168
+ namespaces=tree.nsmap,
169
+ )[0].text,
170
+ )
171
+ .strip()
172
+ .split()
173
+ ):
174
+ data[Parameter.DWPT].append(
175
+ kelvin_to_celsius(float(value))
176
+ if value.lstrip("-").replace(".", "", 1).isdigit()
177
+ else None
178
+ )
179
+
180
+ # Wind direction
181
+ for value in (
182
+ re.sub(
183
+ r"/\s+/",
184
+ " ",
185
+ placemark.xpath(
186
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="DD"]/dwd:value',
187
+ namespaces=tree.nsmap,
188
+ )[0].text,
189
+ )
190
+ .strip()
191
+ .split()
192
+ ):
193
+ data[Parameter.WDIR].append(
194
+ int(float(value))
195
+ if value.lstrip("-").replace(".", "", 1).isdigit()
196
+ else None
197
+ )
198
+
199
+ # Wind speed
200
+ for value in (
201
+ re.sub(
202
+ r"/\s+/",
203
+ " ",
204
+ placemark.xpath(
205
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="FF"]/dwd:value',
206
+ namespaces=tree.nsmap,
207
+ )[0].text,
208
+ )
209
+ .strip()
210
+ .split()
211
+ ):
212
+ data[Parameter.WSPD].append(
213
+ ms_to_kmh(float(value))
214
+ if value.lstrip("-").replace(".", "", 1).isdigit()
215
+ else None
216
+ )
217
+
218
+ # Peak wind gust
219
+ for value in (
220
+ re.sub(
221
+ r"/\s+/",
222
+ " ",
223
+ placemark.xpath(
224
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="FX1"]/dwd:value',
225
+ namespaces=tree.nsmap,
226
+ )[0].text,
227
+ )
228
+ .strip()
229
+ .split()
230
+ ):
231
+ data[Parameter.WPGT].append(
232
+ ms_to_kmh(float(value))
233
+ if value.lstrip("-").replace(".", "", 1).isdigit()
234
+ else None
235
+ )
236
+
237
+ # Weather condition
238
+ for value in (
239
+ re.sub(
240
+ r"/\s+/",
241
+ " ",
242
+ placemark.xpath(
243
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="ww"]/dwd:value',
244
+ namespaces=tree.nsmap,
245
+ )[0].text,
246
+ )
247
+ .strip()
248
+ .split()
249
+ ):
250
+ data[Parameter.COCO].append(
251
+ get_coco(int(float(value)))
252
+ if value.lstrip("-").replace(".", "", 1).isdigit()
253
+ else None
254
+ )
255
+
256
+ # Precipitation
257
+ for value in (
258
+ re.sub(
259
+ r"/\s+/",
260
+ " ",
261
+ placemark.xpath(
262
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="RR1c"]/dwd:value',
263
+ namespaces=tree.nsmap,
264
+ )[0].text,
265
+ )
266
+ .strip()
267
+ .split()
268
+ ):
269
+ data[Parameter.PRCP].append(
270
+ float(value) if value.lstrip("-").replace(".", "", 1).isdigit() else None
271
+ )
272
+
273
+ # Sunshine Duration
274
+ for value in (
275
+ re.sub(
276
+ r"/\s+/",
277
+ " ",
278
+ placemark.xpath(
279
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="SunD1"]/dwd:value',
280
+ namespaces=tree.nsmap,
281
+ )[0].text,
282
+ )
283
+ .strip()
284
+ .split()
285
+ ):
286
+ data[Parameter.TSUN].append(
287
+ float(value) / 60
288
+ if value.lstrip("-").replace(".", "", 1).isdigit()
289
+ else None
290
+ )
291
+
292
+ # Cloud Cover
293
+ for value in (
294
+ re.sub(
295
+ r"/\s+/",
296
+ " ",
297
+ placemark.xpath(
298
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="N"]/dwd:value',
299
+ namespaces=tree.nsmap,
300
+ )[0].text,
301
+ )
302
+ .strip()
303
+ .split()
304
+ ):
305
+ data[Parameter.CLDC].append(
306
+ percentage_to_okta(float(value))
307
+ if value.lstrip("-").replace(".", "", 1).isdigit()
308
+ else None
309
+ )
310
+
311
+ # Visibility
312
+ for value in (
313
+ re.sub(
314
+ r"/\s+/",
315
+ " ",
316
+ placemark.xpath(
317
+ 'kml:ExtendedData/dwd:Forecast[@dwd:elementName="VV"]/dwd:value',
318
+ namespaces=tree.nsmap,
319
+ )[0].text,
320
+ )
321
+ .strip()
322
+ .split()
323
+ ):
324
+ data[Parameter.VSBY].append(
325
+ float(value) if value.lstrip("-").replace(".", "", 1).isdigit() else None
326
+ )
327
+
328
+ # Convert data dict to DataFrame
329
+ df = pd.DataFrame.from_dict(data)
330
+
331
+ # Convert time strings to datetime
332
+ df["time"] = pd.to_datetime(df["time"])
333
+
334
+ # Calculate humidity data
335
+ df[Parameter.RHUM] = df.apply(temp_dwpt_to_rhum, axis=1)
336
+
337
+ # Set index
338
+ df = df.set_index(["time"])
339
+
340
+ # Round decimals
341
+ df = df.round(1)
342
+
343
+ # Remove tz awareness
344
+ df = df.tz_convert(None, level="time")
345
+
346
+ return df
347
+
348
+
349
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
350
+ if "mosmix" in req.station.identifiers:
351
+ return get_df(req.station.identifiers["mosmix"])
@@ -0,0 +1,117 @@
1
+ from typing import Optional, Union
2
+ from urllib.error import HTTPError
3
+
4
+ import pandas as pd
5
+
6
+ from meteostat.core.cache import cache_service
7
+ from meteostat.core.logger import logger
8
+ from meteostat.enumerations import TTL, Parameter
9
+ from meteostat.typing import ProviderRequest
10
+ from meteostat.utils.conversions import percentage_to_okta
11
+
12
+ ENDPOINT = "https://opendata.dwd.de/weather/weather_reports/poi/{station}-BEOB.csv"
13
+ # TODO: add col 11 for solar radiation support (Globalstrahlung)
14
+ USECOLS = [0, 1, 2, 9, 14, 21, 22, 23, 33, 35, 36, 37, 40, 41]
15
+ NAMES = {
16
+ "Wolkenbedeckung": Parameter.CLDC,
17
+ "Temperatur (2m)": Parameter.TEMP,
18
+ "Sichtweite": Parameter.VSBY,
19
+ "Windgeschwindigkeit": Parameter.WSPD,
20
+ "Windboen (letzte Stunde)": Parameter.WPGT,
21
+ "Niederschlag (letzte Stunde)": Parameter.PRCP,
22
+ "Relative Feuchte": Parameter.RHUM,
23
+ "Windrichtung": Parameter.WDIR,
24
+ "Druck (auf Meereshoehe)": Parameter.PRES,
25
+ "Sonnenscheindauer (letzte Stunde)": Parameter.TSUN,
26
+ "aktuelles Wetter": Parameter.COCO,
27
+ "Schneehoehe": Parameter.SNWD,
28
+ }
29
+ COCO_MAP = {
30
+ "1": 1,
31
+ "2": 2,
32
+ "3": 3,
33
+ "4": 4,
34
+ "5": 5,
35
+ "6": 6,
36
+ "7": 7,
37
+ "8": 8,
38
+ "9": 9,
39
+ "10": 10,
40
+ "11": 11,
41
+ "12": 12,
42
+ "13": 13,
43
+ "14": 14,
44
+ "15": 15,
45
+ "16": 16,
46
+ "17": 24,
47
+ "18": 17,
48
+ "19": 18,
49
+ "20": 19,
50
+ "21": 20,
51
+ "22": 21,
52
+ "23": 22,
53
+ "24": 19,
54
+ "25": 20,
55
+ "26": 23,
56
+ "27": 25,
57
+ "28": 26,
58
+ "29": 25,
59
+ "30": 26,
60
+ "31": 27,
61
+ }
62
+
63
+
64
+ def get_coco(code: str | int) -> Union[int, None]:
65
+ """
66
+ Map DWD POI weather condition codes to Meteostat condicodes
67
+ """
68
+ return COCO_MAP.get(str(code))
69
+
70
+
71
+ @cache_service.cache(TTL.HOUR, "pickle")
72
+ def get_df(station: str) -> Optional[pd.DataFrame]:
73
+ try:
74
+ # Read CSV data from DWD server
75
+ df = pd.read_csv( # type: ignore
76
+ ENDPOINT.format(station=station),
77
+ sep=";",
78
+ skiprows=2,
79
+ na_values="---",
80
+ usecols=USECOLS,
81
+ decimal=",",
82
+ )
83
+
84
+ # Rename columns
85
+ df = df.rename(columns=NAMES)
86
+
87
+ # Snow cm -> mm
88
+ df[Parameter.SNWD] = df[Parameter.SNWD].multiply(10)
89
+ df[Parameter.VSBY] = df[Parameter.VSBY].multiply(1000)
90
+
91
+ # Change coco
92
+ df[Parameter.COCO] = df[Parameter.COCO].apply(get_coco)
93
+ df[Parameter.CLDC] = df[Parameter.CLDC].apply(percentage_to_okta)
94
+
95
+ # Set index
96
+ df["time"] = pd.to_datetime(
97
+ df["Datum"] + " " + df["Uhrzeit (UTC)"], format="%d.%m.%y %H:%M"
98
+ )
99
+ df = df.set_index(["time"])
100
+ df = df.drop(["Datum", "Uhrzeit (UTC)"], axis=1)
101
+
102
+ return df
103
+
104
+ except HTTPError as error:
105
+ logger.info(
106
+ f"Couldn't load DWD POI data for weather station {station} (status: {error.status})"
107
+ )
108
+ return None
109
+
110
+ except Exception as error:
111
+ logger.warning(error)
112
+ return None
113
+
114
+
115
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
116
+ if "wmo" in req.station.identifiers:
117
+ return get_df(req.station.identifiers["wmo"])
@@ -0,0 +1,155 @@
1
+ from typing import Union
2
+ from ftplib import FTP
3
+
4
+ from meteostat.core.config import config
5
+
6
+
7
+ DWD_FTP_SERVER = config.dwd_ftp_host
8
+
9
+
10
+ def get_ftp_connection() -> FTP:
11
+ """
12
+ Get DWD Open Data FTP connection
13
+ """
14
+ dwd_ftp_connection = FTP(DWD_FTP_SERVER)
15
+ dwd_ftp_connection.login()
16
+ return dwd_ftp_connection
17
+
18
+
19
+ def get_condicode(code: str) -> Union[int, None]:
20
+ """
21
+ Map DWD codes to Meteostat condicodes
22
+
23
+ https://opendata.dwd.de/climate_environment/CDC/observations_germany/climate/hourly/weather_phenomena/recent/Wetter_Beschreibung.txt
24
+ """
25
+ condicodes = {
26
+ "0": 1,
27
+ "1": 2,
28
+ "2": 1,
29
+ "3": 3,
30
+ "11": 5,
31
+ "12": 5,
32
+ "13": 23,
33
+ "17": 25,
34
+ "21": 8,
35
+ "22": 15,
36
+ "23": 12,
37
+ "24": 10,
38
+ "25": 17,
39
+ "26": 21,
40
+ "27": 24,
41
+ "28": 5,
42
+ "29": 25,
43
+ "40": 5,
44
+ "41": 5,
45
+ "42": 5,
46
+ "43": 5,
47
+ "44": 5,
48
+ "45": 5,
49
+ "46": 5,
50
+ "47": 5,
51
+ "48": 5,
52
+ "49": 5,
53
+ "50": 7,
54
+ "51": 7,
55
+ "52": 8,
56
+ "53": 8,
57
+ "54": 9,
58
+ "55": 9,
59
+ "56": 10,
60
+ "57": 11,
61
+ "58": 7,
62
+ "59": 8,
63
+ "60": 7,
64
+ "61": 7,
65
+ "62": 8,
66
+ "63": 8,
67
+ "64": 9,
68
+ "65": 9,
69
+ "66": 10,
70
+ "67": 11,
71
+ "68": 12,
72
+ "69": 13,
73
+ "70": 14,
74
+ "71": 14,
75
+ "72": 15,
76
+ "73": 15,
77
+ "74": 16,
78
+ "75": 16,
79
+ "80": 17,
80
+ "81": 18,
81
+ "82": 18,
82
+ "83": 19,
83
+ "84": 20,
84
+ "85": 21,
85
+ "86": 22,
86
+ "87": 19,
87
+ "88": 20,
88
+ "89": 24,
89
+ "90": 24,
90
+ "91": 25,
91
+ "92": 26,
92
+ "93": 25,
93
+ "94": 26,
94
+ "95": 25,
95
+ "96": 25,
96
+ "97": 26,
97
+ "98": 25,
98
+ "99": 26,
99
+ "100": 3,
100
+ "101": 2,
101
+ "102": 3,
102
+ "103": 3,
103
+ "120": 5,
104
+ "123": 8,
105
+ "124": 15,
106
+ "125": 10,
107
+ "126": 25,
108
+ "130": 5,
109
+ "131": 5,
110
+ "132": 5,
111
+ "133": 5,
112
+ "134": 5,
113
+ "135": 6,
114
+ "150": 8,
115
+ "151": 7,
116
+ "152": 8,
117
+ "153": 9,
118
+ "154": 10,
119
+ "155": 10,
120
+ "156": 11,
121
+ "157": 7,
122
+ "158": 8,
123
+ "160": 8,
124
+ "161": 7,
125
+ "162": 8,
126
+ "163": 9,
127
+ "164": 10,
128
+ "165": 10,
129
+ "166": 11,
130
+ "167": 12,
131
+ "168": 13,
132
+ "170": 15,
133
+ "171": 14,
134
+ "172": 15,
135
+ "173": 16,
136
+ "177": 14,
137
+ "181": 7,
138
+ "182": 8,
139
+ "183": 9,
140
+ "184": 9,
141
+ "185": 14,
142
+ "186": 15,
143
+ "187": 16,
144
+ "189": 24,
145
+ "190": 25,
146
+ "191": 25,
147
+ "192": 25,
148
+ "193": 25,
149
+ "194": 26,
150
+ "195": 26,
151
+ "196": 26,
152
+ "199": 27,
153
+ }
154
+
155
+ return condicodes.get(str(code), None)
@@ -0,0 +1,87 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ import pandas as pd
5
+
6
+ from meteostat.enumerations import TTL, Parameter
7
+ from meteostat.core.cache import cache_service
8
+ from meteostat.core.network import network_service
9
+ from meteostat.providers.eccc.shared import ENDPOINT, get_meta_data
10
+ from meteostat.typing import ProviderRequest
11
+
12
+ BATCH_LIMIT = 9000
13
+ PROPERTIES = {
14
+ "LOCAL_DATE": "time",
15
+ "MAX_TEMPERATURE": Parameter.TMAX,
16
+ "MEAN_TEMPERATURE": Parameter.TEMP,
17
+ "MIN_TEMPERATURE": Parameter.TMIN,
18
+ "SPEED_MAX_GUST": Parameter.WPGT,
19
+ "TOTAL_PRECIPITATION": Parameter.PRCP,
20
+ "SNOW_ON_GROUND": Parameter.SNWD,
21
+ "TOTAL_SNOW": Parameter.SNOW,
22
+ }
23
+
24
+
25
+ @cache_service.cache(TTL.DAY, "pickle")
26
+ def get_df(climate_id: str, year: int) -> Optional[pd.DataFrame]:
27
+ # Process start & end date
28
+ # ECCC uses the station's local time zone
29
+ start = datetime(year, 1, 1, 0, 0, 0).strftime("%Y-%m-%dT%H:%M:%S")
30
+ end = datetime(year, 12, 31, 23, 59, 59).strftime("%Y-%m-%dT%H:%M:%S")
31
+
32
+ response = network_service.get(
33
+ f"{ENDPOINT}/collections/climate-daily/items",
34
+ params={
35
+ "CLIMATE_IDENTIFIER": climate_id,
36
+ "datetime": f"{start}/{end}",
37
+ "f": "json",
38
+ "properties": ",".join(PROPERTIES.keys()),
39
+ "limit": BATCH_LIMIT,
40
+ },
41
+ )
42
+
43
+ data = response.json()
44
+
45
+ # Extract features from the response
46
+ features = map(
47
+ lambda feature: feature["properties"] if "properties" in feature else {},
48
+ data.get("features", []),
49
+ )
50
+
51
+ # Create a DataFrame from the extracted features
52
+ df = pd.DataFrame(features)
53
+ df = df.rename(columns=PROPERTIES)
54
+
55
+ # Handle time column & set index
56
+ df["time"] = pd.to_datetime(df["time"])
57
+ df = df.set_index(["time"])
58
+
59
+ return df
60
+
61
+
62
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
63
+ if (
64
+ "national" not in req.station.identifiers
65
+ or req.start is None
66
+ or req.end is None
67
+ ):
68
+ return None
69
+
70
+ meta_data = get_meta_data(req.station.identifiers["national"])
71
+ climate_id = meta_data.get("CLIMATE_IDENTIFIER")
72
+ archive_first = meta_data.get("DLY_FIRST_DATE")
73
+ archive_last = meta_data.get("DLY_LAST_DATE")
74
+
75
+ if not (climate_id and archive_first and archive_last):
76
+ return None
77
+
78
+ archive_start = datetime.strptime(archive_first, "%Y-%m-%d %H:%M:%S")
79
+ archive_end = datetime.strptime(archive_last, "%Y-%m-%d %H:%M:%S")
80
+
81
+ years = range(
82
+ max(req.start.year, archive_start.year),
83
+ min(req.end.year, archive_end.year) + 1,
84
+ )
85
+ data = [get_df(climate_id, year) for year in years]
86
+
87
+ return pd.concat(data) if len(data) and not all(d is None for d in data) else None