meteostat 1.7.6__py3-none-any.whl → 2.0.1__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 (94) hide show
  1. meteostat/__init__.py +38 -19
  2. meteostat/api/config.py +158 -0
  3. meteostat/api/daily.py +76 -0
  4. meteostat/api/hourly.py +80 -0
  5. meteostat/api/interpolate.py +378 -0
  6. meteostat/api/inventory.py +59 -0
  7. meteostat/api/merge.py +103 -0
  8. meteostat/api/monthly.py +73 -0
  9. meteostat/api/normals.py +144 -0
  10. meteostat/api/point.py +30 -0
  11. meteostat/api/stations.py +234 -0
  12. meteostat/api/timeseries.py +334 -0
  13. meteostat/core/cache.py +212 -59
  14. meteostat/core/data.py +203 -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/guards.py +51 -0
  53. meteostat/utils/parsers.py +161 -0
  54. meteostat/utils/types.py +113 -0
  55. meteostat/utils/validators.py +31 -0
  56. meteostat-2.0.1.dist-info/METADATA +130 -0
  57. meteostat-2.0.1.dist-info/RECORD +64 -0
  58. {meteostat-1.7.6.dist-info → meteostat-2.0.1.dist-info}/WHEEL +1 -2
  59. meteostat/core/loader.py +0 -103
  60. meteostat/core/warn.py +0 -34
  61. meteostat/enumerations/granularity.py +0 -22
  62. meteostat/interface/base.py +0 -39
  63. meteostat/interface/daily.py +0 -118
  64. meteostat/interface/hourly.py +0 -154
  65. meteostat/interface/meteodata.py +0 -210
  66. meteostat/interface/monthly.py +0 -109
  67. meteostat/interface/normals.py +0 -245
  68. meteostat/interface/point.py +0 -143
  69. meteostat/interface/stations.py +0 -252
  70. meteostat/interface/timeseries.py +0 -237
  71. meteostat/series/aggregate.py +0 -48
  72. meteostat/series/convert.py +0 -28
  73. meteostat/series/count.py +0 -17
  74. meteostat/series/coverage.py +0 -20
  75. meteostat/series/fetch.py +0 -28
  76. meteostat/series/interpolate.py +0 -47
  77. meteostat/series/normalize.py +0 -76
  78. meteostat/series/stations.py +0 -22
  79. meteostat/units.py +0 -149
  80. meteostat/utilities/__init__.py +0 -0
  81. meteostat/utilities/aggregations.py +0 -37
  82. meteostat/utilities/endpoint.py +0 -33
  83. meteostat/utilities/helpers.py +0 -70
  84. meteostat/utilities/mutations.py +0 -89
  85. meteostat/utilities/validations.py +0 -30
  86. meteostat-1.7.6.dist-info/METADATA +0 -112
  87. meteostat-1.7.6.dist-info/RECORD +0 -39
  88. meteostat-1.7.6.dist-info/top_level.txt +0 -1
  89. /meteostat/{core → api}/__init__.py +0 -0
  90. /meteostat/{enumerations → interpolation}/__init__.py +0 -0
  91. /meteostat/{interface → providers}/__init__.py +0 -0
  92. /meteostat/{interface/interpolate.py → py.typed} +0 -0
  93. /meteostat/{series → utils}/__init__.py +0 -0
  94. {meteostat-1.7.6.dist-info → meteostat-2.0.1.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,144 @@
1
+ """
2
+ Climate Normals
3
+
4
+ Access climate normals data for one or multiple weather stations.
5
+ """
6
+
7
+ from typing import List, Optional
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from meteostat.enumerations import Parameter, Provider, Granularity
13
+ from meteostat.core.schema import schema_service
14
+ from meteostat.api.monthly import DEFAULT_PARAMETERS, monthly
15
+ from meteostat.api.timeseries import TimeSeries
16
+ from meteostat.typing import Station
17
+ from meteostat.api.point import Point
18
+ from meteostat.utils.data import reshape_by_source
19
+ from meteostat.utils.parsers import parse_year
20
+
21
+
22
+ def normals(
23
+ station: str | Station | Point | List[str | Station | Point] | pd.DataFrame,
24
+ start: int = 1961,
25
+ end: int = 1990,
26
+ parameters: Optional[List[Parameter]] = None,
27
+ providers: Optional[List[Provider]] = None,
28
+ max_missing: int = 3,
29
+ ):
30
+ """
31
+ Access climate normals data.
32
+
33
+ Parameters
34
+ ----------
35
+ station : str, Station, Point, List[str | Station | Point], pd.Index, pd.Series
36
+ Weather station(s) or Point(s) to query data for. Can be a single station/point or a list.
37
+ Points are converted to virtual stations with IDs like $0001, $0002, etc.
38
+ start : int, optional
39
+ Start year for the data query. Defaults to 1961.
40
+ end : int, optional
41
+ End year for the data query. Defaults to 1990.
42
+ parameters : List[Parameter], optional
43
+ List of parameters to include in the data query. Defaults to a set of common parameters.
44
+ providers : List[Provider], optional
45
+ List of data providers to use for the query. Defaults to the monthly provider.
46
+ max_missing : int, optional
47
+ Maximum number of missing values allowed in a month to calculate the mean. Defaults to 3.
48
+
49
+ Returns
50
+ -------
51
+ TimeSeries
52
+ A TimeSeries object containing the climate normals data for the specified
53
+ stations and parameters.
54
+ """
55
+ if parameters is None:
56
+ parameters = DEFAULT_PARAMETERS
57
+ if providers is None:
58
+ providers = [Provider.MONTHLY]
59
+
60
+ def _mean(group: pd.Series):
61
+ """
62
+ Calculate the monthly mean from multiple years of monthly data
63
+ """
64
+ if group.isna().sum() > max_missing:
65
+ return np.nan
66
+ return group.mean()
67
+
68
+ # Fetch monthly data for the specified stations and parameters
69
+ ts = monthly(
70
+ station,
71
+ parse_year(start),
72
+ parse_year(end, True),
73
+ parameters=parameters,
74
+ providers=providers,
75
+ )
76
+ df = ts.fetch(sources=True)
77
+
78
+ # Process data if available
79
+ if df is not None and not df.empty:
80
+ # Add station level if only a single station is provided
81
+ if not ts._multi_station:
82
+ df = pd.concat(
83
+ [df],
84
+ keys=[ts.stations.index.get_level_values("id")[0]],
85
+ names=["station"],
86
+ )
87
+
88
+ # Extract month from time index
89
+ df["month"] = df.index.get_level_values("time").month
90
+
91
+ # Create aggregation functions for different column types
92
+ agg_funcs = {}
93
+ for col in df.columns:
94
+ if col.endswith("_source"):
95
+ # For source columns, concatenate unique values
96
+ agg_funcs[col] = lambda x: ", ".join(x.dropna().unique())
97
+ elif col == "txmx":
98
+ # For temperature maximum, use max function
99
+ agg_funcs[col] = lambda x: x.max() if not x.isna().all() else np.nan
100
+ elif col == "txmn":
101
+ # For temperature minimum, use min function
102
+ agg_funcs[col] = lambda x: x.min() if not x.isna().all() else np.nan
103
+ else:
104
+ # For data columns, use the mean function
105
+ agg_funcs[col] = _mean
106
+
107
+ # Apply aggregation functions
108
+ df = df.groupby(["station", "month"]).agg(agg_funcs)
109
+ df = df.rename_axis(index={"month": "time"})
110
+
111
+ # Separate parameter columns from source columns for formatting
112
+ param_cols = [str(param) for param in parameters if str(param) in df.columns]
113
+ source_cols = [col for col in df.columns if col.endswith("_source")]
114
+
115
+ # Format only the parameter columns
116
+ if param_cols:
117
+ df_data = schema_service.format(df[param_cols], Granularity.NORMALS)
118
+ # Combine formatted parameter columns with unformatted source columns
119
+ if source_cols:
120
+ df = pd.concat([df_data, df[source_cols]], axis=1)
121
+ else:
122
+ df = df_data
123
+
124
+ # Reshape data by source for each station
125
+ df_fragments = []
126
+ for s in df.index.get_level_values("station").unique():
127
+ station_df = df.loc[s]
128
+ fragment = reshape_by_source(station_df)
129
+ fragment = pd.concat([fragment], keys=[s], names=["station"])
130
+ df_fragments.append(fragment)
131
+
132
+ df = pd.concat(df_fragments)
133
+ else:
134
+ df = None
135
+
136
+ # Return the final TimeSeries object
137
+ return TimeSeries(
138
+ granularity=Granularity.NORMALS,
139
+ stations=ts.stations,
140
+ df=df,
141
+ start=ts.start,
142
+ end=ts.end,
143
+ multi_station=ts._multi_station,
144
+ )
meteostat/api/point.py ADDED
@@ -0,0 +1,30 @@
1
+ """
2
+ Point Class
3
+
4
+ A geographical point used for querying nearby weather stations and spatial interpolation.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+
10
+ class Point:
11
+ """
12
+ A geographical point
13
+ """
14
+
15
+ latitude: float
16
+ longitude: float
17
+ elevation: Optional[int]
18
+
19
+ def __init__(
20
+ self, latitude: float, longitude: float, elevation: Optional[int] = None
21
+ ) -> None:
22
+ if latitude < -90 or latitude > 90:
23
+ raise ValueError("Latitude must be between -90 and 90")
24
+
25
+ if longitude < -180 or longitude > 180:
26
+ raise ValueError("Longitude must be between -180 and 180")
27
+
28
+ self.latitude = latitude
29
+ self.longitude = longitude
30
+ self.elevation = elevation
@@ -0,0 +1,234 @@
1
+ """
2
+ Stations Module
3
+
4
+ Provides the Stations class for working with weather station metadata.
5
+ """
6
+
7
+ import os
8
+ from typing import List, Optional
9
+ from io import BytesIO
10
+ import sqlite3
11
+
12
+ import pandas as pd
13
+
14
+ from requests import Response
15
+ from meteostat.api.inventory import Inventory
16
+ from meteostat.api.point import Point
17
+ from meteostat.core.cache import cache_service
18
+ from meteostat.api.config import config
19
+ from meteostat.core.logger import logger
20
+ from meteostat.core.network import network_service
21
+ from meteostat.enumerations import Provider
22
+ from meteostat.typing import Station
23
+
24
+
25
+ class Stations:
26
+ """
27
+ Stations Database
28
+ """
29
+
30
+ def _fetch_file(self, stream=False) -> Response:
31
+ """
32
+ Download the SQLite database file from the configured URL
33
+ """
34
+ urls = config.stations_db_endpoints
35
+
36
+ if not urls:
37
+ raise Exception("No stations database URLs configured")
38
+
39
+ response = network_service.get_from_mirrors(urls, stream=stream)
40
+
41
+ if response is None:
42
+ raise Exception("Failed to download the database file")
43
+
44
+ return response
45
+
46
+ def _get_file_path(self) -> str:
47
+ """
48
+ Get the file path for the SQLite database
49
+ """
50
+ filepath = config.stations_db_file
51
+ ttl = config.stations_db_ttl
52
+
53
+ if os.path.exists(filepath) and not cache_service.is_stale(filepath, ttl):
54
+ return filepath
55
+
56
+ # Download the database file
57
+ response = self._fetch_file(stream=True)
58
+
59
+ # Create cache directory if it doesn't exist
60
+ cache_service.create_cache_dir()
61
+
62
+ with open(filepath, "wb") as file:
63
+ for chunk in response.iter_content(chunk_size=8192):
64
+ file.write(chunk)
65
+
66
+ return filepath
67
+
68
+ def _connect_memory(self) -> sqlite3.Connection:
69
+ """
70
+ Create an in-memory SQLite database and load the downloaded database file into it
71
+ """
72
+ # Download the database file
73
+ response = self._fetch_file()
74
+
75
+ # Create an in-memory SQLite database
76
+ conn = sqlite3.connect(":memory:")
77
+
78
+ # Read the downloaded database file into memory
79
+ content = BytesIO(response.content)
80
+
81
+ # Convert bytes to string and write the content to the in-memory database
82
+ conn.deserialize(content.read())
83
+
84
+ return conn
85
+
86
+ def _connect_fs(self) -> sqlite3.Connection:
87
+ """
88
+ Connect to the SQLite database file on the filesystem
89
+ """
90
+ file = self._get_file_path()
91
+
92
+ if not file:
93
+ raise FileNotFoundError("SQLite database file not found")
94
+
95
+ conn = sqlite3.connect(file)
96
+
97
+ return conn
98
+
99
+ def connect(self, in_memory: Optional[bool] = None) -> sqlite3.Connection:
100
+ """
101
+ Connect to the database
102
+ """
103
+ if in_memory is None:
104
+ in_memory = config.stations_db_file is None
105
+
106
+ logger.info("Connecting to stations database (in_memory=%s)", in_memory)
107
+
108
+ if in_memory:
109
+ return self._connect_memory()
110
+
111
+ return self._connect_fs()
112
+
113
+ def query(
114
+ self,
115
+ sql: str,
116
+ index_col: Optional[str | list] = None,
117
+ params: Optional[tuple | dict] = None,
118
+ ) -> pd.DataFrame:
119
+ """
120
+ Execute a SQL query and return the result as a DataFrame
121
+ """
122
+ with self.connect() as conn:
123
+ df = pd.read_sql(sql, conn, index_col=index_col, params=params)
124
+
125
+ return df
126
+
127
+ def meta(self, station: str) -> Station:
128
+ """
129
+ Get meta data for a specific weather station
130
+ """
131
+ meta = self.query(
132
+ """
133
+ SELECT
134
+ `stations`.*,
135
+ `names`.`name` as `name`
136
+ FROM `stations`
137
+ LEFT JOIN `names` ON `stations`.`id` = `names`.`station`
138
+ AND `names`.`language` = 'en'
139
+ WHERE `stations`.`id` LIKE ?
140
+ """,
141
+ index_col="id",
142
+ params=(station,),
143
+ ).to_dict("records")[0]
144
+
145
+ identifiers = self.query(
146
+ "SELECT `key`, `value` FROM `identifiers` WHERE `station` LIKE ?",
147
+ params=(station,),
148
+ ).to_dict("records")
149
+
150
+ return Station(
151
+ id=station,
152
+ **meta,
153
+ identifiers={
154
+ identifier["key"]: identifier["value"] for identifier in identifiers
155
+ },
156
+ )
157
+
158
+ def inventory(
159
+ self, station: str | List[str], providers: Optional[List[Provider]] = None
160
+ ) -> Inventory:
161
+ """
162
+ Get inventory records for a single weather station
163
+ """
164
+ query = "SELECT station, provider, parameter, start, end, completeness FROM `inventory`"
165
+ station_list = station if isinstance(station, list) else [station]
166
+
167
+ # Generate the right number of placeholders (?, ?, ?, ...)
168
+ placeholders = ", ".join(["?"] * len(station_list))
169
+ # Add the placeholders to the query
170
+ query += f" WHERE `station` IN ({placeholders})"
171
+ # Add the stations to the params
172
+ params = tuple(station_list)
173
+
174
+ if providers:
175
+ # Generate the right number of placeholders (?, ?, ?, ...)
176
+ placeholders = ", ".join(["?"] * len(providers))
177
+ # Add the placeholders to the query
178
+ query += f" AND provider IN ({placeholders})"
179
+ # Add the providers to the params
180
+ params += tuple(providers)
181
+
182
+ df = self.query(
183
+ query, index_col=["station", "provider", "parameter"], params=params
184
+ )
185
+
186
+ return Inventory(df)
187
+
188
+ def nearby(self, point: Point, radius=50000, limit=100) -> pd.DataFrame:
189
+ """
190
+ Get a list of weather station IDs ordered by distance
191
+ """
192
+ return self.query(
193
+ """
194
+ SELECT
195
+ `id`,
196
+ `names`.`name` as `name`,
197
+ `country`,
198
+ `region`,
199
+ `latitude`,
200
+ `longitude`,
201
+ `elevation`,
202
+ `timezone`,
203
+ ROUND(
204
+ (
205
+ 6371000 * acos(
206
+ cos(radians(:lat)) * cos(radians(`latitude`)) *
207
+ cos(radians(`longitude`) - radians(:lon)) +
208
+ sin(radians(:lat)) * sin(radians(`latitude`))
209
+ )
210
+ ),
211
+ 1
212
+ ) AS `distance`
213
+ FROM
214
+ `stations`
215
+ INNER JOIN `names` ON `stations`.`id` = `names`.`station`
216
+ AND `names`.`language` = "en"
217
+ WHERE
218
+ `distance` <= :radius
219
+ ORDER BY
220
+ `distance`
221
+ LIMIT
222
+ :limit
223
+ """,
224
+ index_col="id",
225
+ params={
226
+ "lat": point.latitude,
227
+ "lon": point.longitude,
228
+ "radius": radius,
229
+ "limit": limit,
230
+ },
231
+ )
232
+
233
+
234
+ stations = Stations()