meteostat 1.7.6__py3-none-any.whl → 2.0.1__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.
- meteostat/__init__.py +38 -19
- meteostat/api/config.py +158 -0
- meteostat/api/daily.py +76 -0
- meteostat/api/hourly.py +80 -0
- meteostat/api/interpolate.py +378 -0
- meteostat/api/inventory.py +59 -0
- meteostat/api/merge.py +103 -0
- meteostat/api/monthly.py +73 -0
- meteostat/api/normals.py +144 -0
- meteostat/api/point.py +30 -0
- meteostat/api/stations.py +234 -0
- meteostat/api/timeseries.py +334 -0
- meteostat/core/cache.py +212 -59
- meteostat/core/data.py +203 -0
- meteostat/core/logger.py +9 -0
- meteostat/core/network.py +82 -0
- meteostat/core/parameters.py +112 -0
- meteostat/core/providers.py +184 -0
- meteostat/core/schema.py +170 -0
- meteostat/core/validator.py +38 -0
- meteostat/enumerations.py +149 -0
- meteostat/interpolation/idw.py +120 -0
- meteostat/interpolation/lapserate.py +91 -0
- meteostat/interpolation/nearest.py +31 -0
- meteostat/parameters.py +354 -0
- meteostat/providers/dwd/climat.py +166 -0
- meteostat/providers/dwd/daily.py +144 -0
- meteostat/providers/dwd/hourly.py +218 -0
- meteostat/providers/dwd/monthly.py +138 -0
- meteostat/providers/dwd/mosmix.py +351 -0
- meteostat/providers/dwd/poi.py +117 -0
- meteostat/providers/dwd/shared.py +155 -0
- meteostat/providers/eccc/daily.py +87 -0
- meteostat/providers/eccc/hourly.py +104 -0
- meteostat/providers/eccc/monthly.py +66 -0
- meteostat/providers/eccc/shared.py +45 -0
- meteostat/providers/index.py +496 -0
- meteostat/providers/meteostat/daily.py +65 -0
- meteostat/providers/meteostat/daily_derived.py +110 -0
- meteostat/providers/meteostat/hourly.py +66 -0
- meteostat/providers/meteostat/monthly.py +45 -0
- meteostat/providers/meteostat/monthly_derived.py +106 -0
- meteostat/providers/meteostat/shared.py +93 -0
- meteostat/providers/metno/forecast.py +186 -0
- meteostat/providers/noaa/ghcnd.py +228 -0
- meteostat/providers/noaa/isd_lite.py +142 -0
- meteostat/providers/noaa/metar.py +163 -0
- meteostat/typing.py +113 -0
- meteostat/utils/conversions.py +231 -0
- meteostat/utils/data.py +194 -0
- meteostat/utils/geo.py +28 -0
- meteostat/utils/guards.py +51 -0
- meteostat/utils/parsers.py +161 -0
- meteostat/utils/types.py +113 -0
- meteostat/utils/validators.py +31 -0
- meteostat-2.0.1.dist-info/METADATA +130 -0
- meteostat-2.0.1.dist-info/RECORD +64 -0
- {meteostat-1.7.6.dist-info → meteostat-2.0.1.dist-info}/WHEEL +1 -2
- meteostat/core/loader.py +0 -103
- meteostat/core/warn.py +0 -34
- meteostat/enumerations/granularity.py +0 -22
- meteostat/interface/base.py +0 -39
- meteostat/interface/daily.py +0 -118
- meteostat/interface/hourly.py +0 -154
- meteostat/interface/meteodata.py +0 -210
- meteostat/interface/monthly.py +0 -109
- meteostat/interface/normals.py +0 -245
- meteostat/interface/point.py +0 -143
- meteostat/interface/stations.py +0 -252
- meteostat/interface/timeseries.py +0 -237
- meteostat/series/aggregate.py +0 -48
- meteostat/series/convert.py +0 -28
- meteostat/series/count.py +0 -17
- meteostat/series/coverage.py +0 -20
- meteostat/series/fetch.py +0 -28
- meteostat/series/interpolate.py +0 -47
- meteostat/series/normalize.py +0 -76
- meteostat/series/stations.py +0 -22
- meteostat/units.py +0 -149
- meteostat/utilities/__init__.py +0 -0
- meteostat/utilities/aggregations.py +0 -37
- meteostat/utilities/endpoint.py +0 -33
- meteostat/utilities/helpers.py +0 -70
- meteostat/utilities/mutations.py +0 -89
- meteostat/utilities/validations.py +0 -30
- meteostat-1.7.6.dist-info/METADATA +0 -112
- meteostat-1.7.6.dist-info/RECORD +0 -39
- meteostat-1.7.6.dist-info/top_level.txt +0 -1
- /meteostat/{core → api}/__init__.py +0 -0
- /meteostat/{enumerations → interpolation}/__init__.py +0 -0
- /meteostat/{interface → providers}/__init__.py +0 -0
- /meteostat/{interface/interpolate.py → py.typed} +0 -0
- /meteostat/{series → utils}/__init__.py +0 -0
- {meteostat-1.7.6.dist-info → meteostat-2.0.1.dist-info/licenses}/LICENSE +0 -0
|
@@ -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.api.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
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pytz
|
|
6
|
+
|
|
7
|
+
from meteostat.enumerations import TTL, Parameter
|
|
8
|
+
from meteostat.core.cache import cache_service
|
|
9
|
+
from meteostat.core.network import network_service
|
|
10
|
+
from meteostat.providers.eccc.shared import ENDPOINT, get_meta_data
|
|
11
|
+
from meteostat.typing import ProviderRequest
|
|
12
|
+
|
|
13
|
+
BATCH_LIMIT = 9000
|
|
14
|
+
PROPERTIES = {
|
|
15
|
+
"UTC_DATE": "time",
|
|
16
|
+
"RELATIVE_HUMIDITY": Parameter.RHUM,
|
|
17
|
+
"WIND_DIRECTION": Parameter.WDIR,
|
|
18
|
+
"WIND_SPEED": Parameter.WSPD,
|
|
19
|
+
"VISIBILITY": Parameter.VSBY,
|
|
20
|
+
"PRECIP_AMOUNT": Parameter.PRCP,
|
|
21
|
+
"TEMP": Parameter.TEMP,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cache_service.cache(TTL.DAY, "pickle")
|
|
26
|
+
def get_df(climate_id: str, year: int, tz: str) -> Optional[pd.DataFrame]:
|
|
27
|
+
# Process start & end date
|
|
28
|
+
# ECCC uses the station's local time zone
|
|
29
|
+
from_timezone = pytz.timezone("UTC")
|
|
30
|
+
to_timezone = pytz.timezone(tz)
|
|
31
|
+
start = (
|
|
32
|
+
from_timezone.localize(datetime(year, 1, 1, 0, 0, 0))
|
|
33
|
+
.astimezone(to_timezone)
|
|
34
|
+
.strftime("%Y-%m-%dT%H:%M:%S")
|
|
35
|
+
)
|
|
36
|
+
end = (
|
|
37
|
+
from_timezone.localize(datetime(year, 12, 31, 23, 59, 59))
|
|
38
|
+
.astimezone(to_timezone)
|
|
39
|
+
.strftime("%Y-%m-%dT%H:%M:%S")
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
response = network_service.get(
|
|
43
|
+
f"{ENDPOINT}/collections/climate-hourly/items",
|
|
44
|
+
params={
|
|
45
|
+
"CLIMATE_IDENTIFIER": climate_id,
|
|
46
|
+
"datetime": f"{start}/{end}",
|
|
47
|
+
"f": "json",
|
|
48
|
+
"properties": ",".join(PROPERTIES.keys()),
|
|
49
|
+
"limit": BATCH_LIMIT,
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
data = response.json()
|
|
54
|
+
|
|
55
|
+
# Extract features from the response
|
|
56
|
+
features = map(
|
|
57
|
+
lambda feature: feature["properties"] if "properties" in feature else {},
|
|
58
|
+
data.get("features", []),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Create a DataFrame from the extracted features
|
|
62
|
+
df = pd.DataFrame(features)
|
|
63
|
+
df = df.rename(columns=PROPERTIES)
|
|
64
|
+
|
|
65
|
+
# Handle time column & set index
|
|
66
|
+
df["time"] = pd.to_datetime(df["time"])
|
|
67
|
+
df = df.set_index(["time"])
|
|
68
|
+
|
|
69
|
+
# Convert data units
|
|
70
|
+
df[Parameter.WDIR] = df[Parameter.WDIR] * 10 # Wind direction is provided 10's deg
|
|
71
|
+
df[Parameter.VSBY] = (
|
|
72
|
+
df[Parameter.VSBY] * 1000
|
|
73
|
+
) # Visibility is provided in kilometres
|
|
74
|
+
|
|
75
|
+
return df
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
79
|
+
if (
|
|
80
|
+
"national" not in req.station.identifiers
|
|
81
|
+
or req.start is None
|
|
82
|
+
or req.end is None
|
|
83
|
+
):
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
meta_data = get_meta_data(req.station.identifiers["national"])
|
|
87
|
+
climate_id = meta_data.get("CLIMATE_IDENTIFIER")
|
|
88
|
+
archive_first = meta_data.get("HLY_FIRST_DATE")
|
|
89
|
+
archive_last = meta_data.get("HLY_LAST_DATE")
|
|
90
|
+
timezone = meta_data.get("TIMEZONE")
|
|
91
|
+
|
|
92
|
+
if not (climate_id and archive_first and archive_last and timezone):
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
archive_start = datetime.strptime(archive_first, "%Y-%m-%d %H:%M:%S")
|
|
96
|
+
archive_end = datetime.strptime(archive_last, "%Y-%m-%d %H:%M:%S")
|
|
97
|
+
|
|
98
|
+
years = range(
|
|
99
|
+
max(req.start.year, archive_start.year),
|
|
100
|
+
min(req.end.year, archive_end.year) + 1,
|
|
101
|
+
)
|
|
102
|
+
data = [get_df(climate_id, year, timezone) for year in years]
|
|
103
|
+
|
|
104
|
+
return pd.concat(data) if len(data) and not all(d is None for d in data) else None
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from meteostat.enumerations import TTL, Parameter
|
|
6
|
+
from meteostat.core.cache import cache_service
|
|
7
|
+
from meteostat.core.network import network_service
|
|
8
|
+
from meteostat.providers.eccc.shared import ENDPOINT, get_meta_data
|
|
9
|
+
from meteostat.typing import ProviderRequest
|
|
10
|
+
|
|
11
|
+
BATCH_LIMIT = 9000
|
|
12
|
+
PROPERTIES = {
|
|
13
|
+
"LOCAL_DATE": "time",
|
|
14
|
+
"MAX_TEMPERATURE": Parameter.TXMX,
|
|
15
|
+
"MEAN_TEMPERATURE": Parameter.TEMP,
|
|
16
|
+
"MIN_TEMPERATURE": Parameter.TXMN,
|
|
17
|
+
"TOTAL_PRECIPITATION": Parameter.PRCP,
|
|
18
|
+
"DAYS_WITH_PRECIP_GE_1MM": Parameter.PDAY,
|
|
19
|
+
"TOTAL_SNOWFALL": Parameter.SNOW,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@cache_service.cache(TTL.WEEK, "pickle")
|
|
24
|
+
def get_df(
|
|
25
|
+
climate_id: str,
|
|
26
|
+
) -> Optional[pd.DataFrame]:
|
|
27
|
+
response = network_service.get(
|
|
28
|
+
f"{ENDPOINT}/collections/climate-monthly/items",
|
|
29
|
+
params={
|
|
30
|
+
"CLIMATE_IDENTIFIER": climate_id,
|
|
31
|
+
"f": "json",
|
|
32
|
+
"properties": ",".join(PROPERTIES.keys()),
|
|
33
|
+
"limit": BATCH_LIMIT,
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
data = response.json()
|
|
38
|
+
|
|
39
|
+
# Extract features from the response
|
|
40
|
+
features = map(
|
|
41
|
+
lambda feature: feature["properties"] if "properties" in feature else {},
|
|
42
|
+
data.get("features", []),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Create a DataFrame from the extracted features
|
|
46
|
+
df = pd.DataFrame(features)
|
|
47
|
+
df = df.rename(columns=PROPERTIES)
|
|
48
|
+
|
|
49
|
+
# Handle time column & set index
|
|
50
|
+
df["time"] = pd.to_datetime(df["time"])
|
|
51
|
+
df = df.set_index(["time"])
|
|
52
|
+
|
|
53
|
+
return df
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
57
|
+
if "national" not in req.station.identifiers:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
meta_data = get_meta_data(req.station.identifiers["national"])
|
|
61
|
+
climate_id = meta_data.get("CLIMATE_IDENTIFIER")
|
|
62
|
+
|
|
63
|
+
if not climate_id:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return get_df(climate_id)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from meteostat.enumerations import TTL
|
|
4
|
+
from meteostat.core.cache import cache_service
|
|
5
|
+
from meteostat.core.network import network_service
|
|
6
|
+
from meteostat.core.logger import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ENDPOINT = "https://api.weather.gc.ca"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@cache_service.cache(TTL.WEEK)
|
|
13
|
+
def get_meta_data(station: str) -> Optional[dict]:
|
|
14
|
+
response = network_service.get(
|
|
15
|
+
f"{ENDPOINT}/collections/climate-stations/items",
|
|
16
|
+
params={
|
|
17
|
+
"STN_ID": station,
|
|
18
|
+
"properties": ",".join(
|
|
19
|
+
[
|
|
20
|
+
"CLIMATE_IDENTIFIER",
|
|
21
|
+
"TIMEZONE",
|
|
22
|
+
"HLY_FIRST_DATE",
|
|
23
|
+
"HLY_LAST_DATE",
|
|
24
|
+
"DLY_FIRST_DATE",
|
|
25
|
+
"DLY_LAST_DATE",
|
|
26
|
+
"MLY_FIRST_DATE",
|
|
27
|
+
"MLY_LAST_DATE",
|
|
28
|
+
]
|
|
29
|
+
),
|
|
30
|
+
"f": "json",
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if response.status_code == 200:
|
|
35
|
+
data = response.json()
|
|
36
|
+
try:
|
|
37
|
+
return data["features"][0]["properties"]
|
|
38
|
+
except (IndexError, KeyError):
|
|
39
|
+
logger.info(f"ECCC climate identifier for station {station} not found")
|
|
40
|
+
else:
|
|
41
|
+
logger.warning(
|
|
42
|
+
f"ECCC climate identifier for station {station} not found (status: {response.status_code})"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return None
|