meteostat 1.7.5__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. meteostat/__init__.py +32 -19
  2. meteostat/api/daily.py +76 -0
  3. meteostat/api/hourly.py +80 -0
  4. meteostat/api/interpolate.py +240 -0
  5. meteostat/api/inventory.py +59 -0
  6. meteostat/api/merge.py +103 -0
  7. meteostat/api/monthly.py +73 -0
  8. meteostat/api/normals.py +144 -0
  9. meteostat/api/point.py +30 -0
  10. meteostat/api/stations.py +234 -0
  11. meteostat/api/timeseries.py +334 -0
  12. meteostat/core/cache.py +212 -59
  13. meteostat/core/config.py +158 -0
  14. meteostat/core/data.py +199 -0
  15. meteostat/core/logger.py +9 -0
  16. meteostat/core/network.py +82 -0
  17. meteostat/core/parameters.py +112 -0
  18. meteostat/core/providers.py +184 -0
  19. meteostat/core/schema.py +170 -0
  20. meteostat/core/validator.py +38 -0
  21. meteostat/enumerations.py +149 -0
  22. meteostat/interpolation/idw.py +120 -0
  23. meteostat/interpolation/lapserate.py +91 -0
  24. meteostat/interpolation/nearest.py +31 -0
  25. meteostat/parameters.py +354 -0
  26. meteostat/providers/dwd/climat.py +166 -0
  27. meteostat/providers/dwd/daily.py +144 -0
  28. meteostat/providers/dwd/hourly.py +218 -0
  29. meteostat/providers/dwd/monthly.py +138 -0
  30. meteostat/providers/dwd/mosmix.py +351 -0
  31. meteostat/providers/dwd/poi.py +117 -0
  32. meteostat/providers/dwd/shared.py +155 -0
  33. meteostat/providers/eccc/daily.py +87 -0
  34. meteostat/providers/eccc/hourly.py +104 -0
  35. meteostat/providers/eccc/monthly.py +66 -0
  36. meteostat/providers/eccc/shared.py +45 -0
  37. meteostat/providers/index.py +496 -0
  38. meteostat/providers/meteostat/daily.py +65 -0
  39. meteostat/providers/meteostat/daily_derived.py +110 -0
  40. meteostat/providers/meteostat/hourly.py +66 -0
  41. meteostat/providers/meteostat/monthly.py +45 -0
  42. meteostat/providers/meteostat/monthly_derived.py +106 -0
  43. meteostat/providers/meteostat/shared.py +93 -0
  44. meteostat/providers/metno/forecast.py +186 -0
  45. meteostat/providers/noaa/ghcnd.py +228 -0
  46. meteostat/providers/noaa/isd_lite.py +142 -0
  47. meteostat/providers/noaa/metar.py +163 -0
  48. meteostat/typing.py +113 -0
  49. meteostat/utils/conversions.py +231 -0
  50. meteostat/utils/data.py +194 -0
  51. meteostat/utils/geo.py +28 -0
  52. meteostat/utils/parsers.py +168 -0
  53. meteostat/utils/types.py +113 -0
  54. meteostat/utils/validators.py +31 -0
  55. meteostat-2.0.0.dist-info/METADATA +134 -0
  56. meteostat-2.0.0.dist-info/RECORD +63 -0
  57. {meteostat-1.7.5.dist-info → meteostat-2.0.0.dist-info}/WHEEL +1 -2
  58. meteostat/core/loader.py +0 -103
  59. meteostat/core/warn.py +0 -34
  60. meteostat/enumerations/granularity.py +0 -22
  61. meteostat/interface/base.py +0 -39
  62. meteostat/interface/daily.py +0 -118
  63. meteostat/interface/hourly.py +0 -154
  64. meteostat/interface/meteodata.py +0 -210
  65. meteostat/interface/monthly.py +0 -109
  66. meteostat/interface/normals.py +0 -245
  67. meteostat/interface/point.py +0 -143
  68. meteostat/interface/stations.py +0 -252
  69. meteostat/interface/timeseries.py +0 -237
  70. meteostat/series/aggregate.py +0 -48
  71. meteostat/series/convert.py +0 -28
  72. meteostat/series/count.py +0 -17
  73. meteostat/series/coverage.py +0 -20
  74. meteostat/series/fetch.py +0 -28
  75. meteostat/series/interpolate.py +0 -47
  76. meteostat/series/normalize.py +0 -76
  77. meteostat/series/stations.py +0 -22
  78. meteostat/units.py +0 -149
  79. meteostat/utilities/__init__.py +0 -0
  80. meteostat/utilities/aggregations.py +0 -37
  81. meteostat/utilities/endpoint.py +0 -33
  82. meteostat/utilities/helpers.py +0 -70
  83. meteostat/utilities/mutations.py +0 -85
  84. meteostat/utilities/validations.py +0 -30
  85. meteostat-1.7.5.dist-info/METADATA +0 -112
  86. meteostat-1.7.5.dist-info/RECORD +0 -39
  87. meteostat-1.7.5.dist-info/top_level.txt +0 -1
  88. /meteostat/{core → api}/__init__.py +0 -0
  89. /meteostat/{enumerations → interpolation}/__init__.py +0 -0
  90. /meteostat/{interface → providers}/__init__.py +0 -0
  91. /meteostat/{interface/interpolate.py → py.typed} +0 -0
  92. /meteostat/{series → utils}/__init__.py +0 -0
  93. {meteostat-1.7.5.dist-info → meteostat-2.0.0.dist-info/licenses}/LICENSE +0 -0
@@ -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()
@@ -0,0 +1,9 @@
1
+ """
2
+ Logger Module
3
+
4
+ Provides a centralized logger instance for the Meteostat package.
5
+ """
6
+
7
+ import logging
8
+
9
+ logger = logging.getLogger("meteostat")
@@ -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)