meteostat 2.0.1__py3-none-any.whl → 2.1.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.
- meteostat/__init__.py +1 -1
- meteostat/api/config.py +3 -0
- meteostat/enumerations.py +4 -0
- meteostat/providers/dwd/climat.py +1 -1
- meteostat/providers/dwd/monthly.py +6 -3
- meteostat/providers/gsa/__init__.py +3 -0
- meteostat/providers/gsa/daily.py +194 -0
- meteostat/providers/gsa/hourly.py +175 -0
- meteostat/providers/gsa/monthly.py +192 -0
- meteostat/providers/gsa/synop.py +184 -0
- meteostat/providers/index.py +111 -0
- meteostat/providers/metno/forecast.py +15 -11
- meteostat/providers/noaa/ghcnd.py +1 -1
- meteostat/providers/noaa/isd_lite.py +14 -1
- meteostat/utils/conversions.py +30 -31
- {meteostat-2.0.1.dist-info → meteostat-2.1.0.dist-info}/METADATA +2 -2
- {meteostat-2.0.1.dist-info → meteostat-2.1.0.dist-info}/RECORD +19 -14
- {meteostat-2.0.1.dist-info → meteostat-2.1.0.dist-info}/WHEEL +1 -1
- {meteostat-2.0.1.dist-info → meteostat-2.1.0.dist-info}/licenses/LICENSE +0 -0
meteostat/__init__.py
CHANGED
meteostat/api/config.py
CHANGED
meteostat/enumerations.py
CHANGED
|
@@ -102,6 +102,10 @@ class Provider(StrEnum):
|
|
|
102
102
|
ECCC_DAILY = "eccc_daily"
|
|
103
103
|
ECCC_MONTHLY = "eccc_monthly"
|
|
104
104
|
METNO_FORECAST = "metno_forecast"
|
|
105
|
+
GSA_HOURLY = "gsa_hourly"
|
|
106
|
+
GSA_SYNOP = "gsa_synop"
|
|
107
|
+
GSA_DAILY = "gsa_daily"
|
|
108
|
+
GSA_MONTHLY = "gsa_monthly"
|
|
105
109
|
|
|
106
110
|
HOURLY = "hourly"
|
|
107
111
|
DAILY = "daily"
|
|
@@ -102,7 +102,7 @@ def get_df(parameter: str, mode: str, station_code: str) -> Optional[pd.DataFram
|
|
|
102
102
|
|
|
103
103
|
buffer.seek(0)
|
|
104
104
|
df = pd.read_csv(buffer, sep=";").rename(columns=lambda col: col.strip().lower())
|
|
105
|
-
df.rename(columns=param_config["stubnames"]
|
|
105
|
+
df = df.rename(columns=param_config["stubnames"])
|
|
106
106
|
|
|
107
107
|
# Convert wide to long format
|
|
108
108
|
df = pd.wide_to_long(
|
|
@@ -23,7 +23,6 @@ from meteostat.providers.dwd.shared import get_ftp_connection
|
|
|
23
23
|
|
|
24
24
|
BASE_DIR = "/climate_environment/CDC/observations_germany/climate/monthly/kl/"
|
|
25
25
|
USECOLS = [1, 4, 5, 6, 7, 9, 10, 11, 12, 14] # CSV cols which should be read
|
|
26
|
-
PARSE_DATES = {"time": [0]} # Which columns should be parsed as dates?
|
|
27
26
|
NAMES = {
|
|
28
27
|
"MO_N": Parameter.CLDC,
|
|
29
28
|
"MO_TT": Parameter.TEMP,
|
|
@@ -83,15 +82,19 @@ def get_df(station: str, mode: str) -> Optional[pd.DataFrame]:
|
|
|
83
82
|
df: pd.DataFrame = pd.read_csv( # type: ignore
|
|
84
83
|
raw,
|
|
85
84
|
sep=r"\s*;\s*",
|
|
86
|
-
date_format="%Y%m%d",
|
|
87
85
|
na_values=["-999", -999],
|
|
88
86
|
usecols=USECOLS,
|
|
89
|
-
parse_dates=PARSE_DATES,
|
|
90
87
|
engine="python",
|
|
91
88
|
)
|
|
92
89
|
|
|
93
90
|
# Rename columns
|
|
94
91
|
df = df.rename(columns=lambda x: x.strip())
|
|
92
|
+
|
|
93
|
+
# Parse date column
|
|
94
|
+
df["MESS_DATUM_BEGINN"] = pd.to_datetime(
|
|
95
|
+
df["MESS_DATUM_BEGINN"].astype(str), format="%Y%m%d"
|
|
96
|
+
)
|
|
97
|
+
df = df.rename(columns={"MESS_DATUM_BEGINN": "time"})
|
|
95
98
|
df = df.rename(columns=NAMES)
|
|
96
99
|
|
|
97
100
|
# Convert data
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoSphere Austria Data Hub daily data import routine
|
|
3
|
+
|
|
4
|
+
Get daily climate data for weather stations in Austria.
|
|
5
|
+
|
|
6
|
+
License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from meteostat.enumerations import TTL, Parameter
|
|
15
|
+
from meteostat.core.logger import logger
|
|
16
|
+
from meteostat.typing import ProviderRequest
|
|
17
|
+
from meteostat.core.cache import cache_service
|
|
18
|
+
from meteostat.api.config import config
|
|
19
|
+
from meteostat.core.network import network_service
|
|
20
|
+
from meteostat.utils.conversions import (
|
|
21
|
+
hours_to_minutes,
|
|
22
|
+
ms_to_kmh,
|
|
23
|
+
percentage_to_okta,
|
|
24
|
+
pres_to_msl,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
RESOURCE_ID = "klima-v2-1d"
|
|
29
|
+
|
|
30
|
+
# Mapping from GeoSphere Austria parameter names to Meteostat parameters
|
|
31
|
+
# See: https://dataset.api.hub.geosphere.at/v1/station/historical/klima-v2-1d/metadata
|
|
32
|
+
PARAMETER_MAPPING: Dict[str, Parameter] = {
|
|
33
|
+
"tl_mittel": Parameter.TEMP, # Mean air temperature (°C) - Lufttemperatur 2m Mittelwert
|
|
34
|
+
"tlmax": Parameter.TMAX, # Maximum air temperature (°C) - Lufttemperatur 2m Maximalwert
|
|
35
|
+
"tlmin": Parameter.TMIN, # Minimum air temperature (°C) - Lufttemperatur 2m Minimalwert
|
|
36
|
+
"rr": Parameter.PRCP, # Precipitation 24h sum (mm) - Niederschlag 24h Summe
|
|
37
|
+
"sh": Parameter.SNWD, # Snow depth (cm) - Schneehöhe
|
|
38
|
+
"vv_mittel": Parameter.WSPD, # Mean wind speed (km/h) - Windgeschwindigkeit Mittelwert
|
|
39
|
+
"ffx": Parameter.WPGT, # Maximum wind gust (m/s) - Windböe Maximum
|
|
40
|
+
"p_mittel": Parameter.PRES, # Mean air pressure (hPa) - Luftdruck Mittelwert
|
|
41
|
+
"rf_mittel": Parameter.RHUM, # Mean relative humidity (%) - Relative Feuchte Mittelwert
|
|
42
|
+
"so_h": Parameter.TSUN, # Sunshine duration (h) - Sonnenscheindauer
|
|
43
|
+
"bewm_mittel": Parameter.CLDC, # Mean cloud cover (%) - Bedeckung Mittelwert
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Inverse mapping
|
|
47
|
+
METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@cache_service.cache(TTL.WEEK, "pickle")
|
|
51
|
+
def get_data(
|
|
52
|
+
station_id: str,
|
|
53
|
+
elevation: int | None,
|
|
54
|
+
parameters: list[str],
|
|
55
|
+
start: datetime,
|
|
56
|
+
end: datetime,
|
|
57
|
+
) -> Optional[pd.DataFrame]:
|
|
58
|
+
"""
|
|
59
|
+
Fetch data from GeoSphere Austria Data Hub API
|
|
60
|
+
"""
|
|
61
|
+
logger.debug(
|
|
62
|
+
f"Fetching daily data for station '{station_id}' from {start} to {end}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Format dates as ISO 8601 (date only for daily data)
|
|
66
|
+
start_str = start.strftime("%Y-%m-%d")
|
|
67
|
+
end_str = end.strftime("%Y-%m-%d")
|
|
68
|
+
|
|
69
|
+
# Build URL
|
|
70
|
+
url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
|
|
71
|
+
|
|
72
|
+
# Make request
|
|
73
|
+
response = network_service.get(
|
|
74
|
+
url,
|
|
75
|
+
params={
|
|
76
|
+
"parameters": ",".join(parameters),
|
|
77
|
+
"station_ids": station_id,
|
|
78
|
+
"start": start_str,
|
|
79
|
+
"end": end_str,
|
|
80
|
+
"output_format": "geojson",
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if response.status_code != 200:
|
|
85
|
+
logger.warning(
|
|
86
|
+
f"Failed to fetch daily data for station {station_id} (status: {response.status_code})"
|
|
87
|
+
)
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
data = response.json()
|
|
92
|
+
|
|
93
|
+
if not data.get("features"):
|
|
94
|
+
logger.info(f"No daily data returned for station {station_id}")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Get timestamps array
|
|
98
|
+
timestamps = data.get("timestamps")
|
|
99
|
+
if not timestamps:
|
|
100
|
+
logger.warning("No timestamps in daily response")
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# Extract time series data from GeoJSON response
|
|
104
|
+
# New API format has timestamps at top level and data as arrays
|
|
105
|
+
feature = data["features"][0]
|
|
106
|
+
props = feature.get("properties", {})
|
|
107
|
+
params_data = props.get("parameters", {})
|
|
108
|
+
|
|
109
|
+
if not params_data:
|
|
110
|
+
logger.info(f"No parameter data returned for station {station_id}")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# Build DataFrame from timestamps and parameter arrays
|
|
114
|
+
df_dict = {}
|
|
115
|
+
for param in parameters:
|
|
116
|
+
if param in params_data:
|
|
117
|
+
param_info = params_data[param]
|
|
118
|
+
if "data" in param_info:
|
|
119
|
+
df_dict[param] = param_info["data"]
|
|
120
|
+
|
|
121
|
+
if not df_dict:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
# Create DataFrame with timestamps as index
|
|
125
|
+
df = pd.DataFrame(df_dict)
|
|
126
|
+
dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
|
|
127
|
+
df.index = dt_index.tz_localize(None)
|
|
128
|
+
df.index.name = "time"
|
|
129
|
+
|
|
130
|
+
# Sort by time
|
|
131
|
+
df = df.sort_index()
|
|
132
|
+
|
|
133
|
+
# Rename columns to Meteostat parameter names
|
|
134
|
+
rename_map = {}
|
|
135
|
+
for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
|
|
136
|
+
if gsadh_param in df.columns:
|
|
137
|
+
rename_map[gsadh_param] = meteostat_param
|
|
138
|
+
|
|
139
|
+
df = df.rename(columns=rename_map)
|
|
140
|
+
|
|
141
|
+
# Convert units where necessary
|
|
142
|
+
if Parameter.WSPD in df.columns:
|
|
143
|
+
df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
|
|
144
|
+
|
|
145
|
+
if Parameter.WPGT in df.columns:
|
|
146
|
+
df[Parameter.WPGT] = df[Parameter.WPGT].apply(ms_to_kmh)
|
|
147
|
+
|
|
148
|
+
if Parameter.TSUN in df.columns:
|
|
149
|
+
df[Parameter.TSUN] = df[Parameter.TSUN].apply(hours_to_minutes)
|
|
150
|
+
|
|
151
|
+
if Parameter.CLDC in df.columns:
|
|
152
|
+
df[Parameter.CLDC] = df[Parameter.CLDC].apply(percentage_to_okta)
|
|
153
|
+
|
|
154
|
+
if Parameter.PRES in df.columns:
|
|
155
|
+
df[Parameter.PRES] = df.apply(
|
|
156
|
+
lambda row: pres_to_msl(row, elevation), axis=1
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Round values
|
|
160
|
+
df = df.round(1)
|
|
161
|
+
|
|
162
|
+
return df
|
|
163
|
+
|
|
164
|
+
except Exception as error:
|
|
165
|
+
logger.warning(f"Error parsing daily response: {error}", exc_info=True)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
170
|
+
"""
|
|
171
|
+
Fetch daily data from GeoSphere Austria Data Hub
|
|
172
|
+
"""
|
|
173
|
+
if "national" not in req.station.identifiers:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
station_id = req.station.identifiers["national"]
|
|
177
|
+
|
|
178
|
+
# Map Meteostat parameters to GeoSphere Austria parameters
|
|
179
|
+
gsa_params = []
|
|
180
|
+
for param in req.parameters:
|
|
181
|
+
if param in METEOSTAT_TO_GSA:
|
|
182
|
+
gsa_params.append(METEOSTAT_TO_GSA[param])
|
|
183
|
+
|
|
184
|
+
if not gsa_params:
|
|
185
|
+
logger.info("No mappable parameters for GeoSphere Austria daily data")
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
# Fetch data
|
|
189
|
+
df = get_data(station_id, req.station.elevation, gsa_params, req.start, req.end)
|
|
190
|
+
|
|
191
|
+
if df is None or df.empty:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
return df
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoSphere Austria Data Hub hourly data import routine
|
|
3
|
+
|
|
4
|
+
Get hourly climate data for weather stations in Austria.
|
|
5
|
+
|
|
6
|
+
License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from meteostat.enumerations import TTL, Parameter
|
|
15
|
+
from meteostat.core.logger import logger
|
|
16
|
+
from meteostat.typing import ProviderRequest
|
|
17
|
+
from meteostat.core.cache import cache_service
|
|
18
|
+
from meteostat.api.config import config
|
|
19
|
+
from meteostat.core.network import network_service
|
|
20
|
+
from meteostat.utils.conversions import hours_to_minutes, ms_to_kmh
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
RESOURCE_ID = "klima-v2-1h"
|
|
24
|
+
|
|
25
|
+
# Mapping from GeoSphere Austria parameter names to Meteostat parameters
|
|
26
|
+
# See: https://dataset.api.hub.geosphere.at/v1/station/historical/klima-v2-1h/metadata
|
|
27
|
+
PARAMETER_MAPPING: Dict[str, Parameter] = {
|
|
28
|
+
"tl": Parameter.TEMP, # Air temperature (°C)
|
|
29
|
+
"rr": Parameter.PRCP, # Precipitation (mm)
|
|
30
|
+
"pred": Parameter.PRES, # Air pressure (hPa)
|
|
31
|
+
"ff": Parameter.WSPD, # Wind speed (m/s)
|
|
32
|
+
"ffx": Parameter.WPGT, # Wind gust (m/s)
|
|
33
|
+
"dd": Parameter.WDIR, # Wind direction (°)
|
|
34
|
+
"rf": Parameter.RHUM, # Relative humidity (%)
|
|
35
|
+
"so_h": Parameter.TSUN, # Sunshine duration (h)
|
|
36
|
+
"sh": Parameter.SNWD, # Snow depth (cm)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Inverse mapping
|
|
40
|
+
METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@cache_service.cache(TTL.DAY, "pickle")
|
|
44
|
+
def get_data(
|
|
45
|
+
station_id: str, parameters: list[str], start: datetime, end: datetime
|
|
46
|
+
) -> Optional[pd.DataFrame]:
|
|
47
|
+
"""
|
|
48
|
+
Fetch data from GeoSphere Austria Data Hub API
|
|
49
|
+
"""
|
|
50
|
+
logger.debug(
|
|
51
|
+
f"Fetching hourly data for station '{station_id}' from {start} to {end}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Format dates as ISO 8601
|
|
55
|
+
start_str = start.strftime("%Y-%m-%dT%H:%M")
|
|
56
|
+
end_str = end.strftime("%Y-%m-%dT%H:%M")
|
|
57
|
+
|
|
58
|
+
# Build URL
|
|
59
|
+
url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
|
|
60
|
+
|
|
61
|
+
# Make request
|
|
62
|
+
response = network_service.get(
|
|
63
|
+
url,
|
|
64
|
+
params={
|
|
65
|
+
"parameters": ",".join(parameters),
|
|
66
|
+
"station_ids": station_id,
|
|
67
|
+
"start": start_str,
|
|
68
|
+
"end": end_str,
|
|
69
|
+
"output_format": "geojson",
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if response.status_code != 200:
|
|
74
|
+
logger.warning(
|
|
75
|
+
f"Failed to fetch data for station {station_id} (status: {response.status_code})"
|
|
76
|
+
)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
data = response.json()
|
|
81
|
+
|
|
82
|
+
if not data.get("features"):
|
|
83
|
+
logger.info(f"No data returned for station {station_id}")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
# Get timestamps array
|
|
87
|
+
timestamps = data.get("timestamps")
|
|
88
|
+
if not timestamps:
|
|
89
|
+
logger.warning("No timestamps in hourly response")
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# Extract time series data from GeoJSON response
|
|
93
|
+
# New API format has timestamps at top level and data as arrays
|
|
94
|
+
feature = data["features"][0]
|
|
95
|
+
props = feature.get("properties", {})
|
|
96
|
+
params_data = props.get("parameters", {})
|
|
97
|
+
|
|
98
|
+
if not params_data:
|
|
99
|
+
logger.info(f"No parameter data returned for station {station_id}")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Build DataFrame from timestamps and parameter arrays
|
|
103
|
+
df_dict = {}
|
|
104
|
+
for param in parameters:
|
|
105
|
+
if param in params_data:
|
|
106
|
+
param_info = params_data[param]
|
|
107
|
+
if "data" in param_info:
|
|
108
|
+
df_dict[param] = param_info["data"]
|
|
109
|
+
|
|
110
|
+
if not df_dict:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# Create DataFrame with timestamps as index
|
|
114
|
+
df = pd.DataFrame(df_dict)
|
|
115
|
+
dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
|
|
116
|
+
df.index = dt_index.tz_localize(None)
|
|
117
|
+
df.index.name = "time"
|
|
118
|
+
|
|
119
|
+
# Sort by time
|
|
120
|
+
df = df.sort_index()
|
|
121
|
+
|
|
122
|
+
# Rename columns to Meteostat parameter names
|
|
123
|
+
rename_map = {}
|
|
124
|
+
for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
|
|
125
|
+
if gsadh_param in df.columns:
|
|
126
|
+
rename_map[gsadh_param] = meteostat_param
|
|
127
|
+
|
|
128
|
+
df = df.rename(columns=rename_map)
|
|
129
|
+
|
|
130
|
+
# Convert units where necessary
|
|
131
|
+
if Parameter.WSPD in df.columns:
|
|
132
|
+
df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
|
|
133
|
+
|
|
134
|
+
if Parameter.WPGT in df.columns:
|
|
135
|
+
df[Parameter.WPGT] = df[Parameter.WPGT].apply(ms_to_kmh)
|
|
136
|
+
|
|
137
|
+
if Parameter.TSUN in df.columns:
|
|
138
|
+
df[Parameter.TSUN] = df[Parameter.TSUN].apply(hours_to_minutes)
|
|
139
|
+
|
|
140
|
+
# Round values
|
|
141
|
+
df = df.round(1)
|
|
142
|
+
|
|
143
|
+
return df
|
|
144
|
+
|
|
145
|
+
except Exception as error:
|
|
146
|
+
logger.warning(f"Error parsing response: {error}", exc_info=True)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
151
|
+
"""
|
|
152
|
+
Fetch hourly data from GeoSphere Austria Data Hub
|
|
153
|
+
"""
|
|
154
|
+
if "national" not in req.station.identifiers:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
station_id = req.station.identifiers["national"]
|
|
158
|
+
|
|
159
|
+
# Map Meteostat parameters to GeoSphere Austria parameters
|
|
160
|
+
gsa_params = []
|
|
161
|
+
for param in req.parameters:
|
|
162
|
+
if param in METEOSTAT_TO_GSA:
|
|
163
|
+
gsa_params.append(METEOSTAT_TO_GSA[param])
|
|
164
|
+
|
|
165
|
+
if not gsa_params:
|
|
166
|
+
logger.info("No mappable parameters for GeoSphere Austria hourly data")
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
# Fetch data
|
|
170
|
+
df = get_data(station_id, gsa_params, req.start, req.end)
|
|
171
|
+
|
|
172
|
+
if df is None or df.empty:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
return df
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoSphere Austria Data Hub monthly data import routine
|
|
3
|
+
|
|
4
|
+
Get monthly climate data for weather stations in Austria.
|
|
5
|
+
|
|
6
|
+
License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from meteostat.enumerations import TTL, Parameter
|
|
15
|
+
from meteostat.core.logger import logger
|
|
16
|
+
from meteostat.typing import ProviderRequest
|
|
17
|
+
from meteostat.core.cache import cache_service
|
|
18
|
+
from meteostat.api.config import config
|
|
19
|
+
from meteostat.core.network import network_service
|
|
20
|
+
from meteostat.utils.conversions import (
|
|
21
|
+
hours_to_minutes,
|
|
22
|
+
ms_to_kmh,
|
|
23
|
+
percentage_to_okta,
|
|
24
|
+
pres_to_msl,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
RESOURCE_ID = "klima-v2-1m"
|
|
29
|
+
|
|
30
|
+
# Mapping from GeoSphere Austria parameter names to Meteostat parameters
|
|
31
|
+
# See: https://dataset.api.hub.geosphere.at/v1/station/historical/klima-v2-1m/metadata
|
|
32
|
+
PARAMETER_MAPPING: Dict[str, Parameter] = {
|
|
33
|
+
"tl_mittel": Parameter.TEMP, # Mean air temperature (°C) - Lufttemperatur 2m Mittelwert
|
|
34
|
+
"tlmin": Parameter.TXMN, # Minimum air temperature (°C) - Lufttemperatur 2m Minimalwert
|
|
35
|
+
"tlmax": Parameter.TXMX, # Maximum air temperature (°C) - Lufttemperatur 2m Maximalwert
|
|
36
|
+
"tlmin_mittel": Parameter.TMIN, # Mean minimum air temperature (°C) - Lufttemperatur 2m Mittelwert der Minima
|
|
37
|
+
"tlmax_mittel": Parameter.TMAX, # Mean maximum air temperature (°C) - Lufttemperatur 2m Mittelwert der Maxima
|
|
38
|
+
"rf_mittel": Parameter.RHUM, # Mean relative humidity (%) - Relative Feuchte Mittelwert
|
|
39
|
+
"rr": Parameter.PRCP, # Precipitation sum (mm) - Niederschlag Summe der 24h-Summen
|
|
40
|
+
"vv_mittel": Parameter.WSPD, # Mean wind speed (m/s) - Windgeschwindigkeit Mittelwert
|
|
41
|
+
"p": Parameter.PRES, # Mean air pressure (hPa) - Luftdruck Mittelwert
|
|
42
|
+
"so_h": Parameter.TSUN, # Sunshine duration (h) - Sonnenscheindauer
|
|
43
|
+
"bewm_mittel": Parameter.CLDC, # Mean cloud cover (%) - Bedeckung Mittelwert
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Inverse mapping
|
|
47
|
+
METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@cache_service.cache(TTL.MONTH, "pickle")
|
|
51
|
+
def get_data(
|
|
52
|
+
station_id: str,
|
|
53
|
+
elevation: int | None,
|
|
54
|
+
parameters: list[str],
|
|
55
|
+
start: datetime,
|
|
56
|
+
end: datetime,
|
|
57
|
+
) -> Optional[pd.DataFrame]:
|
|
58
|
+
"""
|
|
59
|
+
Fetch data from GeoSphere Austria Data Hub API
|
|
60
|
+
"""
|
|
61
|
+
logger.debug(
|
|
62
|
+
f"Fetching monthly data for station '{station_id}' from {start} to {end}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Format dates as ISO 8601 (full date for monthly data)
|
|
66
|
+
start_str = start.strftime("%Y-%m-%d")
|
|
67
|
+
end_str = end.strftime("%Y-%m-%d")
|
|
68
|
+
|
|
69
|
+
# Build URL
|
|
70
|
+
url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
|
|
71
|
+
|
|
72
|
+
# Make request
|
|
73
|
+
response = network_service.get(
|
|
74
|
+
url,
|
|
75
|
+
params={
|
|
76
|
+
"parameters": ",".join(parameters),
|
|
77
|
+
"station_ids": station_id,
|
|
78
|
+
"start": start_str,
|
|
79
|
+
"end": end_str,
|
|
80
|
+
"output_format": "geojson",
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if response.status_code != 200:
|
|
85
|
+
logger.warning(
|
|
86
|
+
f"Failed to fetch monthly data for station {station_id} (status: {response.status_code}): {response.json()}",
|
|
87
|
+
)
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
data = response.json()
|
|
92
|
+
|
|
93
|
+
if not data.get("features"):
|
|
94
|
+
logger.info(f"No monthly data returned for station {station_id}")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Get timestamps array
|
|
98
|
+
timestamps = data.get("timestamps")
|
|
99
|
+
if not timestamps:
|
|
100
|
+
logger.warning("No timestamps in monthly response")
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# Extract time series data from GeoJSON response
|
|
104
|
+
# New API format has timestamps at top level and data as arrays
|
|
105
|
+
feature = data["features"][0]
|
|
106
|
+
props = feature.get("properties", {})
|
|
107
|
+
params_data = props.get("parameters", {})
|
|
108
|
+
|
|
109
|
+
if not params_data:
|
|
110
|
+
logger.info(f"No parameter data returned for station {station_id}")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# Build DataFrame from timestamps and parameter arrays
|
|
114
|
+
df_dict = {}
|
|
115
|
+
for param in parameters:
|
|
116
|
+
if param in params_data:
|
|
117
|
+
param_info = params_data[param]
|
|
118
|
+
if "data" in param_info:
|
|
119
|
+
df_dict[param] = param_info["data"]
|
|
120
|
+
|
|
121
|
+
if not df_dict:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
# Create DataFrame with timestamps as index
|
|
125
|
+
df = pd.DataFrame(df_dict)
|
|
126
|
+
dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
|
|
127
|
+
df.index = dt_index.tz_localize(None)
|
|
128
|
+
df.index.name = "time"
|
|
129
|
+
|
|
130
|
+
# Sort by time
|
|
131
|
+
df = df.sort_index()
|
|
132
|
+
|
|
133
|
+
# Rename columns to Meteostat parameter names
|
|
134
|
+
rename_map = {}
|
|
135
|
+
for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
|
|
136
|
+
if gsadh_param in df.columns:
|
|
137
|
+
rename_map[gsadh_param] = meteostat_param
|
|
138
|
+
|
|
139
|
+
df = df.rename(columns=rename_map)
|
|
140
|
+
|
|
141
|
+
# Convert units where necessary
|
|
142
|
+
if Parameter.WSPD in df.columns:
|
|
143
|
+
df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
|
|
144
|
+
|
|
145
|
+
if Parameter.TSUN in df.columns:
|
|
146
|
+
df[Parameter.TSUN] = df[Parameter.TSUN].apply(hours_to_minutes)
|
|
147
|
+
|
|
148
|
+
if Parameter.CLDC in df.columns:
|
|
149
|
+
# Convert cloud cover from % to okta
|
|
150
|
+
df[Parameter.CLDC] = df[Parameter.CLDC].apply(percentage_to_okta)
|
|
151
|
+
|
|
152
|
+
if Parameter.PRES in df.columns:
|
|
153
|
+
df[Parameter.PRES] = df.apply(
|
|
154
|
+
lambda row: pres_to_msl(row, elevation), axis=1
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Round values
|
|
158
|
+
df = df.round(1)
|
|
159
|
+
|
|
160
|
+
return df
|
|
161
|
+
|
|
162
|
+
except Exception as error:
|
|
163
|
+
logger.warning(f"Error parsing monthly response: {error}", exc_info=True)
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
168
|
+
"""
|
|
169
|
+
Fetch monthly data from GeoSphere Austria Data Hub
|
|
170
|
+
"""
|
|
171
|
+
if "national" not in req.station.identifiers:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
station_id = req.station.identifiers["national"]
|
|
175
|
+
|
|
176
|
+
# Map Meteostat parameters to GeoSphere Austria parameters
|
|
177
|
+
gsa_params = []
|
|
178
|
+
for param in req.parameters:
|
|
179
|
+
if param in METEOSTAT_TO_GSA:
|
|
180
|
+
gsa_params.append(METEOSTAT_TO_GSA[param])
|
|
181
|
+
|
|
182
|
+
if not gsa_params:
|
|
183
|
+
logger.info("No mappable parameters for GeoSphere Austria monthly data")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
# Fetch data
|
|
187
|
+
df = get_data(station_id, req.station.elevation, gsa_params, req.start, req.end)
|
|
188
|
+
|
|
189
|
+
if df is None or df.empty:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
return df
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoSphere Austria Data Hub SYNOP hourly data import routine
|
|
3
|
+
|
|
4
|
+
Get SYNOP (synoptic observation) hourly data for weather stations in Austria.
|
|
5
|
+
|
|
6
|
+
License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from meteostat.enumerations import TTL, Parameter
|
|
15
|
+
from meteostat.core.logger import logger
|
|
16
|
+
from meteostat.typing import ProviderRequest
|
|
17
|
+
from meteostat.core.cache import cache_service
|
|
18
|
+
from meteostat.api.config import config
|
|
19
|
+
from meteostat.core.network import network_service
|
|
20
|
+
from meteostat.utils.conversions import ms_to_kmh, pres_to_msl
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
RESOURCE_ID = "synop-v1-1h"
|
|
24
|
+
|
|
25
|
+
# Mapping from GeoSphere Austria SYNOP parameter names to Meteostat parameters
|
|
26
|
+
# See: https://dataset.api.hub.geosphere.at/v1/station/historical/synop-v1-1h/metadata
|
|
27
|
+
PARAMETER_MAPPING: Dict[str, Parameter] = {
|
|
28
|
+
"T": Parameter.TEMP, # Air temperature (°C) - Lufttemperatur
|
|
29
|
+
"Pg": Parameter.PRES, # Air pressure (hPa) - Luftdruck auf Stationshöhe
|
|
30
|
+
"rel": Parameter.RHUM, # Relative humidity (%) - Relative Feuchte
|
|
31
|
+
"ff": Parameter.WSPD, # Wind speed (m/s) - Windgeschwindigkeit
|
|
32
|
+
"boe": Parameter.WPGT, # Wind gust (m/s) - Windböe
|
|
33
|
+
"dd": Parameter.WDIR, # Wind direction (°) - Windrichtung
|
|
34
|
+
"RRR": Parameter.PRCP, # Precipitation (mm) - Niederschlagsmenge
|
|
35
|
+
"N": Parameter.CLDC, # Cloud cover (okta) - Bedeckungsgrad
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Inverse mapping
|
|
39
|
+
METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@cache_service.cache(TTL.DAY, "pickle")
|
|
43
|
+
def get_data(
|
|
44
|
+
station_id: str,
|
|
45
|
+
elevation: int | None,
|
|
46
|
+
parameters: list[str],
|
|
47
|
+
start: datetime,
|
|
48
|
+
end: datetime,
|
|
49
|
+
) -> Optional[pd.DataFrame]:
|
|
50
|
+
"""
|
|
51
|
+
Fetch SYNOP data from GeoSphere Austria Data Hub API
|
|
52
|
+
"""
|
|
53
|
+
logger.debug(
|
|
54
|
+
f"Fetching SYNOP hourly data for station '{station_id}' from {start} to {end}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Format dates as ISO 8601
|
|
58
|
+
start_str = start.strftime("%Y-%m-%dT%H:%M")
|
|
59
|
+
end_str = end.strftime("%Y-%m-%dT%H:%M")
|
|
60
|
+
|
|
61
|
+
# Build URL
|
|
62
|
+
url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
|
|
63
|
+
|
|
64
|
+
# Make request
|
|
65
|
+
response = network_service.get(
|
|
66
|
+
url,
|
|
67
|
+
params={
|
|
68
|
+
"parameters": ",".join(parameters),
|
|
69
|
+
"station_ids": station_id,
|
|
70
|
+
"start": start_str,
|
|
71
|
+
"end": end_str,
|
|
72
|
+
"output_format": "geojson",
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if response.status_code != 200:
|
|
77
|
+
logger.warning(
|
|
78
|
+
f"Failed to fetch SYNOP data for station {station_id} (status: {response.status_code})"
|
|
79
|
+
)
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
data = response.json()
|
|
84
|
+
|
|
85
|
+
if not data.get("features"):
|
|
86
|
+
logger.info(f"No SYNOP data returned for station {station_id}")
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# Get timestamps array
|
|
90
|
+
timestamps = data.get("timestamps")
|
|
91
|
+
if not timestamps:
|
|
92
|
+
logger.warning("No timestamps in SYNOP response")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
# Extract time series data from GeoJSON response
|
|
96
|
+
# New API format has timestamps at top level and data as arrays
|
|
97
|
+
feature = data["features"][0]
|
|
98
|
+
props = feature.get("properties", {})
|
|
99
|
+
params_data = props.get("parameters", {})
|
|
100
|
+
|
|
101
|
+
if not params_data:
|
|
102
|
+
logger.info(f"No parameter data returned for station {station_id}")
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
# Build DataFrame from timestamps and parameter arrays
|
|
106
|
+
df_dict = {}
|
|
107
|
+
for param in parameters:
|
|
108
|
+
if param in params_data:
|
|
109
|
+
param_info = params_data[param]
|
|
110
|
+
if "data" in param_info:
|
|
111
|
+
df_dict[param] = param_info["data"]
|
|
112
|
+
|
|
113
|
+
if not df_dict:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
# Create DataFrame with timestamps as index
|
|
117
|
+
df = pd.DataFrame(df_dict)
|
|
118
|
+
dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
|
|
119
|
+
df.index = dt_index.tz_localize(None)
|
|
120
|
+
df.index.name = "time"
|
|
121
|
+
|
|
122
|
+
# Sort by time
|
|
123
|
+
df = df.sort_index()
|
|
124
|
+
|
|
125
|
+
# Rename columns to Meteostat parameter names
|
|
126
|
+
rename_map = {}
|
|
127
|
+
for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
|
|
128
|
+
if gsadh_param in df.columns:
|
|
129
|
+
rename_map[gsadh_param] = meteostat_param
|
|
130
|
+
|
|
131
|
+
df = df.rename(columns=rename_map)
|
|
132
|
+
|
|
133
|
+
# Convert units where necessary
|
|
134
|
+
if Parameter.WSPD in df.columns:
|
|
135
|
+
df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
|
|
136
|
+
|
|
137
|
+
if Parameter.WPGT in df.columns:
|
|
138
|
+
df[Parameter.WPGT] = df[Parameter.WPGT].apply(ms_to_kmh)
|
|
139
|
+
|
|
140
|
+
# RRR returns -1 for no precipitation; convert to 0
|
|
141
|
+
if Parameter.PRCP in df.columns:
|
|
142
|
+
df[Parameter.PRCP] = df[Parameter.PRCP].replace(-1, 0)
|
|
143
|
+
|
|
144
|
+
if Parameter.PRES in df.columns:
|
|
145
|
+
df[Parameter.PRES] = df.apply(
|
|
146
|
+
lambda row: pres_to_msl(row, elevation), axis=1
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Round values
|
|
150
|
+
df = df.round(1)
|
|
151
|
+
|
|
152
|
+
return df
|
|
153
|
+
|
|
154
|
+
except Exception as error:
|
|
155
|
+
logger.warning(f"Error parsing SYNOP response: {error}", exc_info=True)
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
160
|
+
"""
|
|
161
|
+
Fetch SYNOP hourly data from GeoSphere Austria Data Hub
|
|
162
|
+
"""
|
|
163
|
+
if "wmo" not in req.station.identifiers:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
station_id = req.station.identifiers["wmo"]
|
|
167
|
+
|
|
168
|
+
# Map Meteostat parameters to GeoSphere Austria SYNOP parameters
|
|
169
|
+
gsa_params = []
|
|
170
|
+
for param in req.parameters:
|
|
171
|
+
if param in METEOSTAT_TO_GSA:
|
|
172
|
+
gsa_params.append(METEOSTAT_TO_GSA[param])
|
|
173
|
+
|
|
174
|
+
if not gsa_params:
|
|
175
|
+
logger.info("No mappable parameters for GeoSphere Austria SYNOP data")
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
# Fetch data
|
|
179
|
+
df = get_data(station_id, req.station.elevation, gsa_params, req.start, req.end)
|
|
180
|
+
|
|
181
|
+
if df is None or df.empty:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
return df
|
meteostat/providers/index.py
CHANGED
|
@@ -473,6 +473,113 @@ PROVIDER_METNO_FORECAST = ProviderSpec(
|
|
|
473
473
|
module="meteostat.providers.metno.forecast",
|
|
474
474
|
)
|
|
475
475
|
|
|
476
|
+
PROVIDER_GSA_HOURLY = ProviderSpec(
|
|
477
|
+
id=Provider.GSA_HOURLY,
|
|
478
|
+
name="GeoSphere Austria Hourly",
|
|
479
|
+
granularity=Granularity.HOURLY,
|
|
480
|
+
priority=Priority.HIGHEST,
|
|
481
|
+
grade=Grade.RECORD,
|
|
482
|
+
license=License(
|
|
483
|
+
commercial=True,
|
|
484
|
+
attribution="GeoSphere Austria",
|
|
485
|
+
name="CC BY 4.0",
|
|
486
|
+
url="https://creativecommons.org/licenses/by/4.0/",
|
|
487
|
+
),
|
|
488
|
+
requires=["identifiers"],
|
|
489
|
+
countries=["AT"],
|
|
490
|
+
parameters=[
|
|
491
|
+
Parameter.TEMP,
|
|
492
|
+
Parameter.PRCP,
|
|
493
|
+
Parameter.PRES,
|
|
494
|
+
Parameter.WSPD,
|
|
495
|
+
Parameter.WDIR,
|
|
496
|
+
Parameter.RHUM,
|
|
497
|
+
Parameter.TSUN,
|
|
498
|
+
],
|
|
499
|
+
start=date(1880, 4, 1),
|
|
500
|
+
module="meteostat.providers.gsa.hourly",
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
PROVIDER_GSA_SYNOP = ProviderSpec(
|
|
504
|
+
id=Provider.GSA_SYNOP,
|
|
505
|
+
name="GeoSphere Austria SYNOP",
|
|
506
|
+
granularity=Granularity.HOURLY,
|
|
507
|
+
priority=Priority.HIGH,
|
|
508
|
+
grade=Grade.OBSERVATION,
|
|
509
|
+
license=License(
|
|
510
|
+
commercial=True,
|
|
511
|
+
attribution="GeoSphere Austria",
|
|
512
|
+
name="CC BY 4.0",
|
|
513
|
+
url="https://creativecommons.org/licenses/by/4.0/",
|
|
514
|
+
),
|
|
515
|
+
requires=["identifiers"],
|
|
516
|
+
countries=["AT"],
|
|
517
|
+
parameters=[
|
|
518
|
+
Parameter.TEMP,
|
|
519
|
+
Parameter.PRES,
|
|
520
|
+
Parameter.RHUM,
|
|
521
|
+
Parameter.WSPD,
|
|
522
|
+
Parameter.WDIR,
|
|
523
|
+
Parameter.PRCP,
|
|
524
|
+
],
|
|
525
|
+
start=date(2000, 1, 1),
|
|
526
|
+
module="meteostat.providers.gsa.synop",
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
PROVIDER_GSA_DAILY = ProviderSpec(
|
|
530
|
+
id=Provider.GSA_DAILY,
|
|
531
|
+
name="GeoSphere Austria Daily",
|
|
532
|
+
granularity=Granularity.DAILY,
|
|
533
|
+
priority=Priority.HIGHEST,
|
|
534
|
+
grade=Grade.RECORD,
|
|
535
|
+
license=License(
|
|
536
|
+
commercial=True,
|
|
537
|
+
attribution="GeoSphere Austria",
|
|
538
|
+
name="CC BY 4.0",
|
|
539
|
+
url="https://creativecommons.org/licenses/by/4.0/",
|
|
540
|
+
),
|
|
541
|
+
requires=["identifiers"],
|
|
542
|
+
countries=["AT"],
|
|
543
|
+
parameters=[
|
|
544
|
+
Parameter.TEMP,
|
|
545
|
+
Parameter.TMIN,
|
|
546
|
+
Parameter.TMAX,
|
|
547
|
+
Parameter.PRCP,
|
|
548
|
+
Parameter.PRES,
|
|
549
|
+
Parameter.RHUM,
|
|
550
|
+
Parameter.WSPD,
|
|
551
|
+
Parameter.TSUN,
|
|
552
|
+
],
|
|
553
|
+
start=date(1880, 4, 1),
|
|
554
|
+
module="meteostat.providers.gsa.daily",
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
PROVIDER_GSA_MONTHLY = ProviderSpec(
|
|
558
|
+
id=Provider.GSA_MONTHLY,
|
|
559
|
+
name="GeoSphere Austria Monthly",
|
|
560
|
+
granularity=Granularity.MONTHLY,
|
|
561
|
+
priority=Priority.HIGHEST,
|
|
562
|
+
grade=Grade.RECORD,
|
|
563
|
+
license=License(
|
|
564
|
+
commercial=True,
|
|
565
|
+
attribution="GeoSphere Austria",
|
|
566
|
+
name="CC BY 4.0",
|
|
567
|
+
url="https://creativecommons.org/licenses/by/4.0/",
|
|
568
|
+
),
|
|
569
|
+
requires=["identifiers"],
|
|
570
|
+
countries=["AT"],
|
|
571
|
+
parameters=[
|
|
572
|
+
Parameter.TEMP,
|
|
573
|
+
Parameter.TMIN,
|
|
574
|
+
Parameter.TMAX,
|
|
575
|
+
Parameter.PRCP,
|
|
576
|
+
Parameter.PRES,
|
|
577
|
+
Parameter.TSUN,
|
|
578
|
+
],
|
|
579
|
+
start=date(1880, 4, 1),
|
|
580
|
+
module="meteostat.providers.gsa.monthly",
|
|
581
|
+
)
|
|
582
|
+
|
|
476
583
|
|
|
477
584
|
DEFAULT_PROVIDERS = [
|
|
478
585
|
PROVIDER_HOURLY,
|
|
@@ -493,4 +600,8 @@ DEFAULT_PROVIDERS = [
|
|
|
493
600
|
PROVIDER_CLIMAT,
|
|
494
601
|
PROVIDER_METAR,
|
|
495
602
|
PROVIDER_METNO_FORECAST,
|
|
603
|
+
PROVIDER_GSA_HOURLY,
|
|
604
|
+
PROVIDER_GSA_SYNOP,
|
|
605
|
+
PROVIDER_GSA_DAILY,
|
|
606
|
+
PROVIDER_GSA_MONTHLY,
|
|
496
607
|
]
|
|
@@ -11,9 +11,6 @@ from meteostat.typing import ProviderRequest
|
|
|
11
11
|
from meteostat.utils.conversions import percentage_to_okta
|
|
12
12
|
from meteostat.core.cache import cache_service
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
ENDPOINT = config.metno_forecast_endpoint
|
|
16
|
-
USER_AGENT = config.metno_user_agent
|
|
17
14
|
CONDICODES = {
|
|
18
15
|
"clearsky": 1,
|
|
19
16
|
"cloudy": 3,
|
|
@@ -130,13 +127,26 @@ def map_data(record):
|
|
|
130
127
|
|
|
131
128
|
@cache_service.cache(TTL.HOUR, "pickle")
|
|
132
129
|
def get_df(latitude: float, longitude: float, elevation: int) -> Optional[pd.DataFrame]:
|
|
133
|
-
|
|
130
|
+
endpoint = config.metno_forecast_endpoint
|
|
131
|
+
user_agent = config.metno_user_agent
|
|
132
|
+
|
|
133
|
+
if not endpoint:
|
|
134
|
+
logger.warning("MET Norway forecast endpoint is not configured.")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
if not user_agent:
|
|
138
|
+
logger.warning(
|
|
139
|
+
"MET Norway requires a unique user agent as per their terms of service. Please use config to specify your user agent. For now, this provider is skipped."
|
|
140
|
+
)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
file_url = endpoint.format(
|
|
134
144
|
latitude=latitude,
|
|
135
145
|
longitude=longitude,
|
|
136
146
|
elevation=elevation,
|
|
137
147
|
)
|
|
138
148
|
|
|
139
|
-
headers = {"User-Agent":
|
|
149
|
+
headers = {"User-Agent": user_agent}
|
|
140
150
|
|
|
141
151
|
try:
|
|
142
152
|
response = network_service.get(file_url, headers=headers)
|
|
@@ -173,12 +183,6 @@ def get_df(latitude: float, longitude: float, elevation: int) -> Optional[pd.Dat
|
|
|
173
183
|
|
|
174
184
|
|
|
175
185
|
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
176
|
-
if not USER_AGENT:
|
|
177
|
-
logger.warning(
|
|
178
|
-
"MET Norway requires a unique user agent as per their terms of service. Please use config to specify your user agent. For now, this provider is skipped."
|
|
179
|
-
)
|
|
180
|
-
return None
|
|
181
|
-
|
|
182
186
|
return get_df(
|
|
183
187
|
req.station.latitude,
|
|
184
188
|
req.station.longitude,
|
|
@@ -82,7 +82,7 @@ def create_df(element, dict_element):
|
|
|
82
82
|
df_element = move_col_to_front(col, df_element)
|
|
83
83
|
|
|
84
84
|
# Convert numerical values to float
|
|
85
|
-
df_element
|
|
85
|
+
df_element[element] = df_element[element].astype(float)
|
|
86
86
|
|
|
87
87
|
return df_element
|
|
88
88
|
|
|
@@ -65,13 +65,26 @@ def get_df(usaf: str, wban: str, year: int) -> Optional[pd.DataFrame]:
|
|
|
65
65
|
try:
|
|
66
66
|
df = pd.read_fwf(
|
|
67
67
|
f"{ISD_LITE_ENDPOINT}{year}/{filename}",
|
|
68
|
-
parse_dates={"time": [0, 1, 2, 3]},
|
|
69
68
|
na_values=["-9999", -9999],
|
|
70
69
|
header=None,
|
|
71
70
|
colspecs=COLSPECS,
|
|
72
71
|
compression="gzip",
|
|
73
72
|
)
|
|
74
73
|
|
|
74
|
+
# Parse datetime from first 4 columns (year, month, day, hour)
|
|
75
|
+
df[0] = pd.to_datetime(
|
|
76
|
+
df[0].astype(str)
|
|
77
|
+
+ "-"
|
|
78
|
+
+ df[1].astype(str).str.zfill(2)
|
|
79
|
+
+ "-"
|
|
80
|
+
+ df[2].astype(str).str.zfill(2)
|
|
81
|
+
+ " "
|
|
82
|
+
+ df[3].astype(str).str.zfill(2)
|
|
83
|
+
+ ":00",
|
|
84
|
+
format="%Y-%m-%d %H:%M",
|
|
85
|
+
)
|
|
86
|
+
df = df.drop(columns=[1, 2, 3])
|
|
87
|
+
|
|
75
88
|
# Rename columns
|
|
76
89
|
df.columns = COLUMN_NAMES
|
|
77
90
|
|
meteostat/utils/conversions.py
CHANGED
|
@@ -8,7 +8,7 @@ The code is licensed under the MIT license.
|
|
|
8
8
|
import math
|
|
9
9
|
from typing import Optional
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import pandas as pd
|
|
12
12
|
|
|
13
13
|
from meteostat.enumerations import Parameter, Unit, UnitSystem
|
|
14
14
|
|
|
@@ -73,14 +73,21 @@ def kelvin_to_celsius(value):
|
|
|
73
73
|
"""
|
|
74
74
|
Convert Kelvin to Celsius
|
|
75
75
|
"""
|
|
76
|
-
return value - 273.15 if
|
|
76
|
+
return value - 273.15 if not pd.isna(value) else None
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
def ms_to_kmh(value):
|
|
80
80
|
"""
|
|
81
81
|
Convert m/s to km/h
|
|
82
82
|
"""
|
|
83
|
-
return value * 3.6 if
|
|
83
|
+
return value * 3.6 if not pd.isna(value) else None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def hours_to_minutes(value):
|
|
87
|
+
"""
|
|
88
|
+
Convert duration from hours to minutes
|
|
89
|
+
"""
|
|
90
|
+
return value * 60 if not pd.isna(value) else None
|
|
84
91
|
|
|
85
92
|
|
|
86
93
|
def temp_dwpt_to_rhum(row: dict):
|
|
@@ -93,38 +100,30 @@ def temp_dwpt_to_rhum(row: dict):
|
|
|
93
100
|
math.exp((17.625 * row["dwpt"]) / (243.04 + row["dwpt"]))
|
|
94
101
|
/ math.exp((17.625 * row["temp"]) / (243.04 + row["temp"]))
|
|
95
102
|
)
|
|
96
|
-
if row["temp"]
|
|
103
|
+
if not pd.isna(row["temp"]) and not pd.isna(row["dwpt"])
|
|
97
104
|
else None
|
|
98
105
|
)
|
|
99
106
|
|
|
100
107
|
|
|
101
108
|
def pres_to_msl(row: dict, altitude: Optional[int] = None, temp: str = Parameter.TEMP):
|
|
102
|
-
"""
|
|
103
|
-
Convert local air pressure to MSL
|
|
104
|
-
"""
|
|
105
109
|
try:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
),
|
|
124
|
-
-5.257,
|
|
125
|
-
),
|
|
126
|
-
1,
|
|
127
|
-
)
|
|
110
|
+
pres = row.get(Parameter.PRES)
|
|
111
|
+
t = row.get(temp)
|
|
112
|
+
|
|
113
|
+
if pd.isna(pres) or pd.isna(t) or altitude is None or pres == -999:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
# Type narrowing for arithmetic operations
|
|
117
|
+
if not isinstance(pres, (int, float)) or not isinstance(t, (int, float)):
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
return round(
|
|
121
|
+
pres
|
|
122
|
+
* math.pow(
|
|
123
|
+
1 - (0.0065 * altitude) / (t + 0.0065 * altitude + 273.15),
|
|
124
|
+
-5.257,
|
|
125
|
+
),
|
|
126
|
+
1,
|
|
128
127
|
)
|
|
129
128
|
except Exception:
|
|
130
129
|
return None
|
|
@@ -134,14 +133,14 @@ def percentage_to_okta(value):
|
|
|
134
133
|
"""
|
|
135
134
|
Convert cloud cover percentage to oktas
|
|
136
135
|
"""
|
|
137
|
-
return round(value / 12.5) if
|
|
136
|
+
return round(value / 12.5) if not pd.isna(value) else None
|
|
138
137
|
|
|
139
138
|
|
|
140
139
|
def jcm2_to_wm2(value):
|
|
141
140
|
"""
|
|
142
141
|
Convert Joule/CM^2 to Watt/M^2
|
|
143
142
|
"""
|
|
144
|
-
return round(value * 2.78) if
|
|
143
|
+
return round(value * 2.78) if not pd.isna(value) else None
|
|
145
144
|
|
|
146
145
|
|
|
147
146
|
def to_direction(value):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meteostat
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Access and analyze historical weather and climate data with Python.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Meteostat
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
-
Requires-Dist: pandas (>=2.3.
|
|
13
|
+
Requires-Dist: pandas (>=2.3.0,<4.0.0)
|
|
14
14
|
Requires-Dist: pytz (>=2023.3.post1,<2024.0)
|
|
15
15
|
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
meteostat/__init__.py,sha256=
|
|
1
|
+
meteostat/__init__.py,sha256=bKXwCYMUdZrlNTPTHImXKbELKuEe0JS_YhBhZnAgWrI,1555
|
|
2
2
|
meteostat/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
meteostat/api/config.py,sha256=
|
|
3
|
+
meteostat/api/config.py,sha256=NYRZBHZkxXsGQXgPptO_ic-Ct2dtczWWgpCIt2LgL78,5166
|
|
4
4
|
meteostat/api/daily.py,sha256=SUYxR7HC5Cqyd6cBG42QmSpEP74HwDq5VnqfSznM46Y,2371
|
|
5
5
|
meteostat/api/hourly.py,sha256=KGJ4xBwO__fBrQ5ll5ruLEkrpOCD8bybSBcXJ94-ntE,2556
|
|
6
6
|
meteostat/api/interpolate.py,sha256=6i128g_x5ea3X2Nkn6IlrtPaKqRfakozOcSheTbmxIM,12602
|
|
@@ -19,17 +19,17 @@ meteostat/core/parameters.py,sha256=7BJIkFQaxcBRi1yIDgpAYm2VZxBfVPbi5_UdZL9teXE,
|
|
|
19
19
|
meteostat/core/providers.py,sha256=DChq-vILb9uWEcsZmUcUnyeuVGtl3Ote49QhibdeNBo,5490
|
|
20
20
|
meteostat/core/schema.py,sha256=Jbp84bTgWmfhPYhORluGWLEnogevsFiAjwC21Y4Mbwg,5070
|
|
21
21
|
meteostat/core/validator.py,sha256=pJOJIU8FtYO4k7ihGqFxM6CSs9O6oO-fByYyJeJ9pZY,887
|
|
22
|
-
meteostat/enumerations.py,sha256=
|
|
22
|
+
meteostat/enumerations.py,sha256=FxB1s9gP1Ros2hN0zDZa1TSABfSgym11E0pigHqXPPY,3389
|
|
23
23
|
meteostat/interpolation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
meteostat/interpolation/idw.py,sha256=AyZWxXjq8mS9BEQtxgSEHQzn7TZYIdkc22kPkF5qZww,4435
|
|
25
25
|
meteostat/interpolation/lapserate.py,sha256=D5Y2DLMgjCNmqfdm2J5MU67w0O6L0vx_TbgMaGrMQuc,2637
|
|
26
26
|
meteostat/interpolation/nearest.py,sha256=xZg_6OCvU8uI-i7UbPQHjsJmtlz_DbZqrDz00dJthEM,782
|
|
27
27
|
meteostat/parameters.py,sha256=w7G3ktLVasAbaR9OnjuMAvrq_c1zLz5PVt3zofDFun4,10074
|
|
28
28
|
meteostat/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
-
meteostat/providers/dwd/climat.py,sha256=
|
|
29
|
+
meteostat/providers/dwd/climat.py,sha256=vLCTk0PZgPliyTsNwbNWnKeUEXChA9dA23UtCxOa7tA,4866
|
|
30
30
|
meteostat/providers/dwd/daily.py,sha256=qVZjMUu6a1GODaWrKpzbOYdXS9HyEkKp9XwnY47Md44,4030
|
|
31
31
|
meteostat/providers/dwd/hourly.py,sha256=LAjnQJrp80wOhX24JwuceAtCi104DC8EDwEZetHDrso,6249
|
|
32
|
-
meteostat/providers/dwd/monthly.py,sha256=
|
|
32
|
+
meteostat/providers/dwd/monthly.py,sha256=FiUdMzLwnGLFu1Nxt9KW5VAUyRYA9tXCbsr2T1iCw_w,3719
|
|
33
33
|
meteostat/providers/dwd/mosmix.py,sha256=uDalWL-_Vayyq3iiiCUWPFmKXSYqQttXiDOF2Lk_qB4,8892
|
|
34
34
|
meteostat/providers/dwd/poi.py,sha256=Qv4eDsi_a8hdBQKl1LPQJ0BO2UbTxfba9gH5Ht2giZ0,3087
|
|
35
35
|
meteostat/providers/dwd/shared.py,sha256=z4LT0fd7LFUqpVPJRJyNWJxK_6TRzJFDvolv6_Y9PME,2917
|
|
@@ -37,28 +37,33 @@ meteostat/providers/eccc/daily.py,sha256=K-akr9JXYMnsdG1YlM9SPXUcDfVzv61KsCqD9Ie
|
|
|
37
37
|
meteostat/providers/eccc/hourly.py,sha256=W2qMX0HxffMs1IlBzKY4b0M58wPJvtDw7Py4xyDuNO8,3209
|
|
38
38
|
meteostat/providers/eccc/monthly.py,sha256=eA5JRpDa7-2H-Ce4FOstY24ECWj_gTY5vRbvZa_LCLE,1805
|
|
39
39
|
meteostat/providers/eccc/shared.py,sha256=2m00uWYdWQrVqvxwMioLIKFxlrjLmCfqIYwjx30r_9k,1324
|
|
40
|
-
meteostat/providers/
|
|
40
|
+
meteostat/providers/gsa/__init__.py,sha256=a4SQxle7PvurmZWrzw8mxOpWxv1cztnhf0I6OXza2Lk,44
|
|
41
|
+
meteostat/providers/gsa/daily.py,sha256=OBD2oq2RQJ8CQBAKLsEyzfGWSkA2QylFLFlp5n_46Q8,6298
|
|
42
|
+
meteostat/providers/gsa/hourly.py,sha256=yDeF-periIV0aq2PSHDHVpcgfHMaNA_twXZ-Ob6sP5o,5392
|
|
43
|
+
meteostat/providers/gsa/monthly.py,sha256=hniTVaSvsidP0I7No42OncmeGBVU0k05jDnFrZrabhI,6376
|
|
44
|
+
meteostat/providers/gsa/synop.py,sha256=P46RtF6nF5eaMiFUECML1XZiKzMcxbV0G5S6JCLNWRs,5827
|
|
45
|
+
meteostat/providers/index.py,sha256=W4Jd3Fgq-iYpPLZopqKcFGPU_hCUjhTejLVw-l3WYk4,15511
|
|
41
46
|
meteostat/providers/meteostat/daily.py,sha256=32vh0J5caMT9gshfUBQXoH8CB3r3qf4BUlB3VztvPN0,1795
|
|
42
47
|
meteostat/providers/meteostat/daily_derived.py,sha256=_fiLSKxkaH9Caxp-Tg77rH39UXwy7SPDlCS_M-7kIqE,3170
|
|
43
48
|
meteostat/providers/meteostat/hourly.py,sha256=INO5JVaDbd3zIEo3_spIwACoBpYqDXinyrL-pW1AIAk,1798
|
|
44
49
|
meteostat/providers/meteostat/monthly.py,sha256=4N2ThwBroQVz2wI5UI2WpvIUGOH61gS63rP8ZVaMqOE,1253
|
|
45
50
|
meteostat/providers/meteostat/monthly_derived.py,sha256=8BNSd2-xqABdr3lDU_9noypF91qhRJPhz5liTK0eoR8,3075
|
|
46
51
|
meteostat/providers/meteostat/shared.py,sha256=mK2K3tbgtCcKLMITR5NJVuJSBkPm_jLcN0dBRT9hM90,2571
|
|
47
|
-
meteostat/providers/metno/forecast.py,sha256=
|
|
48
|
-
meteostat/providers/noaa/ghcnd.py,sha256=
|
|
49
|
-
meteostat/providers/noaa/isd_lite.py,sha256=
|
|
52
|
+
meteostat/providers/metno/forecast.py,sha256=aMoOikEh2agMCw6ZLFMcHPnT-9VMQ1_qpx7e2K3B_-g,5785
|
|
53
|
+
meteostat/providers/noaa/ghcnd.py,sha256=xOvp0DGi6XARZQBwbImDI5o_5QoNEXXr2e2JtUAUTyk,6775
|
|
54
|
+
meteostat/providers/noaa/isd_lite.py,sha256=bPnGEFzvb3LA8n8OmurCToW5HTb5aJSa1FXJQgC_3rI,4200
|
|
50
55
|
meteostat/providers/noaa/metar.py,sha256=wqH0zEUZO0gS0inK5vvxKulz-LF3NEZIAsYHmR0Do64,4188
|
|
51
56
|
meteostat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
57
|
meteostat/typing.py,sha256=Q4_le4P_yOVyv1FAxtccNDfFc0OPzYKm3p-GTTptGAY,3504
|
|
53
58
|
meteostat/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
|
-
meteostat/utils/conversions.py,sha256=
|
|
59
|
+
meteostat/utils/conversions.py,sha256=qR4eSyN8hpaSDKk-VvDmgT-paJq0fjxaT-lz6X1pmYk,4652
|
|
55
60
|
meteostat/utils/data.py,sha256=d_aQy53jdhDAvFvo0qVPyhR2NH-WKIAeNm_kIobJOq8,6119
|
|
56
61
|
meteostat/utils/geo.py,sha256=n_imXze6-u0vzFdYrgPyStKqGNuERcUmXUA-PsAja2w,664
|
|
57
62
|
meteostat/utils/guards.py,sha256=fuBd4_6BXfZT_jPYU6GzXgDBnCc_GVLDpI6PSK3PCno,1746
|
|
58
63
|
meteostat/utils/parsers.py,sha256=2y-QSubyrh_xdXJBdgWNiWNI_DDXCGxddcjGOZB9hCU,4646
|
|
59
64
|
meteostat/utils/types.py,sha256=_hzGcVueIGZdjCB6mR4vVFedrPUWw0iMRaLkpnqaN2Y,3470
|
|
60
65
|
meteostat/utils/validators.py,sha256=iBywF68ZhL3eD6fgemY2Tng3a-409eFU2oNZVqpYCpY,524
|
|
61
|
-
meteostat-2.0.
|
|
62
|
-
meteostat-2.0.
|
|
63
|
-
meteostat-2.0.
|
|
64
|
-
meteostat-2.0.
|
|
66
|
+
meteostat-2.1.0.dist-info/METADATA,sha256=wgyYCQMPf3x7L7AJVxfuIyMl0D02e5UYxGOwdSz5waM,5024
|
|
67
|
+
meteostat-2.1.0.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
68
|
+
meteostat-2.1.0.dist-info/licenses/LICENSE,sha256=kqpl7FVzWOCe11BZqJBZ1aRQi-aK87j3ljtG7P3VxLc,1066
|
|
69
|
+
meteostat-2.1.0.dist-info/RECORD,,
|
|
File without changes
|