meteostat 2.0.0__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 +10 -4
- meteostat/{core → api}/config.py +8 -5
- meteostat/api/interpolate.py +217 -79
- meteostat/api/stations.py +1 -1
- meteostat/core/cache.py +1 -1
- meteostat/core/data.py +4 -0
- meteostat/core/network.py +1 -1
- meteostat/enumerations.py +4 -0
- meteostat/interpolation/lapserate.py +1 -1
- meteostat/providers/dwd/climat.py +2 -2
- meteostat/providers/dwd/daily.py +1 -1
- meteostat/providers/dwd/hourly.py +1 -1
- meteostat/providers/dwd/monthly.py +6 -3
- meteostat/providers/dwd/shared.py +1 -1
- 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/meteostat/daily.py +1 -1
- meteostat/providers/meteostat/hourly.py +1 -1
- meteostat/providers/meteostat/monthly.py +1 -1
- meteostat/providers/meteostat/shared.py +1 -1
- meteostat/providers/metno/forecast.py +16 -12
- meteostat/providers/noaa/ghcnd.py +1 -1
- meteostat/providers/noaa/isd_lite.py +14 -1
- meteostat/providers/noaa/metar.py +1 -1
- meteostat/utils/conversions.py +30 -31
- meteostat/utils/guards.py +51 -0
- meteostat/utils/parsers.py +0 -7
- {meteostat-2.0.0.dist-info → meteostat-2.1.0.dist-info}/METADATA +15 -19
- meteostat-2.1.0.dist-info/RECORD +69 -0
- {meteostat-2.0.0.dist-info → meteostat-2.1.0.dist-info}/WHEEL +1 -1
- meteostat-2.0.0.dist-info/RECORD +0 -63
- {meteostat-2.0.0.dist-info → meteostat-2.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
]
|
|
@@ -10,7 +10,7 @@ import pandas as pd
|
|
|
10
10
|
from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
|
|
11
11
|
from meteostat.typing import ProviderRequest
|
|
12
12
|
from meteostat.core.cache import cache_service
|
|
13
|
-
from meteostat.
|
|
13
|
+
from meteostat.api.config import config
|
|
14
14
|
from meteostat.utils.data import reshape_by_source
|
|
15
15
|
|
|
16
16
|
ENDPOINT = config.daily_endpoint
|
|
@@ -9,7 +9,7 @@ import pandas as pd
|
|
|
9
9
|
|
|
10
10
|
from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
|
|
11
11
|
from meteostat.typing import ProviderRequest
|
|
12
|
-
from meteostat.
|
|
12
|
+
from meteostat.api.config import config
|
|
13
13
|
from meteostat.core.cache import cache_service
|
|
14
14
|
from meteostat.utils.data import reshape_by_source
|
|
15
15
|
|
|
@@ -6,7 +6,7 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
import pandas as pd
|
|
8
8
|
|
|
9
|
-
from meteostat.
|
|
9
|
+
from meteostat.api.config import config
|
|
10
10
|
from meteostat.enumerations import TTL
|
|
11
11
|
from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
|
|
12
12
|
from meteostat.typing import ProviderRequest
|
|
@@ -4,7 +4,7 @@ from typing import Optional, Callable, TypeVar
|
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
|
|
7
|
-
from meteostat.
|
|
7
|
+
from meteostat.api.config import config
|
|
8
8
|
from meteostat.core.logger import logger
|
|
9
9
|
from meteostat.core.providers import provider_service
|
|
10
10
|
from meteostat.enumerations import Grade
|
|
@@ -3,7 +3,7 @@ from urllib.error import HTTPError
|
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
|
|
6
|
-
from meteostat.
|
|
6
|
+
from meteostat.api.config import config
|
|
7
7
|
from meteostat.enumerations import TTL, Parameter
|
|
8
8
|
from meteostat.core.logger import logger
|
|
9
9
|
from meteostat.core.network import network_service
|
|
@@ -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
|
|
|
@@ -8,7 +8,7 @@ import pandas as pd
|
|
|
8
8
|
from metar import Metar
|
|
9
9
|
|
|
10
10
|
from meteostat.core.logger import logger
|
|
11
|
-
from meteostat.
|
|
11
|
+
from meteostat.api.config import config
|
|
12
12
|
from meteostat.enumerations import TTL, Frequency, Parameter
|
|
13
13
|
from meteostat.typing import ProviderRequest
|
|
14
14
|
from meteostat.utils.conversions import temp_dwpt_to_rhum
|
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):
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Guard functions for Meteostat.
|
|
3
|
+
|
|
4
|
+
The code is licensed under the MIT license.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from meteostat.api.config import config
|
|
9
|
+
from meteostat.core.logger import logger
|
|
10
|
+
from meteostat.enumerations import Granularity
|
|
11
|
+
from meteostat.typing import Request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def request_size_guard(req: Request) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Guard to block large requests
|
|
17
|
+
"""
|
|
18
|
+
if not config.block_large_requests:
|
|
19
|
+
logger.debug("Large request blocking is disabled.")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
if isinstance(req.station, list) and len(req.station) > 10:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
"Requests with more than 10 stations are blocked by default. "
|
|
25
|
+
"To enable large requests, set `config.block_large_requests = False`."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if req.granularity not in [Granularity.HOURLY, Granularity.DAILY]:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
if req.start is None:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"Hourly and daily requests without a start date are blocked by default. "
|
|
34
|
+
"To enable large requests, set `config.block_large_requests = False`."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
time_diff_years = abs((req.end or datetime.now()).year - req.start.year)
|
|
38
|
+
|
|
39
|
+
logger.debug(f"Request time range: {time_diff_years} years.")
|
|
40
|
+
|
|
41
|
+
if req.granularity is Granularity.HOURLY and time_diff_years > 3:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Hourly requests longer than 3 years are blocked by default. "
|
|
44
|
+
"To enable large requests, set `config.block_large_requests = False`."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if req.granularity is Granularity.DAILY and time_diff_years > 30:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"Daily requests longer than 30 years are blocked by default. "
|
|
50
|
+
"To enable large requests, set `config.block_large_requests = False`."
|
|
51
|
+
)
|
meteostat/utils/parsers.py
CHANGED
|
@@ -13,7 +13,6 @@ import pytz
|
|
|
13
13
|
|
|
14
14
|
from meteostat.api.stations import stations as stations_service
|
|
15
15
|
from meteostat.api.point import Point
|
|
16
|
-
from meteostat.core.config import config
|
|
17
16
|
from meteostat.typing import Station
|
|
18
17
|
|
|
19
18
|
|
|
@@ -54,12 +53,6 @@ def parse_station(
|
|
|
54
53
|
# It's a list
|
|
55
54
|
stations = station
|
|
56
55
|
|
|
57
|
-
if config.block_large_requests and len(stations) > 10:
|
|
58
|
-
raise ValueError(
|
|
59
|
-
"Requests with more than 10 stations are blocked by default. "
|
|
60
|
-
"To enable large requests, set `config.block_large_requests = False`."
|
|
61
|
-
)
|
|
62
|
-
|
|
63
56
|
# Get station meta data
|
|
64
57
|
data = []
|
|
65
58
|
point_counter = 0
|