meteostat 2.0.0__tar.gz → 2.1.0__tar.gz

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 (69) hide show
  1. {meteostat-2.0.0 → meteostat-2.1.0}/PKG-INFO +15 -19
  2. {meteostat-2.0.0 → meteostat-2.1.0}/README.md +13 -16
  3. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/__init__.py +10 -4
  4. {meteostat-2.0.0/meteostat/core → meteostat-2.1.0/meteostat/api}/config.py +8 -5
  5. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/interpolate.py +217 -79
  6. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/stations.py +1 -1
  7. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/core/cache.py +1 -1
  8. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/core/data.py +4 -0
  9. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/core/network.py +1 -1
  10. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/enumerations.py +4 -0
  11. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/interpolation/lapserate.py +1 -1
  12. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/dwd/climat.py +2 -2
  13. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/dwd/daily.py +1 -1
  14. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/dwd/hourly.py +1 -1
  15. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/dwd/monthly.py +6 -3
  16. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/dwd/shared.py +1 -1
  17. meteostat-2.1.0/meteostat/providers/gsa/__init__.py +3 -0
  18. meteostat-2.1.0/meteostat/providers/gsa/daily.py +194 -0
  19. meteostat-2.1.0/meteostat/providers/gsa/hourly.py +175 -0
  20. meteostat-2.1.0/meteostat/providers/gsa/monthly.py +192 -0
  21. meteostat-2.1.0/meteostat/providers/gsa/synop.py +184 -0
  22. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/index.py +111 -0
  23. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/meteostat/daily.py +1 -1
  24. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/meteostat/hourly.py +1 -1
  25. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/meteostat/monthly.py +1 -1
  26. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/meteostat/shared.py +1 -1
  27. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/metno/forecast.py +16 -12
  28. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/noaa/ghcnd.py +1 -1
  29. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/noaa/isd_lite.py +14 -1
  30. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/noaa/metar.py +1 -1
  31. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/utils/conversions.py +30 -31
  32. meteostat-2.1.0/meteostat/utils/guards.py +51 -0
  33. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/utils/parsers.py +0 -7
  34. {meteostat-2.0.0 → meteostat-2.1.0}/pyproject.toml +2 -2
  35. {meteostat-2.0.0 → meteostat-2.1.0}/LICENSE +0 -0
  36. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/__init__.py +0 -0
  37. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/daily.py +0 -0
  38. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/hourly.py +0 -0
  39. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/inventory.py +0 -0
  40. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/merge.py +0 -0
  41. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/monthly.py +0 -0
  42. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/normals.py +0 -0
  43. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/point.py +0 -0
  44. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/api/timeseries.py +0 -0
  45. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/core/logger.py +0 -0
  46. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/core/parameters.py +0 -0
  47. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/core/providers.py +0 -0
  48. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/core/schema.py +0 -0
  49. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/core/validator.py +0 -0
  50. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/interpolation/__init__.py +0 -0
  51. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/interpolation/idw.py +0 -0
  52. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/interpolation/nearest.py +0 -0
  53. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/parameters.py +0 -0
  54. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/__init__.py +0 -0
  55. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/dwd/mosmix.py +0 -0
  56. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/dwd/poi.py +0 -0
  57. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/eccc/daily.py +0 -0
  58. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/eccc/hourly.py +0 -0
  59. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/eccc/monthly.py +0 -0
  60. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/eccc/shared.py +0 -0
  61. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/meteostat/daily_derived.py +0 -0
  62. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/providers/meteostat/monthly_derived.py +0 -0
  63. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/py.typed +0 -0
  64. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/typing.py +0 -0
  65. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/utils/__init__.py +0 -0
  66. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/utils/data.py +0 -0
  67. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/utils/geo.py +0 -0
  68. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/utils/types.py +0 -0
  69. {meteostat-2.0.0 → meteostat-2.1.0}/meteostat/utils/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meteostat
3
- Version: 2.0.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.3,<3.0.0)
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
@@ -18,12 +18,11 @@ Description-Content-Type: text/markdown
18
18
  <!-- PROJECT SHIELDS -->
19
19
  <div align="center">
20
20
 
21
- [![Contributors][contributors-shield]][contributors-url]
22
- [![Forks][forks-shield]][forks-url]
23
- [![Stargazers][stars-shield]][stars-url]
21
+ [![Downloads][downloads-shield]][downloads-url]
22
+ [![Provider Tests][provider-tests-shield]][provider-tests-url]
24
23
  [![Issues][issues-shield]][issues-url]
25
- [![Unlicense License][license-shield]][license-url]
26
- [![LinkedIn][linkedin-shield]][linkedin-url]
24
+ [![MIT License][license-shield]][license-url]
25
+ [![Stargazers][stars-shield]][stars-url]
27
26
 
28
27
  </div>
29
28
 
@@ -118,17 +117,14 @@ Meteostat is licensed under the [**MIT License**](https://github.com/meteostat/m
118
117
 
119
118
  <!-- MARKDOWN LINKS & IMAGES -->
120
119
  <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
121
- [contributors-shield]: https://img.shields.io/github/contributors/meteostat/meteostat.svg?style=for-the-badge
122
- [contributors-url]: https://github.com/meteostat/meteostat/graphs/contributors
123
- [forks-shield]: https://img.shields.io/github/forks/meteostat/meteostat.svg?style=for-the-badge
124
- [forks-url]: https://github.com/meteostat/meteostat/network/members
125
- [stars-shield]: https://img.shields.io/github/stars/meteostat/meteostat.svg?style=for-the-badge
126
- [stars-url]: https://github.com/meteostat/meteostat/stargazers
127
- [issues-shield]: https://img.shields.io/github/issues/meteostat/meteostat.svg?style=for-the-badge
120
+ [downloads-shield]: https://img.shields.io/pypi/dm/meteostat
121
+ [downloads-url]: https://pypi.org/project/meteostat/
122
+ [provider-tests-shield]: https://github.com/meteostat/meteostat/actions/workflows/provider-tests.yml/badge.svg
123
+ [provider-tests-url]: https://github.com/meteostat/meteostat/actions/workflows/provider-tests.yml
124
+ [issues-shield]: https://img.shields.io/github/issues/meteostat/meteostat.svg
128
125
  [issues-url]: https://github.com/meteostat/meteostat/issues
129
- [license-shield]: https://img.shields.io/github/license/meteostat/meteostat.svg?style=for-the-badge
130
- [license-url]: https://github.com/meteostat/meteostat/blob/main/LICENSE
131
- [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
132
- [linkedin-url]: https://www.linkedin.com/company/meteostat
126
+ [license-shield]: https://img.shields.io/github/license/meteostat/meteostat.svg
127
+ [license-url]: https://github.com/meteostat/meteostat?tab=readme-ov-file#-license
133
128
  [product-screenshot]: https://dev.meteostat.net/assets/images/example-8b6cf2a3fe2efa285bc72d7dc72c4865.png
134
-
129
+ [stars-shield]: https://img.shields.io/github/stars/meteostat/meteostat.svg
130
+ [stars-url]: https://github.com/meteostat/meteostat/stargazers
@@ -1,12 +1,11 @@
1
1
  <!-- PROJECT SHIELDS -->
2
2
  <div align="center">
3
3
 
4
- [![Contributors][contributors-shield]][contributors-url]
5
- [![Forks][forks-shield]][forks-url]
6
- [![Stargazers][stars-shield]][stars-url]
4
+ [![Downloads][downloads-shield]][downloads-url]
5
+ [![Provider Tests][provider-tests-shield]][provider-tests-url]
7
6
  [![Issues][issues-shield]][issues-url]
8
- [![Unlicense License][license-shield]][license-url]
9
- [![LinkedIn][linkedin-shield]][linkedin-url]
7
+ [![MIT License][license-shield]][license-url]
8
+ [![Stargazers][stars-shield]][stars-url]
10
9
 
11
10
  </div>
12
11
 
@@ -101,16 +100,14 @@ Meteostat is licensed under the [**MIT License**](https://github.com/meteostat/m
101
100
 
102
101
  <!-- MARKDOWN LINKS & IMAGES -->
103
102
  <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
104
- [contributors-shield]: https://img.shields.io/github/contributors/meteostat/meteostat.svg?style=for-the-badge
105
- [contributors-url]: https://github.com/meteostat/meteostat/graphs/contributors
106
- [forks-shield]: https://img.shields.io/github/forks/meteostat/meteostat.svg?style=for-the-badge
107
- [forks-url]: https://github.com/meteostat/meteostat/network/members
108
- [stars-shield]: https://img.shields.io/github/stars/meteostat/meteostat.svg?style=for-the-badge
109
- [stars-url]: https://github.com/meteostat/meteostat/stargazers
110
- [issues-shield]: https://img.shields.io/github/issues/meteostat/meteostat.svg?style=for-the-badge
103
+ [downloads-shield]: https://img.shields.io/pypi/dm/meteostat
104
+ [downloads-url]: https://pypi.org/project/meteostat/
105
+ [provider-tests-shield]: https://github.com/meteostat/meteostat/actions/workflows/provider-tests.yml/badge.svg
106
+ [provider-tests-url]: https://github.com/meteostat/meteostat/actions/workflows/provider-tests.yml
107
+ [issues-shield]: https://img.shields.io/github/issues/meteostat/meteostat.svg
111
108
  [issues-url]: https://github.com/meteostat/meteostat/issues
112
- [license-shield]: https://img.shields.io/github/license/meteostat/meteostat.svg?style=for-the-badge
113
- [license-url]: https://github.com/meteostat/meteostat/blob/main/LICENSE
114
- [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
115
- [linkedin-url]: https://www.linkedin.com/company/meteostat
109
+ [license-shield]: https://img.shields.io/github/license/meteostat/meteostat.svg
110
+ [license-url]: https://github.com/meteostat/meteostat?tab=readme-ov-file#-license
116
111
  [product-screenshot]: https://dev.meteostat.net/assets/images/example-8b6cf2a3fe2efa285bc72d7dc72c4865.png
112
+ [stars-shield]: https://img.shields.io/github/stars/meteostat/meteostat.svg
113
+ [stars-url]: https://github.com/meteostat/meteostat/stargazers
@@ -12,29 +12,34 @@ The code is licensed under the MIT license.
12
12
  """
13
13
 
14
14
  __appname__ = "meteostat"
15
- __version__ = "2.0.0"
15
+ __version__ = "2.1.0"
16
16
 
17
17
  from meteostat.api.daily import daily
18
18
  from meteostat.api.hourly import hourly
19
19
  from meteostat.api.interpolate import interpolate
20
+ from meteostat.api.inventory import Inventory
20
21
  from meteostat.api.merge import merge
21
22
  from meteostat.api.monthly import monthly
22
23
  from meteostat.api.normals import normals
23
24
  from meteostat.api.point import Point
24
25
  from meteostat.api.stations import stations
26
+ from meteostat.api.timeseries import TimeSeries
25
27
  from meteostat.core.cache import purge
26
- from meteostat.core.config import config
27
- from meteostat.enumerations import Parameter, Provider, UnitSystem
28
+ from meteostat.api.config import config
29
+ from meteostat.enumerations import Granularity, Parameter, Provider, UnitSystem
28
30
  from meteostat.interpolation.lapserate import lapse_rate
29
- from meteostat.typing import Station
31
+ from meteostat.typing import Station, License
30
32
 
31
33
  # Export public API
32
34
  __all__ = [
33
35
  "config",
34
36
  "daily",
35
37
  "hourly",
38
+ "Granularity",
36
39
  "interpolate",
40
+ "Inventory",
37
41
  "lapse_rate",
42
+ "License",
38
43
  "merge",
39
44
  "monthly",
40
45
  "normals",
@@ -44,5 +49,6 @@ __all__ = [
44
49
  "purge",
45
50
  "Station",
46
51
  "stations",
52
+ "TimeSeries",
47
53
  "UnitSystem",
48
54
  ]
@@ -15,9 +15,9 @@ from meteostat.enumerations import TTL, Parameter
15
15
  from meteostat.utils.types import extract_property_type, validate_parsed_value
16
16
 
17
17
 
18
- class Config:
18
+ class ConfigService:
19
19
  """
20
- Configuration Base Class
20
+ Configuration Service for Meteostat
21
21
  """
22
22
 
23
23
  prefix: str
@@ -92,9 +92,9 @@ class Config:
92
92
  self._set_env_value(key, value)
93
93
 
94
94
 
95
- class ConfigService(Config):
95
+ class Config(ConfigService):
96
96
  """
97
- Configuration Service for Meteostat
97
+ Meteostat Configuration
98
98
 
99
99
  Manages all configuration settings including cache, network, stations,
100
100
  interpolation, and provider-specific settings. Supports loading configuration
@@ -154,5 +154,8 @@ class ConfigService(Config):
154
154
  )
155
155
  metno_user_agent: Optional[str] = None
156
156
 
157
+ # [Provider] GSA settings
158
+ gsa_api_base_url: str = "https://dataset.api.hub.geosphere.at/v1"
159
+
157
160
 
158
- config = ConfigService("MS")
161
+ config = Config("MS")
@@ -12,12 +12,19 @@ import pandas as pd
12
12
  from meteostat.api.point import Point
13
13
  from meteostat.api.timeseries import TimeSeries
14
14
  from meteostat.typing import Station
15
+ from meteostat.enumerations import Parameter
15
16
  from meteostat.interpolation.lapserate import apply_lapse_rate
16
17
  from meteostat.interpolation.nearest import nearest_neighbor
17
18
  from meteostat.interpolation.idw import inverse_distance_weighting
18
19
  from meteostat.utils.data import aggregate_sources, reshape_by_source, stations_to_df
19
20
  from meteostat.utils.geo import get_distance
20
21
  from meteostat.utils.parsers import parse_station
22
+ from meteostat.core.schema import schema_service
23
+ from meteostat.core.logger import logger
24
+
25
+
26
+ # Parameters that are categorical and should not use IDW interpolation
27
+ CATEGORICAL_PARAMETERS = {Parameter.WDIR, Parameter.CLDC, Parameter.COCO}
21
28
 
22
29
 
23
30
  def _create_timeseries(
@@ -77,6 +84,191 @@ def _add_source_columns(
77
84
  return result
78
85
 
79
86
 
87
+ def _prepare_data_with_distances(
88
+ df: pd.DataFrame, point: Point, elevation_weight: float
89
+ ) -> pd.DataFrame:
90
+ """
91
+ Add distance and elevation calculations to the DataFrame
92
+ """
93
+ # Add distance column
94
+ df["distance"] = get_distance(
95
+ point.latitude, point.longitude, df["latitude"], df["longitude"]
96
+ )
97
+
98
+ # Add effective distance column if elevation is available
99
+ if point.elevation is not None and "elevation" in df.columns:
100
+ elev_diff = np.abs(df["elevation"] - point.elevation)
101
+ df["effective_distance"] = np.sqrt(
102
+ df["distance"] ** 2 + (elev_diff * elevation_weight) ** 2
103
+ )
104
+ else:
105
+ df["effective_distance"] = df["distance"]
106
+
107
+ # Add elevation difference column
108
+ if "elevation" in df.columns and point.elevation is not None:
109
+ df["elevation_diff"] = np.abs(df["elevation"] - point.elevation)
110
+ else:
111
+ df["elevation_diff"] = np.nan
112
+
113
+ return df
114
+
115
+
116
+ def _should_use_nearest_neighbor(
117
+ df: pd.DataFrame,
118
+ point: Point,
119
+ distance_threshold: Union[int, None],
120
+ elevation_threshold: Union[int, None],
121
+ ) -> bool:
122
+ """
123
+ Determine if nearest neighbor should be used based on thresholds
124
+ """
125
+ min_distance = df["distance"].min()
126
+ use_nearest = distance_threshold is None or min_distance <= distance_threshold
127
+
128
+ if use_nearest and point.elevation is not None and "elevation" in df.columns:
129
+ min_elev_diff = np.abs(df["elevation"] - point.elevation).min()
130
+ use_nearest = (
131
+ elevation_threshold is None or min_elev_diff <= elevation_threshold
132
+ )
133
+
134
+ return use_nearest
135
+
136
+
137
+ def _get_categorical_columns(df: pd.DataFrame) -> list:
138
+ """
139
+ Identify categorical columns in the data (excluding source columns)
140
+ """
141
+ data_cols = [c for c in df.columns if not c.endswith("_source")]
142
+ return [c for c in data_cols if c in CATEGORICAL_PARAMETERS]
143
+
144
+
145
+ def _interpolate_with_nearest_neighbor(
146
+ df: pd.DataFrame,
147
+ ts: TimeSeries,
148
+ point: Point,
149
+ distance_threshold: Union[int, None],
150
+ elevation_threshold: Union[int, None],
151
+ ) -> Optional[pd.DataFrame]:
152
+ """
153
+ Perform nearest neighbor interpolation with threshold filtering
154
+ """
155
+ distance_filter = (
156
+ pd.Series([True] * len(df), index=df.index)
157
+ if distance_threshold is None
158
+ else (df["distance"] <= distance_threshold)
159
+ )
160
+ elevation_filter = (
161
+ pd.Series([True] * len(df), index=df.index)
162
+ if elevation_threshold is None
163
+ else (np.abs(df["elevation"] - point.elevation) <= elevation_threshold)
164
+ )
165
+ df_filtered = df[distance_filter & elevation_filter]
166
+ return nearest_neighbor(df_filtered, ts, point)
167
+
168
+
169
+ def _interpolate_with_idw_and_categorical(
170
+ df: pd.DataFrame,
171
+ ts: TimeSeries,
172
+ point: Point,
173
+ categorical_cols: list,
174
+ power: float,
175
+ ) -> Optional[pd.DataFrame]:
176
+ """
177
+ Perform IDW interpolation for non-categorical parameters and nearest neighbor for categorical
178
+ """
179
+ # For categorical parameters, always use nearest neighbor
180
+ if categorical_cols:
181
+ df_categorical = nearest_neighbor(df, ts, point)
182
+ # Keep only categorical columns that exist in the result
183
+ existing_categorical = [
184
+ c for c in categorical_cols if c in df_categorical.columns
185
+ ]
186
+ df_categorical = (
187
+ df_categorical[existing_categorical]
188
+ if existing_categorical
189
+ else pd.DataFrame()
190
+ )
191
+ else:
192
+ df_categorical = pd.DataFrame()
193
+
194
+ # Perform IDW interpolation for all parameters
195
+ idw_func = inverse_distance_weighting(power=power)
196
+ df_idw = idw_func(df, ts, point)
197
+
198
+ # Remove categorical columns from IDW result if they exist
199
+ if not df_categorical.empty and df_idw is not None:
200
+ # Drop categorical columns from IDW result
201
+ idw_cols_to_keep = [c for c in df_idw.columns if c not in categorical_cols]
202
+ df_idw = df_idw[idw_cols_to_keep] if idw_cols_to_keep else pd.DataFrame()
203
+
204
+ # Combine categorical (nearest) and non-categorical (IDW) results
205
+ if not df_categorical.empty and not df_idw.empty:
206
+ return pd.concat([df_idw, df_categorical], axis=1)
207
+ elif not df_categorical.empty:
208
+ return df_categorical
209
+ else:
210
+ return df_idw
211
+
212
+
213
+ def _merge_interpolation_results(
214
+ df_nearest: Optional[pd.DataFrame],
215
+ df_idw: Optional[pd.DataFrame],
216
+ use_nearest: bool,
217
+ ) -> Optional[pd.DataFrame]:
218
+ """
219
+ Merge nearest neighbor and IDW results with appropriate priority
220
+ """
221
+ if use_nearest and df_nearest is not None and len(df_nearest) > 0:
222
+ if df_idw is not None:
223
+ # Combine nearest and IDW results, prioritizing nearest values
224
+ return df_nearest.combine_first(df_idw)
225
+ else:
226
+ return df_nearest
227
+ else:
228
+ return df_idw
229
+
230
+
231
+ def _postprocess_result(
232
+ result: pd.DataFrame, df: pd.DataFrame, ts: TimeSeries
233
+ ) -> pd.DataFrame:
234
+ """
235
+ Post-process the interpolation result: drop location columns, add sources, format, reshape
236
+ """
237
+ # Drop location-related columns
238
+ result = result.drop(
239
+ [
240
+ "latitude",
241
+ "longitude",
242
+ "elevation",
243
+ "distance",
244
+ "effective_distance",
245
+ "elevation_diff",
246
+ ],
247
+ axis=1,
248
+ errors="ignore",
249
+ )
250
+
251
+ # Add source columns
252
+ result = _add_source_columns(result, df)
253
+
254
+ # Reshape by source
255
+ result = reshape_by_source(result)
256
+
257
+ # Add station index
258
+ result["station"] = "$0001"
259
+ result = result.set_index("station", append=True).reorder_levels(
260
+ ["station", "time", "source"]
261
+ )
262
+
263
+ # Reorder columns to match the canonical schema order
264
+ result = schema_service.purge(result, ts.parameters)
265
+
266
+ # Format the result using schema_service to apply proper rounding
267
+ result = schema_service.format(result, ts.granularity)
268
+
269
+ return result
270
+
271
+
80
272
  def interpolate(
81
273
  ts: TimeSeries,
82
274
  point: Point,
@@ -127,27 +319,11 @@ def interpolate(
127
319
 
128
320
  # If no data is returned, return None
129
321
  if df is None:
322
+ logger.debug("No data available for interpolation. Returning empty TimeSeries.")
130
323
  return _create_timeseries(ts, point)
131
324
 
132
- # Add distance column
133
- df["distance"] = get_distance(
134
- point.latitude, point.longitude, df["latitude"], df["longitude"]
135
- )
136
-
137
- # Add effective distance column if elevation is available
138
- if point.elevation is not None and "elevation" in df.columns:
139
- elev_diff = np.abs(df["elevation"] - point.elevation)
140
- df["effective_distance"] = np.sqrt(
141
- df["distance"] ** 2 + (elev_diff * elevation_weight) ** 2
142
- )
143
- else:
144
- df["effective_distance"] = df["distance"]
145
-
146
- # Add elevation difference column
147
- if "elevation" in df.columns and point.elevation is not None:
148
- df["elevation_diff"] = np.abs(df["elevation"] - point.elevation)
149
- else:
150
- df["elevation_diff"] = np.nan
325
+ # Prepare data with distance and elevation calculations
326
+ df = _prepare_data_with_distances(df, point, elevation_weight)
151
327
 
152
328
  # Apply lapse rate if specified and elevation is available
153
329
  if (
@@ -155,86 +331,48 @@ def interpolate(
155
331
  and point.elevation
156
332
  and df["elevation_diff"].max() >= lapse_rate_threshold
157
333
  ):
334
+ logger.debug("Applying lapse rate correction.")
158
335
  df = apply_lapse_rate(df, point.elevation, lapse_rate)
159
336
 
160
- # Check if any stations are close enough for nearest neighbor
161
- min_distance = df["distance"].min()
162
- use_nearest = distance_threshold is None or min_distance <= distance_threshold
163
- if use_nearest and point.elevation is not None and "elevation" in df.columns:
164
- # Calculate minimum elevation difference
165
- min_elev_diff = np.abs(df["elevation"] - point.elevation).min()
166
- use_nearest = (
167
- elevation_threshold is None or min_elev_diff <= elevation_threshold
168
- )
337
+ # Determine if nearest neighbor should be used
338
+ use_nearest = _should_use_nearest_neighbor(
339
+ df, point, distance_threshold, elevation_threshold
340
+ )
169
341
 
170
- # Initialize variables
342
+ # Identify categorical columns
343
+ categorical_cols = _get_categorical_columns(df)
344
+ logger.debug(f"Categorical columns identified: {categorical_cols}")
345
+
346
+ # Perform interpolation
171
347
  df_nearest = None
172
348
  df_idw = None
173
349
 
174
- # Perform nearest neighbor if applicable
175
350
  if use_nearest:
176
- # Filter applicable stations based on thresholds
177
- distance_filter = (
178
- pd.Series([True] * len(df), index=df.index)
179
- if distance_threshold is None
180
- else (df["distance"] <= distance_threshold)
181
- )
182
- elevation_filter = (
183
- pd.Series([True] * len(df), index=df.index)
184
- if elevation_threshold is None
185
- else (np.abs(df["elevation"] - point.elevation) <= elevation_threshold)
351
+ logger.debug("Using nearest neighbor interpolation.")
352
+ df_nearest = _interpolate_with_nearest_neighbor(
353
+ df, ts, point, distance_threshold, elevation_threshold
186
354
  )
187
- df_filtered = df[distance_filter & elevation_filter]
188
- df_nearest = nearest_neighbor(df_filtered, ts, point)
189
355
 
190
- # Check if we need to use IDW
356
+ # Use IDW if nearest neighbor doesn't provide complete data
191
357
  if (
192
358
  not use_nearest
193
359
  or df_nearest is None
194
360
  or len(df_nearest) == 0
195
361
  or df_nearest.isna().any().any()
196
362
  ):
197
- # Perform IDW interpolation
198
- idw_func = inverse_distance_weighting(power=power)
199
- df_idw = idw_func(df, ts, point)
363
+ logger.debug("Using IDW interpolation.")
364
+ df_idw = _interpolate_with_idw_and_categorical(
365
+ df, ts, point, categorical_cols, power
366
+ )
200
367
 
201
- # Merge DataFrames with priority to nearest neighbor
202
- if use_nearest and df_nearest is not None and len(df_nearest) > 0:
203
- if df_idw is not None:
204
- # Combine nearest and IDW results, prioritizing nearest values
205
- result = df_nearest.combine_first(df_idw)
206
- else:
207
- result = df_nearest
208
- else:
209
- result = df_idw
368
+ # Merge results
369
+ result = _merge_interpolation_results(df_nearest, df_idw, use_nearest)
210
370
 
211
371
  # If no data is returned, return None
212
372
  if result is None or result.empty:
213
373
  return _create_timeseries(ts, point)
214
374
 
215
- # Drop location-related columns & return
216
- result = result.drop(
217
- [
218
- "latitude",
219
- "longitude",
220
- "elevation",
221
- "distance",
222
- "effective_distance",
223
- "elevation_diff",
224
- ],
225
- axis=1,
226
- )
227
-
228
- # Add source columns: aggregate all columns that end with "_source"
229
- result = _add_source_columns(result, df)
230
-
231
- # Reshape by source
232
- result = reshape_by_source(result)
233
-
234
- # Add station index
235
- result["station"] = "$0001"
236
- result = result.set_index("station", append=True).reorder_levels(
237
- ["station", "time", "source"]
238
- )
375
+ # Post-process result
376
+ result = _postprocess_result(result, df, ts)
239
377
 
240
378
  return _create_timeseries(ts, point, result)
@@ -15,7 +15,7 @@ from requests import Response
15
15
  from meteostat.api.inventory import Inventory
16
16
  from meteostat.api.point import Point
17
17
  from meteostat.core.cache import cache_service
18
- from meteostat.core.config import config
18
+ from meteostat.api.config import config
19
19
  from meteostat.core.logger import logger
20
20
  from meteostat.core.network import network_service
21
21
  from meteostat.enumerations import Provider
@@ -14,7 +14,7 @@ from typing import Any, Callable, Optional
14
14
 
15
15
  import pandas as pd
16
16
 
17
- from meteostat.core.config import config
17
+ from meteostat.api.config import config
18
18
  from meteostat.core.logger import logger
19
19
 
20
20
 
@@ -18,6 +18,7 @@ from meteostat.core.schema import schema_service
18
18
  from meteostat.enumerations import Parameter, Provider
19
19
  from meteostat.typing import Station, Request
20
20
  from meteostat.utils.data import stations_to_df
21
+ from meteostat.utils.guards import request_size_guard
21
22
 
22
23
 
23
24
  class DataService:
@@ -147,6 +148,9 @@ class DataService:
147
148
  """
148
149
  Load meteorological time series data from different providers
149
150
  """
151
+ # Guard request
152
+ request_size_guard(req)
153
+
150
154
  # Convert stations to list if single Station
151
155
  stations: List[Station] = (
152
156
  cast(List[Station], req.station)
@@ -11,7 +11,7 @@ import requests
11
11
 
12
12
  from meteostat import __version__
13
13
  from meteostat.core.logger import logger
14
- from meteostat.core.config import config
14
+ from meteostat.api.config import config
15
15
 
16
16
 
17
17
  class NetworkService:
@@ -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"
@@ -5,7 +5,7 @@ import numpy as np
5
5
  import pandas as pd
6
6
 
7
7
  from meteostat.api.timeseries import TimeSeries
8
- from meteostat.core.config import config
8
+ from meteostat.api.config import config
9
9
  from meteostat.enumerations import Parameter
10
10
 
11
11
 
@@ -10,7 +10,7 @@ from typing import List, Optional
10
10
  import pandas as pd
11
11
 
12
12
  from meteostat.core.logger import logger
13
- from meteostat.core.config import config
13
+ from meteostat.api.config import config
14
14
  from meteostat.enumerations import TTL, Parameter
15
15
  from meteostat.typing import ProviderRequest, Station
16
16
  from meteostat.core.cache import cache_service
@@ -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"], inplace=True)
105
+ df = df.rename(columns=param_config["stubnames"])
106
106
 
107
107
  # Convert wide to long format
108
108
  df = pd.wide_to_long(
@@ -14,7 +14,7 @@ from zipfile import ZipFile
14
14
 
15
15
  import pandas as pd
16
16
 
17
- from meteostat.core.config import config
17
+ from meteostat.api.config import config
18
18
  from meteostat.enumerations import TTL, Parameter
19
19
  from meteostat.typing import ProviderRequest
20
20
  from meteostat.core.cache import cache_service
@@ -18,7 +18,7 @@ from meteostat.enumerations import TTL, Parameter
18
18
  from meteostat.core.logger import logger
19
19
  from meteostat.typing import ProviderRequest, Station
20
20
  from meteostat.core.cache import cache_service
21
- from meteostat.core.config import config
21
+ from meteostat.api.config import config
22
22
  from meteostat.utils.conversions import ms_to_kmh
23
23
  from meteostat.providers.dwd.shared import get_condicode
24
24
  from meteostat.providers.dwd.shared import get_ftp_connection