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,228 @@
1
+ """
2
+ Process GHCND data
3
+
4
+ This code is based on an implementation by
5
+ Aaron Penne (https://github.com/aaronpenne).
6
+
7
+ The code is licensed under the MIT license.
8
+ """
9
+
10
+ from io import StringIO
11
+ from ftplib import FTP
12
+ from typing import Optional
13
+
14
+ from numpy import nan
15
+ import pandas as pd
16
+
17
+ from meteostat.enumerations import TTL, Parameter
18
+ from meteostat.typing import ProviderRequest
19
+ from meteostat.core.cache import cache_service
20
+ from meteostat.utils.conversions import ms_to_kmh, percentage_to_okta
21
+
22
+ FTP_SERVER = "ftp.ncdc.noaa.gov"
23
+ COLUMN_NAMES = {
24
+ "MM/DD/YYYY": "time",
25
+ "TMAX": Parameter.TMAX,
26
+ "TMIN": Parameter.TMIN,
27
+ "TAVG": Parameter.TEMP,
28
+ "PRCP": Parameter.PRCP,
29
+ "SNWD": Parameter.SNWD,
30
+ "AWDR": Parameter.WDIR,
31
+ "AWND": Parameter.WSPD,
32
+ "TSUN": Parameter.TSUN,
33
+ "WSFG": Parameter.WPGT,
34
+ "ACMC": Parameter.CLDC,
35
+ }
36
+
37
+
38
+ def connect_to_ftp():
39
+ """
40
+ Connect to FTP server
41
+ """
42
+ ftp = FTP(FTP_SERVER)
43
+ ftp.login()
44
+
45
+ return ftp
46
+
47
+
48
+ def get_flags(string):
49
+ """
50
+ Get flags, replacing empty flags with '_' for clarity (' S ' becomes '_S_')
51
+ """
52
+ m_flag = string.read(1)
53
+ m_flag = m_flag if m_flag.strip() else "_"
54
+ q_flag = string.read(1)
55
+ q_flag = q_flag if q_flag.strip() else "_"
56
+ s_flag = string.read(1)
57
+ s_flag = s_flag if s_flag.strip() else "_"
58
+
59
+ return [m_flag + q_flag + s_flag]
60
+
61
+
62
+ def create_df(element, dict_element):
63
+ """
64
+ Create dataframes from dicts, turn indices into date strings (YYYY-MM-DD)
65
+ """
66
+ element = element.upper()
67
+ df_element = pd.DataFrame(dict_element)
68
+
69
+ # Add dates (YYYY-MM-DD) as index on df. Pad days with zeros to two places
70
+ df_element.index = (
71
+ df_element["YEAR"]
72
+ + "-"
73
+ + df_element["MONTH"]
74
+ + "-"
75
+ + df_element["DAY"].str.zfill(2)
76
+ )
77
+ df_element.index.name = "DATE"
78
+
79
+ # Arrange columns so ID, YEAR, MONTH, DAY are at front. Leaving them in
80
+ # for plotting later - https://stackoverflow.com/a/31396042
81
+ for col in ["DAY", "MONTH", "YEAR", "ID"]:
82
+ df_element = move_col_to_front(col, df_element)
83
+
84
+ # Convert numerical values to float
85
+ df_element.loc[:, element] = df_element.loc[:, element].astype(float)
86
+
87
+ return df_element
88
+
89
+
90
+ def move_col_to_front(element, df):
91
+ """
92
+ Move DataFrame column to position 0
93
+ """
94
+ element = element.upper()
95
+ cols = df.columns.tolist()
96
+ cols.insert(0, cols.pop(cols.index(element)))
97
+ df = df.reindex(columns=cols)
98
+
99
+ return df
100
+
101
+
102
+ # pylint: disable=too-many-locals
103
+ def dly_to_df(ftp, station_id):
104
+ """
105
+ Convert .dly files to DataFrame
106
+ """
107
+ ftp_filename = station_id + ".dly"
108
+
109
+ # Write .dly file to stream using StringIO using FTP command 'RETR'
110
+ stream = StringIO()
111
+ ftp.retrlines("RETR " + "/pub/data/ghcn/daily/all/" + ftp_filename, stream.write)
112
+
113
+ # Move to first char in file
114
+ stream.seek(0)
115
+
116
+ # File params
117
+ num_chars_line = 269
118
+
119
+ # Read through entire StringIO stream (the .dly file)
120
+ # and collect the data
121
+ all_dicts = {}
122
+ element_flag = {}
123
+ index = 0
124
+
125
+ while True:
126
+ index += 1
127
+
128
+ # Read metadata for each line
129
+ # (one month of data for a particular element per line)
130
+ stream.read(11) # station ID (not used in this loop)
131
+ year = stream.read(4)
132
+ month = stream.read(2)
133
+ day = 0
134
+ element = stream.read(4)
135
+
136
+ # If this is blank then we've reached EOF and should exit loop
137
+ if not element:
138
+ break
139
+
140
+ # Loop through each day in rest of row,
141
+ # break if current position is end of row
142
+ while stream.tell() % num_chars_line != 0:
143
+ day += 1
144
+ # Fill in contents of each dict depending on element type in
145
+ # current row
146
+ if day == 1:
147
+ try:
148
+ element_flag[element]
149
+ except BaseException:
150
+ element_flag[element] = 1
151
+ all_dicts[element] = {}
152
+ all_dicts[element]["ID"] = []
153
+ all_dicts[element]["YEAR"] = []
154
+ all_dicts[element]["MONTH"] = []
155
+ all_dicts[element]["DAY"] = []
156
+ all_dicts[element][element.upper()] = []
157
+ all_dicts[element][element.upper() + "_FLAGS"] = []
158
+
159
+ value = stream.read(5)
160
+ flags = get_flags(stream)
161
+ if value == "-9999":
162
+ continue
163
+ all_dicts[element]["ID"] += [station_id]
164
+ all_dicts[element]["YEAR"] += [year]
165
+ all_dicts[element]["MONTH"] += [month]
166
+ all_dicts[element]["DAY"] += [str(day)]
167
+ all_dicts[element][element.upper()] += [value]
168
+ all_dicts[element][element.upper() + "_FLAGS"] += flags
169
+
170
+ # Create dataframes from dict
171
+ all_dfs = {}
172
+ for element in list(all_dicts.keys()):
173
+ all_dfs[element] = create_df(element, all_dicts[element])
174
+
175
+ # Combine all element dataframes into one dataframe,
176
+ # indexed on date.
177
+ #
178
+ # pd.concat automagically aligns values to matching indices, therefore the
179
+ # data is date aligned!
180
+ list_dfs = []
181
+ for df in list(all_dfs.keys()):
182
+ list_dfs += [all_dfs[df]]
183
+ df_all = pd.concat(list_dfs, axis=1, sort=False)
184
+ df_all.index.name = "MM/DD/YYYY"
185
+
186
+ # Remove duplicated/broken columns and rows
187
+ # https://stackoverflow.com/a/40435354
188
+ df_all = df_all.loc[:, ~df_all.columns.duplicated()]
189
+ df_all = df_all.loc[df_all["ID"].notnull(), :]
190
+
191
+ return df_all
192
+
193
+
194
+ @cache_service.cache(TTL.DAY, "pickle")
195
+ def get_df(station: str) -> pd.DataFrame:
196
+ ftp = connect_to_ftp()
197
+ df = dly_to_df(ftp, station)
198
+ # Filter relevant columns
199
+ df = df.drop(columns=[col for col in df if col not in COLUMN_NAMES.keys()])
200
+ # Add missing columns
201
+ for col in list(COLUMN_NAMES.keys())[1:]:
202
+ if col not in df.columns:
203
+ df[col] = nan
204
+
205
+ # Rename columns
206
+ df = df.reset_index().rename(columns=COLUMN_NAMES)
207
+
208
+ # Adapt columns
209
+ df["time"] = pd.to_datetime(df["time"])
210
+ df[Parameter.TEMP] = df[Parameter.TEMP].div(10)
211
+ df[Parameter.TMIN] = df[Parameter.TMIN].div(10)
212
+ df[Parameter.TMAX] = df[Parameter.TMAX].div(10)
213
+ df[Parameter.PRCP] = df[Parameter.PRCP].div(10)
214
+ df[Parameter.SNWD] = df[Parameter.SNWD].div(10)
215
+ df[Parameter.WSPD] = df[Parameter.WSPD].div(10).apply(ms_to_kmh)
216
+ df[Parameter.WPGT] = df[Parameter.WPGT].div(10).apply(ms_to_kmh)
217
+ df[Parameter.CLDC] = df[Parameter.CLDC].apply(percentage_to_okta)
218
+
219
+ return df.set_index("time")
220
+
221
+
222
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
223
+ ghcn_id = (
224
+ req.station.identifiers["ghcn"] if "ghcn" in req.station.identifiers else None
225
+ )
226
+ if not ghcn_id:
227
+ return None
228
+ return get_df(ghcn_id)
@@ -0,0 +1,142 @@
1
+ from datetime import datetime
2
+ from typing import Optional, Union
3
+ from urllib.error import HTTPError
4
+
5
+ from numpy import isnan
6
+ import pandas as pd
7
+
8
+ from meteostat.enumerations import TTL, Parameter
9
+ from meteostat.core.logger import logger
10
+ from meteostat.core.cache import cache_service
11
+ from meteostat.typing import ProviderRequest
12
+ from meteostat.utils.conversions import ms_to_kmh, temp_dwpt_to_rhum
13
+
14
+ ISD_LITE_ENDPOINT = "https://www.ncei.noaa.gov/pub/data/noaa/isd-lite/"
15
+ COLSPECS = [
16
+ (0, 4),
17
+ (5, 7),
18
+ (8, 10),
19
+ (11, 13),
20
+ (13, 19),
21
+ (19, 25),
22
+ (25, 31),
23
+ (31, 37),
24
+ (37, 43),
25
+ (43, 49),
26
+ (49, 55),
27
+ ]
28
+ COLUMN_NAMES = [
29
+ "time",
30
+ Parameter.TEMP,
31
+ Parameter.DWPT,
32
+ Parameter.PRES,
33
+ Parameter.WDIR,
34
+ Parameter.WSPD,
35
+ Parameter.CLDC,
36
+ Parameter.PRCP,
37
+ ]
38
+
39
+
40
+ def map_sky_code(code: Union[int, str]) -> Optional[int]:
41
+ """
42
+ Only accept okta
43
+ """
44
+ return int(code) if not isnan(code) and int(code) >= 0 and int(code) <= 8 else None
45
+
46
+
47
+ def get_ttl(_usaf: str, _wban: str, year: int) -> int:
48
+ """
49
+ Get TTL based on year
50
+
51
+ Current + previous year = one day
52
+ Else = 30 days
53
+ """
54
+ current_year = datetime.now().year
55
+ return TTL.DAY if current_year - year < 2 else TTL.MONTH
56
+
57
+
58
+ @cache_service.cache(get_ttl, "pickle")
59
+ def get_df(usaf: str, wban: str, year: int) -> Optional[pd.DataFrame]:
60
+ if not usaf:
61
+ return None
62
+
63
+ filename = f"{usaf}-{wban if wban else '99999'}-{year}.gz"
64
+
65
+ try:
66
+ df = pd.read_fwf(
67
+ f"{ISD_LITE_ENDPOINT}{year}/{filename}",
68
+ parse_dates={"time": [0, 1, 2, 3]},
69
+ na_values=["-9999", -9999],
70
+ header=None,
71
+ colspecs=COLSPECS,
72
+ compression="gzip",
73
+ )
74
+
75
+ # Rename columns
76
+ df.columns = COLUMN_NAMES
77
+
78
+ # Adapt columns
79
+ df[Parameter.TEMP] = df[Parameter.TEMP].div(10)
80
+ df[Parameter.DWPT] = df[Parameter.DWPT].div(10)
81
+ df[Parameter.PRES] = df[Parameter.PRES].div(10)
82
+ df[Parameter.WSPD] = df[Parameter.WSPD].div(10).apply(ms_to_kmh)
83
+ df[Parameter.CLDC] = df[Parameter.CLDC].apply(map_sky_code)
84
+ df[Parameter.PRCP] = df[Parameter.PRCP].div(10)
85
+
86
+ # Calculate humidity data
87
+ # pylint: disable=unnecessary-lambda
88
+ df[Parameter.RHUM] = df.apply(lambda row: temp_dwpt_to_rhum(row), axis=1)
89
+
90
+ # Drop dew point column
91
+ # pylint: disable=no-member
92
+ df = df.drop(Parameter.DWPT, axis=1)
93
+
94
+ # Set index
95
+ df = df.set_index("time")
96
+
97
+ # Round decimals
98
+ return df.round(1)
99
+
100
+ except HTTPError as error:
101
+ if error.status == 404:
102
+ logger.info(f"ISD Lite file not found: {filename}")
103
+ else:
104
+ logger.warning(
105
+ f"Couldn't load ISD Lite file {filename} (status: {error.status})"
106
+ )
107
+ return None
108
+
109
+ except Exception as error:
110
+ logger.warning(error)
111
+ return None
112
+
113
+
114
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
115
+ """ """
116
+ if req.start is None or req.end is None:
117
+ return None
118
+
119
+ years = range(req.start.year, req.end.year + 1)
120
+ data = tuple(
121
+ map(
122
+ lambda i: get_df(*i),
123
+ (
124
+ (
125
+ (
126
+ req.station.identifiers["usaf"]
127
+ if "usaf" in req.station.identifiers
128
+ else None
129
+ ),
130
+ (
131
+ req.station.identifiers["wban"]
132
+ if "wban" in req.station.identifiers
133
+ else None
134
+ ),
135
+ year,
136
+ )
137
+ for year in years
138
+ ),
139
+ )
140
+ )
141
+
142
+ return pd.concat(data) if len(data) and not all(d is None for d in data) else None
@@ -0,0 +1,163 @@
1
+ """
2
+ The code is licensed under the MIT license.
3
+ """
4
+
5
+ from typing import Any, Optional
6
+
7
+ import pandas as pd
8
+ from metar import Metar
9
+
10
+ from meteostat.core.logger import logger
11
+ from meteostat.core.config import config
12
+ from meteostat.enumerations import TTL, Frequency, Parameter
13
+ from meteostat.typing import ProviderRequest
14
+ from meteostat.utils.conversions import temp_dwpt_to_rhum
15
+ from meteostat.core.cache import cache_service
16
+ from meteostat.utils.data import enforce_freq
17
+ from meteostat.core.network import network_service
18
+
19
+
20
+ ENDPOINT = config.aviationweather_endpoint
21
+ USER_AGENT = config.aviationweather_user_agent
22
+ CLDC_MAP = {
23
+ "FEW": 2, # 1-2 octas
24
+ "SCT": 4, # 3-4 octas
25
+ "BKN": 6, # 5-7 octas
26
+ "OVC": 8, # 8 octas (fully overcast)
27
+ }
28
+ COCO_MAP = {
29
+ "RA": 8,
30
+ "SHRA": 17,
31
+ "DZ": 7,
32
+ "DZRA": 7,
33
+ "FZRA": 10,
34
+ "FZDZ": 10,
35
+ "RASN": 12,
36
+ "SN": 15,
37
+ "SHSN": 21,
38
+ "SG": 12,
39
+ "IC": 12,
40
+ "PL": 24,
41
+ "GR": 24,
42
+ "GS": 24,
43
+ "FG": 5,
44
+ "BR": 5,
45
+ "MIFG": 5,
46
+ "BCFG": 5,
47
+ "HZ": 5,
48
+ "TS": 25,
49
+ "TSRA": 25,
50
+ "PO": 27,
51
+ "SQ": 27,
52
+ "FC": 27,
53
+ "SS": 27,
54
+ "DS": 27,
55
+ }
56
+
57
+
58
+ def safe_get(obj: Optional[Any]) -> Optional[Any]:
59
+ try:
60
+ if obj is not None and hasattr(obj, "value"):
61
+ return obj.value()
62
+ return None
63
+ except AttributeError:
64
+ return None
65
+
66
+
67
+ def get_cldc(report: Metar.Metar) -> Optional[int]:
68
+ """
69
+ Get cloud cover (octas) from METAR report
70
+ """
71
+ try:
72
+ cloud_cover = report.sky[0][0]
73
+ return CLDC_MAP.get(cloud_cover)
74
+ except IndexError:
75
+ return None
76
+
77
+
78
+ def get_coco(report: Metar.Metar) -> Optional[int]:
79
+ """
80
+ Get weather condition code from METAR report
81
+ """
82
+ try:
83
+ condition_code = "".join(
84
+ [item for item in report.weather[0] if item is not None]
85
+ )
86
+ return COCO_MAP.get(condition_code)
87
+ except IndexError:
88
+ return None
89
+
90
+
91
+ def map_data(record):
92
+ """
93
+ Map METAR data to Meteostat column names
94
+ """
95
+ try:
96
+ parsed_report = Metar.Metar(record)
97
+
98
+ return {
99
+ "time": parsed_report.time,
100
+ Parameter.TEMP: safe_get(parsed_report.temp),
101
+ Parameter.DWPT: safe_get(parsed_report.dewpt),
102
+ Parameter.PRCP: safe_get(parsed_report.precip_1hr),
103
+ Parameter.SNWD: safe_get(parsed_report.snowdepth),
104
+ Parameter.WDIR: safe_get(parsed_report.wind_dir),
105
+ Parameter.WSPD: safe_get(parsed_report.wind_speed),
106
+ Parameter.WPGT: safe_get(parsed_report.wind_gust),
107
+ Parameter.PRES: safe_get(parsed_report.press),
108
+ Parameter.VSBY: safe_get(parsed_report.vis),
109
+ Parameter.CLDC: get_cldc(parsed_report),
110
+ Parameter.COCO: get_coco(parsed_report),
111
+ }
112
+ except Metar.ParserError:
113
+ return None
114
+
115
+
116
+ @cache_service.cache(TTL.HOUR, "pickle")
117
+ def get_df(station: str) -> Optional[pd.DataFrame]:
118
+ """
119
+ Get CSV file from Meteostat and convert to DataFrame
120
+ """
121
+ url = ENDPOINT.format(station=station)
122
+
123
+ if not USER_AGENT:
124
+ logger.warning(
125
+ "Consider specifying a unique user agent when querying the Aviation Weather Center's data API."
126
+ )
127
+
128
+ headers = {"User-Agent": USER_AGENT}
129
+
130
+ response = network_service.get(url, headers=headers)
131
+
132
+ # Raise an exception if the request was unsuccessful
133
+ response.raise_for_status()
134
+
135
+ # Parse the JSON content into a DataFrame
136
+ data = [
137
+ item for item in map(map_data, response.text.splitlines()) if item is not None
138
+ ]
139
+
140
+ # Return None if no data is available
141
+ if not len(data):
142
+ return None
143
+
144
+ # Create DataFrame
145
+ df = pd.DataFrame(data)
146
+
147
+ # Return None if DataFrame is empty
148
+ if df.empty:
149
+ return None
150
+
151
+ # Add RHUM column
152
+ df[Parameter.RHUM] = df.apply(lambda row: temp_dwpt_to_rhum(row), axis=1)
153
+ df[Parameter.RHUM] = df[Parameter.RHUM].round()
154
+
155
+ # Set time index
156
+ df = df.set_index(["time"])
157
+
158
+ return enforce_freq(df, Frequency.HOURLY)
159
+
160
+
161
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
162
+ if "icao" in req.station.identifiers:
163
+ return get_df(req.station.identifiers["icao"])
meteostat/typing.py ADDED
@@ -0,0 +1,113 @@
1
+ """
2
+ Meteostat Typing
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import date, datetime
7
+ from typing import Callable, List, Literal, Optional
8
+
9
+ from meteostat.core.validator import Validator
10
+ from meteostat.enumerations import (
11
+ Grade,
12
+ Priority,
13
+ Granularity,
14
+ Parameter,
15
+ Provider,
16
+ Unit,
17
+ )
18
+
19
+
20
+ @dataclass
21
+ class Station:
22
+ """
23
+ A weather station
24
+
25
+ For virtual stations created from Point objects, some fields will be None.
26
+ """
27
+
28
+ id: str # The Meteostat station ID (e.g., "10637" or "$0001" for virtual stations)
29
+ name: Optional[str] = None # The (usually English) name of the station
30
+ country: Optional[str] = None # ISO 3166-1 alpha-2 country code
31
+ region: Optional[str] = None # ISO 3166-2 state or region code
32
+ identifiers: dict[str, str] = field(default_factory=dict) # Provider identifiers
33
+ latitude: Optional[float] = None # The latitude in degrees
34
+ longitude: Optional[float] = None # The longitude in degrees
35
+ elevation: Optional[int] = None # The elevation in meters
36
+ timezone: Optional[str] = None # The IANA timezone name
37
+
38
+
39
+ @dataclass
40
+ class License:
41
+ """
42
+ A license
43
+ """
44
+
45
+ commercial: bool
46
+ attribution: Optional[str] = None
47
+ name: Optional[str] = None
48
+ url: Optional[str] = None
49
+
50
+
51
+ @dataclass
52
+ class ProviderSpec:
53
+ """
54
+ A provider's meta data
55
+ """
56
+
57
+ id: Provider | str # The provider ID
58
+ name: str # A descriptive provider name
59
+ granularity: Granularity # The provider's time series granularity
60
+ priority: Priority | int # The priority of the provider
61
+ grade: Optional[Grade] # The provider's data quality grade
62
+ license: Optional[License] # The provider's license
63
+ requires: List[
64
+ Literal["id", "identifiers", "location"]
65
+ ] # Required station properties for the provider
66
+ parameters: List[Parameter] # List of supported meteorological parameters
67
+ start: date # The start date of the provider's data
68
+ end: Optional[datetime] = None # The end date of the provider's data
69
+ countries: Optional[list[str]] = None # List of supported countries
70
+ module: Optional[str] = None # Module path to the provider's API
71
+
72
+
73
+ @dataclass
74
+ class ParameterSpec:
75
+ """
76
+ A parameter's meta data
77
+ """
78
+
79
+ id: Parameter | str # The parameter ID
80
+ name: str # A descriptive parameter name
81
+ granularity: Granularity # The parameter's granularity
82
+ dtype: str # The parameter's data type
83
+ unit: Optional[Unit] = None # The parameter's data unit
84
+ validators: List[Validator | Callable] = field(
85
+ default_factory=list
86
+ ) # The parameter's validators
87
+
88
+
89
+ @dataclass
90
+ class Request:
91
+ """
92
+ A request to fetch meteorological time series data
93
+ """
94
+
95
+ granularity: Granularity # Query's time series granularity
96
+ providers: List[Provider] # Providers to query
97
+ parameters: List[Parameter] # Schema of the query's data
98
+ station: Station | List[Station] # Station(s) to query
99
+ start: Optional[datetime] = None # Start date of the query
100
+ end: Optional[datetime] = None # End date of the query
101
+ timezone: Optional[str] = None # Time zone of the query's data
102
+
103
+
104
+ @dataclass
105
+ class ProviderRequest:
106
+ """
107
+ A query to fetch meteorological data from a provider
108
+ """
109
+
110
+ station: Station # Station to query
111
+ parameters: List[Parameter] # List of meteorological parameters to query
112
+ start: Optional[datetime] # Start date of the query
113
+ end: Optional[datetime] # End date of the query