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,66 @@
1
+ """
2
+ The code is licensed under the MIT license.
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import Optional
7
+
8
+ import pandas as pd
9
+
10
+ from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
11
+ from meteostat.typing import ProviderRequest
12
+ from meteostat.api.config import config
13
+ from meteostat.core.cache import cache_service
14
+ from meteostat.utils.data import reshape_by_source
15
+
16
+
17
+ ENDPOINT = config.hourly_endpoint
18
+
19
+
20
+ def get_ttl(_station: str, year: int) -> int:
21
+ """
22
+ Get TTL based on year
23
+
24
+ Current + previous year = one day
25
+ Else = 30 days
26
+ """
27
+ current_year = datetime.now().year
28
+ return 60 * 60 * 24 if current_year - year < 2 else 60 * 60 * 24 * 30
29
+
30
+
31
+ @cache_service.cache(get_ttl, "pickle")
32
+ @handle_exceptions
33
+ def get_df(station: str, year: int) -> Optional[pd.DataFrame]:
34
+ """
35
+ Get CSV file from Meteostat and convert to DataFrame
36
+ """
37
+ file_url = ENDPOINT.format(station=station, year=str(year))
38
+
39
+ df = pd.read_csv(file_url, sep=",", compression="gzip")
40
+
41
+ time_cols = df.columns[0:4]
42
+ df["time"] = pd.to_datetime(df[time_cols])
43
+ df = df.drop(time_cols, axis=1).set_index("time")
44
+
45
+ return reshape_by_source(df)
46
+
47
+
48
+ @filter_model_data
49
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
50
+ """
51
+ Fetch hourly weather data from Meteostat's central data repository
52
+ """
53
+ if req.start is None or req.end is None:
54
+ return None
55
+
56
+ # Get a list of relevant years
57
+ years = range(req.start.year, req.end.year + 1)
58
+ # Get list of annual DataFrames
59
+ df_yearly = [get_df(req.station.id, year) for year in years]
60
+ # Concatenate into a single DataFrame
61
+ df = (
62
+ pd.concat(df_yearly)
63
+ if len(df_yearly) and not all(d is None for d in df_yearly)
64
+ else None
65
+ )
66
+ return df
@@ -0,0 +1,45 @@
1
+ """
2
+ The code is licensed under the MIT license.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ import pandas as pd
8
+
9
+ from meteostat.api.config import config
10
+ from meteostat.enumerations import TTL
11
+ from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
12
+ from meteostat.typing import ProviderRequest
13
+ from meteostat.core.cache import cache_service
14
+ from meteostat.utils.data import reshape_by_source
15
+
16
+ ENDPOINT = config.monthly_endpoint
17
+
18
+
19
+ @cache_service.cache(TTL.MONTH, "pickle")
20
+ @handle_exceptions
21
+ def get_df(station: str) -> Optional[pd.DataFrame]:
22
+ """
23
+ Get CSV file from Meteostat and convert to DataFrame
24
+ """
25
+ file_url = ENDPOINT.format(station=station)
26
+
27
+ df = pd.read_csv(file_url, sep=",", compression="gzip")
28
+
29
+ time_cols = df.columns[0:2]
30
+ df["date"] = (
31
+ df["year"].astype(str) + "-" + df["month"].astype(str).str.zfill(2) + "-01"
32
+ )
33
+ df["time"] = pd.to_datetime(df["date"])
34
+ df = df.drop(time_cols, axis=1).drop("date", axis=1).set_index("time")
35
+
36
+ return reshape_by_source(df)
37
+
38
+
39
+ @filter_model_data
40
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
41
+ """
42
+ Fetch monthly weather data from Meteostat's central data repository
43
+ """
44
+ df = get_df(req.station.id)
45
+ return df
@@ -0,0 +1,106 @@
1
+ from typing import Optional
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ from meteostat.enumerations import Parameter, Provider
7
+ from meteostat.api.daily import daily
8
+ from meteostat.typing import ProviderRequest
9
+ from meteostat.utils.data import aggregate_sources, reshape_by_source
10
+ from meteostat.utils.parsers import parse_month
11
+
12
+
13
+ def monthly_mean(group: pd.Series):
14
+ """
15
+ Calculate the monthly mean from a series of daily data
16
+ """
17
+ if group.isna().sum() > 3:
18
+ return np.nan
19
+ return group.interpolate(axis=0).mean()
20
+
21
+
22
+ def monthly_sum(group: pd.Series):
23
+ """
24
+ Calculate the monthly sum from a series of daily data
25
+ """
26
+ if group.isna().sum() > 0:
27
+ return np.nan
28
+ return group.sum()
29
+
30
+
31
+ def monthly_min(group: pd.Series):
32
+ """
33
+ Calculate the absolute minimum from a series of daily data
34
+ """
35
+ if group.isna().sum() > 0:
36
+ return np.nan
37
+ return group.interpolate(axis=0).min()
38
+
39
+
40
+ def monthly_max(group: pd.Series):
41
+ """
42
+ Calculate the absolute maximum from a series of daily data
43
+ """
44
+ if group.isna().sum() > 0:
45
+ return np.nan
46
+ return group.interpolate(axis=0).max()
47
+
48
+
49
+ # Available parameters with source column and aggregation method
50
+ PARAMETER_AGGS = {
51
+ Parameter.TEMP: (Parameter.TEMP, monthly_mean),
52
+ Parameter.TMIN: (Parameter.TMIN, monthly_mean),
53
+ Parameter.TMAX: (Parameter.TMAX, monthly_mean),
54
+ Parameter.TXMN: (Parameter.TMIN, monthly_min),
55
+ Parameter.TXMX: (Parameter.TMAX, monthly_max),
56
+ Parameter.PRCP: (Parameter.PRCP, monthly_sum),
57
+ Parameter.PRES: (Parameter.PRES, monthly_mean),
58
+ Parameter.TSUN: (Parameter.TSUN, monthly_sum),
59
+ }
60
+
61
+
62
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
63
+ """
64
+ Fetch daily weather data from Meteostat's central data
65
+ repository and aggregate to monthly granularity
66
+ """
67
+ # Get all source columns
68
+ source_cols = list(dict.fromkeys([PARAMETER_AGGS[p][0] for p in req.parameters]))
69
+
70
+ # Get daily DataFrame
71
+ ts_daily = daily(
72
+ req.station.id,
73
+ parse_month(req.start),
74
+ parse_month(req.end, is_end=True),
75
+ parameters=source_cols,
76
+ providers=[Provider.DAILY],
77
+ )
78
+
79
+ df_daily = ts_daily.fetch(fill=True, sources=True)
80
+
81
+ # If no daily data is available, exit
82
+ if df_daily is None:
83
+ return None
84
+
85
+ # Create monthly aggregations
86
+ df = pd.DataFrame()
87
+ for parameter in req.parameters:
88
+ [daily_param_name, agg_func] = PARAMETER_AGGS[parameter]
89
+ df[parameter] = (
90
+ df_daily[daily_param_name]
91
+ .groupby(pd.Grouper(level="time", freq="MS"))
92
+ .agg(agg_func)
93
+ )
94
+ df[f"{parameter}_source"] = (
95
+ df_daily[f"{daily_param_name}_source"]
96
+ .groupby(pd.Grouper(level="time", freq="MS"))
97
+ .agg(aggregate_sources)
98
+ )
99
+
100
+ # Adjust DataFrame and add index
101
+ df = df.round(1)
102
+ df.index = pd.to_datetime(df.index.astype("datetime64").dt.date) # type: ignore[union-attr]
103
+ df.index.name = "time"
104
+
105
+ # Return final DataFrame
106
+ return reshape_by_source(df)
@@ -0,0 +1,93 @@
1
+ import functools
2
+ from urllib.error import HTTPError
3
+ from typing import Optional, Callable, TypeVar
4
+
5
+ import pandas as pd
6
+
7
+ from meteostat.api.config import config
8
+ from meteostat.core.logger import logger
9
+ from meteostat.core.providers import provider_service
10
+ from meteostat.enumerations import Grade
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ def _get_station_year_info(args: tuple) -> str:
16
+ """
17
+ Get station and year info
18
+ """
19
+ station = args[0] if len(args) > 0 else None
20
+ year = args[1] if len(args) > 1 else None
21
+
22
+ info = "no station or year info"
23
+
24
+ if station is not None:
25
+ info = f'station "{station}"'
26
+ if year is not None:
27
+ info += f' and year "{year}"'
28
+
29
+ return info
30
+
31
+
32
+ def handle_exceptions(func: Callable[..., Optional[T]]) -> Callable[..., Optional[T]]:
33
+ """
34
+ Decorator to handle exceptions during data fetching
35
+ """
36
+
37
+ @functools.wraps(func)
38
+ def wrapper(*args, **kwargs) -> Optional[T]:
39
+ try:
40
+ return func(*args, **kwargs)
41
+ except HTTPError as error:
42
+ if error.code == 404:
43
+ logger.info(
44
+ f"Data file for {_get_station_year_info(args)} was not found"
45
+ )
46
+ else:
47
+ logger.warning(
48
+ f"HTTP error while loading data file for {_get_station_year_info(args)} (status: {error.code})",
49
+ exc_info=True,
50
+ )
51
+ except Exception:
52
+ logger.warning(
53
+ f"Could not load data file for {_get_station_year_info(args)}",
54
+ exc_info=True,
55
+ )
56
+ return None
57
+
58
+ return wrapper
59
+
60
+
61
+ def filter_model_data(func: Callable[..., Optional[T]]) -> Callable[..., Optional[T]]:
62
+ """
63
+ Decorator to filter out model/forecast data based on configuration
64
+ """
65
+
66
+ @functools.wraps(func)
67
+ def wrapper(*args, **kwargs) -> Optional[T]:
68
+ result = func(*args, **kwargs)
69
+
70
+ if (
71
+ not config.include_model_data
72
+ and isinstance(result, pd.DataFrame)
73
+ and result is not None
74
+ ):
75
+ logger.debug("Filtering out model/forecast data")
76
+
77
+ excluded_providers = [
78
+ provider.id
79
+ for provider in provider_service.providers
80
+ if provider.grade
81
+ in (
82
+ Grade.FORECAST,
83
+ Grade.ANALYSIS,
84
+ )
85
+ ]
86
+
87
+ mask = result.index.get_level_values("source").isin(excluded_providers)
88
+
89
+ result = result[~mask]
90
+
91
+ return result
92
+
93
+ return wrapper
@@ -0,0 +1,186 @@
1
+ from typing import Optional, Union
2
+ from urllib.error import HTTPError
3
+
4
+ import pandas as pd
5
+
6
+ from meteostat.api.config import config
7
+ from meteostat.enumerations import TTL, Parameter
8
+ from meteostat.core.logger import logger
9
+ from meteostat.core.network import network_service
10
+ from meteostat.typing import ProviderRequest
11
+ from meteostat.utils.conversions import percentage_to_okta
12
+ from meteostat.core.cache import cache_service
13
+
14
+
15
+ ENDPOINT = config.metno_forecast_endpoint
16
+ USER_AGENT = config.metno_user_agent
17
+ CONDICODES = {
18
+ "clearsky": 1,
19
+ "cloudy": 3,
20
+ "fair": 2,
21
+ "fog": 5,
22
+ "heavyrain": 9,
23
+ "heavyrainandthunder": 26,
24
+ "heavyrainshowers": 18,
25
+ "heavyrainshowersandthunder": 26,
26
+ "heavysleet": 13,
27
+ "heavysleetandthunder": 26,
28
+ "heavysleetshowers": 20,
29
+ "heavysleetshowersandthunder": 26,
30
+ "heavysnow": 16,
31
+ "heavysnowandthunder": 26,
32
+ "heavysnowshowers": 22,
33
+ "heavysnowshowersandthunder": 26,
34
+ "lightrain": 7,
35
+ "lightrainandthunder": 25,
36
+ "lightrainshowers": 17,
37
+ "lightrainshowersandthunder": 25,
38
+ "lightsleet": 12,
39
+ "lightsleetandthunder": 25,
40
+ "lightsleetshowers": 19,
41
+ "lightsnow": 14,
42
+ "lightsnowandthunder": 25,
43
+ "lightsnowshowers": 21,
44
+ "lightssleetshowersandthunder": 25,
45
+ "lightssnowshowersandthunder": 25,
46
+ "partlycloudy": 3,
47
+ "rain": 8,
48
+ "rainandthunder": 25,
49
+ "rainshowers": 17,
50
+ "rainshowersandthunder": 25,
51
+ "sleet": 12,
52
+ "sleetandthunder": 25,
53
+ "sleetshowers": 19,
54
+ "sleetshowersandthunder": 25,
55
+ "snow": 15,
56
+ "snowandthunder": 25,
57
+ "snowshowers": 21,
58
+ "snowshowersandthunder": 25,
59
+ }
60
+
61
+
62
+ def get_condicode(code: str) -> Union[int, None]:
63
+ """
64
+ Map Met.no symbol codes to Meteostat condition codes
65
+
66
+ Documentation: https://api.met.no/weatherapi/weathericon/2.0/documentation
67
+ """
68
+ return CONDICODES.get(str(code).split("_")[0], None)
69
+
70
+
71
+ def safe_get(data, keys, default=None, transform=lambda x: x):
72
+ """
73
+ Safely get a nested value from a dictionary, with an optional transformation.
74
+
75
+ :param data: The dictionary to get the value from.
76
+ :param keys: A list of keys to navigate through the dictionary.
77
+ :param default: The default value to return if any key is missing.
78
+ :param transform: A function to apply to the retrieved value if found.
79
+ :return: The retrieved value or the default.
80
+ """
81
+ for key in keys:
82
+ data = data.get(key)
83
+ if data is None:
84
+ return default
85
+ return transform(data)
86
+
87
+
88
+ def map_data(record):
89
+ """
90
+ Map Met.no JSON data to Meteostat column names
91
+ """
92
+ details_instant = ["data", "instant", "details"]
93
+ details_next_1_hour = ["data", "next_1_hours", "details"]
94
+
95
+ return {
96
+ "time": record["time"],
97
+ Parameter.TEMP: safe_get(record, details_instant + ["air_temperature"]),
98
+ Parameter.CLDC: safe_get(
99
+ record,
100
+ details_instant + ["cloud_area_fraction"],
101
+ transform=percentage_to_okta,
102
+ ),
103
+ Parameter.RHUM: safe_get(record, details_instant + ["relative_humidity"]),
104
+ Parameter.PRCP: safe_get(
105
+ record, details_next_1_hour + ["precipitation_amount"]
106
+ ),
107
+ Parameter.WSPD: safe_get(
108
+ record, details_instant + ["wind_speed"], transform=lambda x: x * 3.6
109
+ ),
110
+ Parameter.WPGT: safe_get(
111
+ record,
112
+ details_instant + ["wind_speed_of_gust"],
113
+ transform=lambda x: x * 3.6,
114
+ ),
115
+ Parameter.WDIR: safe_get(
116
+ record,
117
+ details_instant + ["wind_from_direction"],
118
+ transform=lambda x: int(round(x)),
119
+ ),
120
+ Parameter.PRES: safe_get(
121
+ record, details_instant + ["air_pressure_at_sea_level"]
122
+ ),
123
+ Parameter.COCO: safe_get(
124
+ record,
125
+ ["data", "next_1_hours", "summary", "symbol_code"],
126
+ transform=get_condicode,
127
+ ),
128
+ }
129
+
130
+
131
+ @cache_service.cache(TTL.HOUR, "pickle")
132
+ def get_df(latitude: float, longitude: float, elevation: int) -> Optional[pd.DataFrame]:
133
+ file_url = ENDPOINT.format(
134
+ latitude=latitude,
135
+ longitude=longitude,
136
+ elevation=elevation,
137
+ )
138
+
139
+ headers = {"User-Agent": USER_AGENT}
140
+
141
+ try:
142
+ response = network_service.get(file_url, headers=headers)
143
+
144
+ # Raise an exception if the request was unsuccessful
145
+ response.raise_for_status()
146
+
147
+ # Parse the JSON content into a DataFrame
148
+ data = response.json()
149
+
150
+ # Create DataFrame
151
+ df = pd.DataFrame(map(map_data, data["properties"]["timeseries"]))
152
+
153
+ # Handle time column & set index
154
+ df["time"] = pd.to_datetime(df["time"])
155
+ df = df.set_index(["time"])
156
+
157
+ # Remove the UTC timezone from the time index
158
+ df.index = df.index.tz_localize(None) # type: ignore[union-attr]
159
+
160
+ # Shift prcp and coco columns by 1 (as they refer to the next hour)
161
+ df["prcp"] = df["prcp"].shift(1)
162
+ df["coco"] = df["coco"].shift(1)
163
+
164
+ return df
165
+
166
+ except HTTPError as error:
167
+ logger.warning(
168
+ f"Couldn't load weather forecast from met.no (status: {error.status})"
169
+ )
170
+
171
+ except Exception as error:
172
+ logger.error(error, exc_info=True)
173
+
174
+
175
+ 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
+ return get_df(
183
+ req.station.latitude,
184
+ req.station.longitude,
185
+ req.station.elevation,
186
+ )