meteostat 1.7.6__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meteostat/__init__.py +32 -19
- meteostat/api/daily.py +76 -0
- meteostat/api/hourly.py +80 -0
- meteostat/api/interpolate.py +240 -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/config.py +158 -0
- meteostat/core/data.py +199 -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/parsers.py +168 -0
- meteostat/utils/types.py +113 -0
- meteostat/utils/validators.py +31 -0
- meteostat-2.0.0.dist-info/METADATA +134 -0
- meteostat-2.0.0.dist-info/RECORD +63 -0
- {meteostat-1.7.6.dist-info → meteostat-2.0.0.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.0.dist-info/licenses}/LICENSE +0 -0
meteostat/core/config.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Service
|
|
3
|
+
|
|
4
|
+
Manages configuration settings for Meteostat, including cache, network,
|
|
5
|
+
stations, interpolation, and provider-specific settings. Configuration can be
|
|
6
|
+
loaded from environment variables with the MS_ prefix.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, List, Optional
|
|
12
|
+
|
|
13
|
+
from meteostat.core.logger import logger
|
|
14
|
+
from meteostat.enumerations import TTL, Parameter
|
|
15
|
+
from meteostat.utils.types import extract_property_type, validate_parsed_value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Config:
|
|
19
|
+
"""
|
|
20
|
+
Configuration Base Class
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
prefix: str
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def _prefix(self) -> str:
|
|
27
|
+
"""
|
|
28
|
+
The environment variable prefix
|
|
29
|
+
"""
|
|
30
|
+
return f"{self.prefix}_" if self.prefix else ""
|
|
31
|
+
|
|
32
|
+
def _parse_env_value(self, key: str, value: str) -> Any:
|
|
33
|
+
"""
|
|
34
|
+
Parse an environment variable value and validate against property type
|
|
35
|
+
"""
|
|
36
|
+
# Extract the expected type for the property
|
|
37
|
+
expected_type, original_type = extract_property_type(self.__class__, key)
|
|
38
|
+
|
|
39
|
+
if expected_type is None:
|
|
40
|
+
# Fallback to JSON parsing if no type annotation is available
|
|
41
|
+
try:
|
|
42
|
+
return json.loads(value)
|
|
43
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
44
|
+
logger.error("Failed to parse environment variable '%s'", key)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
# Parse the value using JSON
|
|
48
|
+
try:
|
|
49
|
+
parsed_value = json.loads(value)
|
|
50
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
51
|
+
logger.error("Failed to parse environment variable '%s'", key)
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
# Validate and potentially convert the parsed value
|
|
55
|
+
return validate_parsed_value(parsed_value, expected_type, original_type, key)
|
|
56
|
+
|
|
57
|
+
def _set_env_value(self, key: str, value: Any) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Set a configuration using a key-value pair
|
|
60
|
+
"""
|
|
61
|
+
setattr(self, key, value)
|
|
62
|
+
|
|
63
|
+
def __init__(self, prefix: str = "MS") -> None:
|
|
64
|
+
"""
|
|
65
|
+
Initialize configuration service
|
|
66
|
+
"""
|
|
67
|
+
self.prefix = prefix
|
|
68
|
+
self.load_env()
|
|
69
|
+
|
|
70
|
+
def get_env_name(self, key: str) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Get the environment variable name for a given key
|
|
73
|
+
"""
|
|
74
|
+
if not hasattr(self, key):
|
|
75
|
+
raise KeyError(f"Configuration has no key '{key}'")
|
|
76
|
+
|
|
77
|
+
key = f"{self._prefix}{key}"
|
|
78
|
+
return key.upper()
|
|
79
|
+
|
|
80
|
+
def load_env(self) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Update configuration from environment variables with a given prefix.
|
|
83
|
+
"""
|
|
84
|
+
for key, value in os.environ.items():
|
|
85
|
+
if not key.startswith(self._prefix):
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
key = key.replace(self._prefix, "").lower()
|
|
89
|
+
value = self._parse_env_value(key, value)
|
|
90
|
+
|
|
91
|
+
if value is not None:
|
|
92
|
+
self._set_env_value(key, value)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ConfigService(Config):
|
|
96
|
+
"""
|
|
97
|
+
Configuration Service for Meteostat
|
|
98
|
+
|
|
99
|
+
Manages all configuration settings including cache, network, stations,
|
|
100
|
+
interpolation, and provider-specific settings. Supports loading configuration
|
|
101
|
+
from environment variables.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
# General settings
|
|
105
|
+
block_large_requests: bool = True # Block requests that include too many stations
|
|
106
|
+
|
|
107
|
+
# Cache settings
|
|
108
|
+
cache_enable: bool = True
|
|
109
|
+
cache_directory: str = (
|
|
110
|
+
os.path.expanduser("~") + os.sep + ".meteostat" + os.sep + "cache"
|
|
111
|
+
)
|
|
112
|
+
cache_ttl: int = TTL.MONTH
|
|
113
|
+
cache_autoclean: bool = True
|
|
114
|
+
|
|
115
|
+
# Network settings
|
|
116
|
+
network_proxies: Optional[dict] = None
|
|
117
|
+
|
|
118
|
+
# Station meta data settings
|
|
119
|
+
stations_db_ttl: int = TTL.WEEK
|
|
120
|
+
stations_db_endpoints: List[str] = [
|
|
121
|
+
"https://data.meteostat.net/stations.db",
|
|
122
|
+
"https://raw.githubusercontent.com/meteostat/weather-stations/master/stations.db",
|
|
123
|
+
]
|
|
124
|
+
stations_db_file: str = (
|
|
125
|
+
os.path.expanduser("~") + os.sep + ".meteostat" + os.sep + "stations.db"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Interpolation settings
|
|
129
|
+
lapse_rate_parameters = [Parameter.TEMP, Parameter.TMIN, Parameter.TMAX]
|
|
130
|
+
|
|
131
|
+
# [Provider] Meteostat settings
|
|
132
|
+
include_model_data: bool = True
|
|
133
|
+
hourly_endpoint: str = "https://data.meteostat.net/hourly/{year}/{station}.csv.gz"
|
|
134
|
+
daily_endpoint: str = "https://data.meteostat.net/daily/{year}/{station}.csv.gz"
|
|
135
|
+
monthly_endpoint: str = "https://data.meteostat.net/monthly/{station}.csv.gz"
|
|
136
|
+
|
|
137
|
+
# [Provider] DWD settings
|
|
138
|
+
dwd_ftp_host: str = "opendata.dwd.de"
|
|
139
|
+
dwd_hourly_modes: Optional[List[str]] = None
|
|
140
|
+
dwd_daily_modes: Optional[List[str]] = None
|
|
141
|
+
dwd_climat_modes: Optional[List[str]] = None
|
|
142
|
+
|
|
143
|
+
# [Provider] NOAA settings
|
|
144
|
+
aviationweather_endpoint: str = (
|
|
145
|
+
"https://aviationweather.gov/api/data/metar?"
|
|
146
|
+
"ids={station}&format=raw&taf=false&hours=24"
|
|
147
|
+
)
|
|
148
|
+
aviationweather_user_agent: Optional[str] = None
|
|
149
|
+
|
|
150
|
+
# [Provider] Met.no settings
|
|
151
|
+
metno_forecast_endpoint: str = (
|
|
152
|
+
"https://api.met.no/weatherapi/locationforecast/2.0/compact?"
|
|
153
|
+
"lat={latitude}&lon={longitude}&altitude={elevation}"
|
|
154
|
+
)
|
|
155
|
+
metno_user_agent: Optional[str] = None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
config = ConfigService("MS")
|
meteostat/core/data.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data Service
|
|
3
|
+
|
|
4
|
+
The Data Service is responsible for fetching meteorological data from
|
|
5
|
+
different providers and merging it into a single time series.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import List, Optional, Union, cast
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from meteostat.api.timeseries import TimeSeries
|
|
14
|
+
from meteostat.core.logger import logger
|
|
15
|
+
from meteostat.core.parameters import parameter_service
|
|
16
|
+
from meteostat.core.providers import provider_service
|
|
17
|
+
from meteostat.core.schema import schema_service
|
|
18
|
+
from meteostat.enumerations import Parameter, Provider
|
|
19
|
+
from meteostat.typing import Station, Request
|
|
20
|
+
from meteostat.utils.data import stations_to_df
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DataService:
|
|
24
|
+
"""
|
|
25
|
+
Data Service
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def _add_source(df: pd.DataFrame, provider_id: str) -> pd.DataFrame:
|
|
30
|
+
"""
|
|
31
|
+
Add source column to DataFrame
|
|
32
|
+
"""
|
|
33
|
+
if "source" not in df.index.names:
|
|
34
|
+
df["source"] = provider_id
|
|
35
|
+
df = df.set_index(["source"], append=True)
|
|
36
|
+
|
|
37
|
+
return df
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def filter_time(
|
|
41
|
+
df: pd.DataFrame,
|
|
42
|
+
start: Union[datetime, None] = None,
|
|
43
|
+
end: Union[datetime, None] = None,
|
|
44
|
+
) -> pd.DataFrame:
|
|
45
|
+
"""
|
|
46
|
+
Filter time series data based on start and end date
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# Return empty DataFrame if input is empty
|
|
50
|
+
if df.empty:
|
|
51
|
+
return df
|
|
52
|
+
|
|
53
|
+
# Get time index
|
|
54
|
+
time = df.index.get_level_values("time")
|
|
55
|
+
|
|
56
|
+
# Filter & return
|
|
57
|
+
try:
|
|
58
|
+
return df.loc[(time >= start) & (time <= end)] if start and end else df
|
|
59
|
+
except TypeError:
|
|
60
|
+
return (
|
|
61
|
+
df.loc[(time >= start.date()) & (time <= end.date())]
|
|
62
|
+
if start and end
|
|
63
|
+
else df
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def concat_fragments(
|
|
68
|
+
fragments: List[pd.DataFrame],
|
|
69
|
+
parameters: List[Parameter],
|
|
70
|
+
) -> pd.DataFrame:
|
|
71
|
+
"""
|
|
72
|
+
Concatenate multiple fragments into a single DataFrame
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
cleaned = [
|
|
76
|
+
df.dropna(how="all", axis=1) if not df.empty else None
|
|
77
|
+
for df in fragments
|
|
78
|
+
]
|
|
79
|
+
filtered = [df for df in cleaned if df is not None]
|
|
80
|
+
if not filtered:
|
|
81
|
+
return pd.DataFrame()
|
|
82
|
+
df = pd.concat(filtered)
|
|
83
|
+
df = schema_service.fill(df, parameters)
|
|
84
|
+
df = schema_service.purge(df, parameters)
|
|
85
|
+
return df
|
|
86
|
+
except ValueError:
|
|
87
|
+
return pd.DataFrame()
|
|
88
|
+
|
|
89
|
+
def _fetch_provider_data(
|
|
90
|
+
self, req: Request, station: Station, provider: Provider
|
|
91
|
+
) -> Optional[pd.DataFrame]:
|
|
92
|
+
"""
|
|
93
|
+
Fetch data for a single weather station and provider
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
# Fetch DataFrame for current provider
|
|
97
|
+
df = provider_service.fetch_data(provider, req, station)
|
|
98
|
+
|
|
99
|
+
# Continue if no data was returned
|
|
100
|
+
if df is None:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# Add current station ID to DataFrame
|
|
104
|
+
df = pd.concat([df], keys=[station.id], names=["station"])
|
|
105
|
+
|
|
106
|
+
# Add source index column to DataFrame
|
|
107
|
+
df = self._add_source(df, provider)
|
|
108
|
+
|
|
109
|
+
# Filter DataFrame for requested parameters and time range
|
|
110
|
+
df = self.filter_time(df, req.start, req.end)
|
|
111
|
+
|
|
112
|
+
# Drop empty rows
|
|
113
|
+
df = df.dropna(how="all")
|
|
114
|
+
|
|
115
|
+
return df
|
|
116
|
+
|
|
117
|
+
except Exception:
|
|
118
|
+
logger.error(
|
|
119
|
+
'Could not fetch data for provider "%s"',
|
|
120
|
+
provider,
|
|
121
|
+
exc_info=True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _fetch_station_data(self, req: Request, station: Station) -> List[pd.DataFrame]:
|
|
125
|
+
"""
|
|
126
|
+
Fetch data for a single weather station
|
|
127
|
+
"""
|
|
128
|
+
fragments = []
|
|
129
|
+
|
|
130
|
+
filtered_providers = provider_service.filter_providers(req, station)
|
|
131
|
+
|
|
132
|
+
for provider in filtered_providers:
|
|
133
|
+
df = self._fetch_provider_data(req, station, provider)
|
|
134
|
+
|
|
135
|
+
# Continue if no data was returned
|
|
136
|
+
if df is None:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
fragments.append(df)
|
|
140
|
+
|
|
141
|
+
return fragments
|
|
142
|
+
|
|
143
|
+
def fetch(
|
|
144
|
+
self,
|
|
145
|
+
req: Request,
|
|
146
|
+
) -> TimeSeries:
|
|
147
|
+
"""
|
|
148
|
+
Load meteorological time series data from different providers
|
|
149
|
+
"""
|
|
150
|
+
# Convert stations to list if single Station
|
|
151
|
+
stations: List[Station] = (
|
|
152
|
+
cast(List[Station], req.station)
|
|
153
|
+
if isinstance(req.station, list)
|
|
154
|
+
else [req.station]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
logger.debug(
|
|
158
|
+
"%s time series requested for %s station(s)", req.granularity, len(stations)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Filter parameters
|
|
162
|
+
req.parameters = parameter_service.filter_parameters(
|
|
163
|
+
req.granularity, req.parameters
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
fragments = []
|
|
167
|
+
|
|
168
|
+
# Go through all weather stations
|
|
169
|
+
for station in stations:
|
|
170
|
+
station_fragments = self._fetch_station_data(req, station)
|
|
171
|
+
|
|
172
|
+
if station_fragments:
|
|
173
|
+
fragments.extend(station_fragments)
|
|
174
|
+
|
|
175
|
+
# Merge data in a single DataFrame
|
|
176
|
+
if fragments:
|
|
177
|
+
df = self.concat_fragments(fragments, req.parameters)
|
|
178
|
+
else:
|
|
179
|
+
df = pd.DataFrame()
|
|
180
|
+
|
|
181
|
+
# Set data types
|
|
182
|
+
df = schema_service.format(df, req.granularity)
|
|
183
|
+
|
|
184
|
+
# Create time series
|
|
185
|
+
ts = TimeSeries(
|
|
186
|
+
req.granularity,
|
|
187
|
+
stations_to_df(stations),
|
|
188
|
+
df,
|
|
189
|
+
req.start,
|
|
190
|
+
req.end,
|
|
191
|
+
req.timezone,
|
|
192
|
+
multi_station=isinstance(req.station, list),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Return time series
|
|
196
|
+
return ts
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
data_service = DataService()
|
meteostat/core/logger.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Network Service
|
|
3
|
+
|
|
4
|
+
The Network Service provides methods to send HTTP requests
|
|
5
|
+
considering the Meteostat configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from meteostat import __version__
|
|
13
|
+
from meteostat.core.logger import logger
|
|
14
|
+
from meteostat.core.config import config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NetworkService:
|
|
18
|
+
"""
|
|
19
|
+
Network Service
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def _process_headers(headers: dict) -> dict:
|
|
24
|
+
"""
|
|
25
|
+
Process headers
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
headers["X-Meteostat-Version"] = __version__
|
|
29
|
+
|
|
30
|
+
return headers
|
|
31
|
+
|
|
32
|
+
def get(
|
|
33
|
+
self,
|
|
34
|
+
url: str,
|
|
35
|
+
params=None,
|
|
36
|
+
headers: Optional[dict] = None,
|
|
37
|
+
stream: Optional[bool] = None,
|
|
38
|
+
) -> requests.Response:
|
|
39
|
+
"""
|
|
40
|
+
Send a GET request using the Meteostat configuration
|
|
41
|
+
"""
|
|
42
|
+
if headers is None:
|
|
43
|
+
headers = {}
|
|
44
|
+
|
|
45
|
+
headers = self._process_headers(headers)
|
|
46
|
+
|
|
47
|
+
return requests.get(
|
|
48
|
+
url,
|
|
49
|
+
params,
|
|
50
|
+
headers=headers,
|
|
51
|
+
stream=stream,
|
|
52
|
+
proxies=config.network_proxies,
|
|
53
|
+
timeout=30,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def get_from_mirrors(
|
|
57
|
+
self,
|
|
58
|
+
mirrors: list[str],
|
|
59
|
+
params=None,
|
|
60
|
+
headers: Optional[dict] = None,
|
|
61
|
+
stream: Optional[bool] = None,
|
|
62
|
+
) -> Optional[requests.Response]:
|
|
63
|
+
"""
|
|
64
|
+
Send a GET request to multiple mirrors using the Meteostat configuration
|
|
65
|
+
"""
|
|
66
|
+
for mirror in mirrors:
|
|
67
|
+
try:
|
|
68
|
+
response = self.get(
|
|
69
|
+
mirror,
|
|
70
|
+
params=params,
|
|
71
|
+
headers=headers,
|
|
72
|
+
stream=stream,
|
|
73
|
+
)
|
|
74
|
+
if response.status_code == 200:
|
|
75
|
+
return response
|
|
76
|
+
except requests.RequestException:
|
|
77
|
+
logger.warning("Could not fetch data from '%s'", mirror)
|
|
78
|
+
continue
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
network_service = NetworkService()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parameter Service
|
|
3
|
+
|
|
4
|
+
The Parameter Service provides methods to manage and access
|
|
5
|
+
supported parameters for data requests.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from meteostat.core.logger import logger
|
|
11
|
+
from meteostat.enumerations import Granularity, Parameter
|
|
12
|
+
from meteostat.parameters import DEFAULT_PARAMETERS
|
|
13
|
+
from meteostat.typing import ParameterSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ParameterService:
|
|
17
|
+
"""
|
|
18
|
+
Parameter Service
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_parameters: List[ParameterSpec]
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _has_duplicates(parameter_specs: List[ParameterSpec]) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Check if parameter list contains duplicates
|
|
27
|
+
"""
|
|
28
|
+
seen = set()
|
|
29
|
+
for spec in parameter_specs:
|
|
30
|
+
key = (spec.id, spec.granularity)
|
|
31
|
+
if key in seen:
|
|
32
|
+
return True # Duplicate found
|
|
33
|
+
seen.add(key)
|
|
34
|
+
return False # No duplicates found
|
|
35
|
+
|
|
36
|
+
def _parameter_exists(self, parameter: ParameterSpec) -> bool:
|
|
37
|
+
"""
|
|
38
|
+
Check if a parameter already exists
|
|
39
|
+
"""
|
|
40
|
+
key = (parameter.id, parameter.granularity)
|
|
41
|
+
return any((spec.id, spec.granularity) == key for spec in self.parameters)
|
|
42
|
+
|
|
43
|
+
def __init__(self, parameters: List[ParameterSpec]) -> None:
|
|
44
|
+
if self._has_duplicates(parameters):
|
|
45
|
+
raise ValueError("List of parameters contains duplicates")
|
|
46
|
+
|
|
47
|
+
self._parameters = parameters
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def parameters(self) -> List[ParameterSpec]:
|
|
51
|
+
"""
|
|
52
|
+
Get supported parameters
|
|
53
|
+
"""
|
|
54
|
+
return self._parameters
|
|
55
|
+
|
|
56
|
+
def register(self, parameter: ParameterSpec) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Register a parameter
|
|
59
|
+
"""
|
|
60
|
+
if self._parameter_exists(parameter):
|
|
61
|
+
raise ValueError("The parameter already exists")
|
|
62
|
+
|
|
63
|
+
self._parameters.append(parameter)
|
|
64
|
+
|
|
65
|
+
def get_parameter(
|
|
66
|
+
self, parameter_id: Parameter, granularity: Granularity
|
|
67
|
+
) -> Optional[ParameterSpec]:
|
|
68
|
+
"""
|
|
69
|
+
Get parameter by ID and granularity
|
|
70
|
+
"""
|
|
71
|
+
return next(
|
|
72
|
+
(
|
|
73
|
+
parameter
|
|
74
|
+
for parameter in self.parameters
|
|
75
|
+
if parameter.id == parameter_id and parameter.granularity == granularity
|
|
76
|
+
),
|
|
77
|
+
None,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def filter_parameters(
|
|
81
|
+
self, granularity: Granularity, parameters: List[Parameter]
|
|
82
|
+
) -> List[Parameter]:
|
|
83
|
+
"""
|
|
84
|
+
Raise exception if a requested parameter is not part of the schema
|
|
85
|
+
"""
|
|
86
|
+
supported_parameters = list(
|
|
87
|
+
map(
|
|
88
|
+
lambda parameter: parameter.id,
|
|
89
|
+
filter(
|
|
90
|
+
lambda parameter: parameter.granularity == granularity,
|
|
91
|
+
self.parameters,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
# Get difference between requested parameters and root schema
|
|
96
|
+
diff = set(parameters).difference(supported_parameters)
|
|
97
|
+
# Log warning
|
|
98
|
+
if diff:
|
|
99
|
+
logger.error(
|
|
100
|
+
"Tried to request data for unsupported parameter(s): %s",
|
|
101
|
+
", ".join(diff),
|
|
102
|
+
)
|
|
103
|
+
# Return intersection
|
|
104
|
+
return list(
|
|
105
|
+
filter(
|
|
106
|
+
lambda parameter: parameter in parameters,
|
|
107
|
+
supported_parameters,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
parameter_service = ParameterService(DEFAULT_PARAMETERS)
|