meteostat 2.0.0__py3-none-any.whl → 2.1.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 (36) hide show
  1. meteostat/__init__.py +10 -4
  2. meteostat/{core → api}/config.py +8 -5
  3. meteostat/api/interpolate.py +217 -79
  4. meteostat/api/stations.py +1 -1
  5. meteostat/core/cache.py +1 -1
  6. meteostat/core/data.py +4 -0
  7. meteostat/core/network.py +1 -1
  8. meteostat/enumerations.py +4 -0
  9. meteostat/interpolation/lapserate.py +1 -1
  10. meteostat/providers/dwd/climat.py +2 -2
  11. meteostat/providers/dwd/daily.py +1 -1
  12. meteostat/providers/dwd/hourly.py +1 -1
  13. meteostat/providers/dwd/monthly.py +6 -3
  14. meteostat/providers/dwd/shared.py +1 -1
  15. meteostat/providers/gsa/__init__.py +3 -0
  16. meteostat/providers/gsa/daily.py +194 -0
  17. meteostat/providers/gsa/hourly.py +175 -0
  18. meteostat/providers/gsa/monthly.py +192 -0
  19. meteostat/providers/gsa/synop.py +184 -0
  20. meteostat/providers/index.py +111 -0
  21. meteostat/providers/meteostat/daily.py +1 -1
  22. meteostat/providers/meteostat/hourly.py +1 -1
  23. meteostat/providers/meteostat/monthly.py +1 -1
  24. meteostat/providers/meteostat/shared.py +1 -1
  25. meteostat/providers/metno/forecast.py +16 -12
  26. meteostat/providers/noaa/ghcnd.py +1 -1
  27. meteostat/providers/noaa/isd_lite.py +14 -1
  28. meteostat/providers/noaa/metar.py +1 -1
  29. meteostat/utils/conversions.py +30 -31
  30. meteostat/utils/guards.py +51 -0
  31. meteostat/utils/parsers.py +0 -7
  32. {meteostat-2.0.0.dist-info → meteostat-2.1.0.dist-info}/METADATA +15 -19
  33. meteostat-2.1.0.dist-info/RECORD +69 -0
  34. {meteostat-2.0.0.dist-info → meteostat-2.1.0.dist-info}/WHEEL +1 -1
  35. meteostat-2.0.0.dist-info/RECORD +0 -63
  36. {meteostat-2.0.0.dist-info → meteostat-2.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,194 @@
1
+ """
2
+ GeoSphere Austria Data Hub daily data import routine
3
+
4
+ Get daily climate data for weather stations in Austria.
5
+
6
+ License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
7
+ """
8
+
9
+ from datetime import datetime
10
+ from typing import Dict, Optional
11
+
12
+ import pandas as pd
13
+
14
+ from meteostat.enumerations import TTL, Parameter
15
+ from meteostat.core.logger import logger
16
+ from meteostat.typing import ProviderRequest
17
+ from meteostat.core.cache import cache_service
18
+ from meteostat.api.config import config
19
+ from meteostat.core.network import network_service
20
+ from meteostat.utils.conversions import (
21
+ hours_to_minutes,
22
+ ms_to_kmh,
23
+ percentage_to_okta,
24
+ pres_to_msl,
25
+ )
26
+
27
+
28
+ RESOURCE_ID = "klima-v2-1d"
29
+
30
+ # Mapping from GeoSphere Austria parameter names to Meteostat parameters
31
+ # See: https://dataset.api.hub.geosphere.at/v1/station/historical/klima-v2-1d/metadata
32
+ PARAMETER_MAPPING: Dict[str, Parameter] = {
33
+ "tl_mittel": Parameter.TEMP, # Mean air temperature (°C) - Lufttemperatur 2m Mittelwert
34
+ "tlmax": Parameter.TMAX, # Maximum air temperature (°C) - Lufttemperatur 2m Maximalwert
35
+ "tlmin": Parameter.TMIN, # Minimum air temperature (°C) - Lufttemperatur 2m Minimalwert
36
+ "rr": Parameter.PRCP, # Precipitation 24h sum (mm) - Niederschlag 24h Summe
37
+ "sh": Parameter.SNWD, # Snow depth (cm) - Schneehöhe
38
+ "vv_mittel": Parameter.WSPD, # Mean wind speed (km/h) - Windgeschwindigkeit Mittelwert
39
+ "ffx": Parameter.WPGT, # Maximum wind gust (m/s) - Windböe Maximum
40
+ "p_mittel": Parameter.PRES, # Mean air pressure (hPa) - Luftdruck Mittelwert
41
+ "rf_mittel": Parameter.RHUM, # Mean relative humidity (%) - Relative Feuchte Mittelwert
42
+ "so_h": Parameter.TSUN, # Sunshine duration (h) - Sonnenscheindauer
43
+ "bewm_mittel": Parameter.CLDC, # Mean cloud cover (%) - Bedeckung Mittelwert
44
+ }
45
+
46
+ # Inverse mapping
47
+ METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
48
+
49
+
50
+ @cache_service.cache(TTL.WEEK, "pickle")
51
+ def get_data(
52
+ station_id: str,
53
+ elevation: int | None,
54
+ parameters: list[str],
55
+ start: datetime,
56
+ end: datetime,
57
+ ) -> Optional[pd.DataFrame]:
58
+ """
59
+ Fetch data from GeoSphere Austria Data Hub API
60
+ """
61
+ logger.debug(
62
+ f"Fetching daily data for station '{station_id}' from {start} to {end}"
63
+ )
64
+
65
+ # Format dates as ISO 8601 (date only for daily data)
66
+ start_str = start.strftime("%Y-%m-%d")
67
+ end_str = end.strftime("%Y-%m-%d")
68
+
69
+ # Build URL
70
+ url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
71
+
72
+ # Make request
73
+ response = network_service.get(
74
+ url,
75
+ params={
76
+ "parameters": ",".join(parameters),
77
+ "station_ids": station_id,
78
+ "start": start_str,
79
+ "end": end_str,
80
+ "output_format": "geojson",
81
+ },
82
+ )
83
+
84
+ if response.status_code != 200:
85
+ logger.warning(
86
+ f"Failed to fetch daily data for station {station_id} (status: {response.status_code})"
87
+ )
88
+ return None
89
+
90
+ try:
91
+ data = response.json()
92
+
93
+ if not data.get("features"):
94
+ logger.info(f"No daily data returned for station {station_id}")
95
+ return None
96
+
97
+ # Get timestamps array
98
+ timestamps = data.get("timestamps")
99
+ if not timestamps:
100
+ logger.warning("No timestamps in daily response")
101
+ return None
102
+
103
+ # Extract time series data from GeoJSON response
104
+ # New API format has timestamps at top level and data as arrays
105
+ feature = data["features"][0]
106
+ props = feature.get("properties", {})
107
+ params_data = props.get("parameters", {})
108
+
109
+ if not params_data:
110
+ logger.info(f"No parameter data returned for station {station_id}")
111
+ return None
112
+
113
+ # Build DataFrame from timestamps and parameter arrays
114
+ df_dict = {}
115
+ for param in parameters:
116
+ if param in params_data:
117
+ param_info = params_data[param]
118
+ if "data" in param_info:
119
+ df_dict[param] = param_info["data"]
120
+
121
+ if not df_dict:
122
+ return None
123
+
124
+ # Create DataFrame with timestamps as index
125
+ df = pd.DataFrame(df_dict)
126
+ dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
127
+ df.index = dt_index.tz_localize(None)
128
+ df.index.name = "time"
129
+
130
+ # Sort by time
131
+ df = df.sort_index()
132
+
133
+ # Rename columns to Meteostat parameter names
134
+ rename_map = {}
135
+ for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
136
+ if gsadh_param in df.columns:
137
+ rename_map[gsadh_param] = meteostat_param
138
+
139
+ df = df.rename(columns=rename_map)
140
+
141
+ # Convert units where necessary
142
+ if Parameter.WSPD in df.columns:
143
+ df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
144
+
145
+ if Parameter.WPGT in df.columns:
146
+ df[Parameter.WPGT] = df[Parameter.WPGT].apply(ms_to_kmh)
147
+
148
+ if Parameter.TSUN in df.columns:
149
+ df[Parameter.TSUN] = df[Parameter.TSUN].apply(hours_to_minutes)
150
+
151
+ if Parameter.CLDC in df.columns:
152
+ df[Parameter.CLDC] = df[Parameter.CLDC].apply(percentage_to_okta)
153
+
154
+ if Parameter.PRES in df.columns:
155
+ df[Parameter.PRES] = df.apply(
156
+ lambda row: pres_to_msl(row, elevation), axis=1
157
+ )
158
+
159
+ # Round values
160
+ df = df.round(1)
161
+
162
+ return df
163
+
164
+ except Exception as error:
165
+ logger.warning(f"Error parsing daily response: {error}", exc_info=True)
166
+ return None
167
+
168
+
169
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
170
+ """
171
+ Fetch daily data from GeoSphere Austria Data Hub
172
+ """
173
+ if "national" not in req.station.identifiers:
174
+ return None
175
+
176
+ station_id = req.station.identifiers["national"]
177
+
178
+ # Map Meteostat parameters to GeoSphere Austria parameters
179
+ gsa_params = []
180
+ for param in req.parameters:
181
+ if param in METEOSTAT_TO_GSA:
182
+ gsa_params.append(METEOSTAT_TO_GSA[param])
183
+
184
+ if not gsa_params:
185
+ logger.info("No mappable parameters for GeoSphere Austria daily data")
186
+ return None
187
+
188
+ # Fetch data
189
+ df = get_data(station_id, req.station.elevation, gsa_params, req.start, req.end)
190
+
191
+ if df is None or df.empty:
192
+ return None
193
+
194
+ return df
@@ -0,0 +1,175 @@
1
+ """
2
+ GeoSphere Austria Data Hub hourly data import routine
3
+
4
+ Get hourly climate data for weather stations in Austria.
5
+
6
+ License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
7
+ """
8
+
9
+ from datetime import datetime
10
+ from typing import Dict, Optional
11
+
12
+ import pandas as pd
13
+
14
+ from meteostat.enumerations import TTL, Parameter
15
+ from meteostat.core.logger import logger
16
+ from meteostat.typing import ProviderRequest
17
+ from meteostat.core.cache import cache_service
18
+ from meteostat.api.config import config
19
+ from meteostat.core.network import network_service
20
+ from meteostat.utils.conversions import hours_to_minutes, ms_to_kmh
21
+
22
+
23
+ RESOURCE_ID = "klima-v2-1h"
24
+
25
+ # Mapping from GeoSphere Austria parameter names to Meteostat parameters
26
+ # See: https://dataset.api.hub.geosphere.at/v1/station/historical/klima-v2-1h/metadata
27
+ PARAMETER_MAPPING: Dict[str, Parameter] = {
28
+ "tl": Parameter.TEMP, # Air temperature (°C)
29
+ "rr": Parameter.PRCP, # Precipitation (mm)
30
+ "pred": Parameter.PRES, # Air pressure (hPa)
31
+ "ff": Parameter.WSPD, # Wind speed (m/s)
32
+ "ffx": Parameter.WPGT, # Wind gust (m/s)
33
+ "dd": Parameter.WDIR, # Wind direction (°)
34
+ "rf": Parameter.RHUM, # Relative humidity (%)
35
+ "so_h": Parameter.TSUN, # Sunshine duration (h)
36
+ "sh": Parameter.SNWD, # Snow depth (cm)
37
+ }
38
+
39
+ # Inverse mapping
40
+ METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
41
+
42
+
43
+ @cache_service.cache(TTL.DAY, "pickle")
44
+ def get_data(
45
+ station_id: str, parameters: list[str], start: datetime, end: datetime
46
+ ) -> Optional[pd.DataFrame]:
47
+ """
48
+ Fetch data from GeoSphere Austria Data Hub API
49
+ """
50
+ logger.debug(
51
+ f"Fetching hourly data for station '{station_id}' from {start} to {end}"
52
+ )
53
+
54
+ # Format dates as ISO 8601
55
+ start_str = start.strftime("%Y-%m-%dT%H:%M")
56
+ end_str = end.strftime("%Y-%m-%dT%H:%M")
57
+
58
+ # Build URL
59
+ url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
60
+
61
+ # Make request
62
+ response = network_service.get(
63
+ url,
64
+ params={
65
+ "parameters": ",".join(parameters),
66
+ "station_ids": station_id,
67
+ "start": start_str,
68
+ "end": end_str,
69
+ "output_format": "geojson",
70
+ },
71
+ )
72
+
73
+ if response.status_code != 200:
74
+ logger.warning(
75
+ f"Failed to fetch data for station {station_id} (status: {response.status_code})"
76
+ )
77
+ return None
78
+
79
+ try:
80
+ data = response.json()
81
+
82
+ if not data.get("features"):
83
+ logger.info(f"No data returned for station {station_id}")
84
+ return None
85
+
86
+ # Get timestamps array
87
+ timestamps = data.get("timestamps")
88
+ if not timestamps:
89
+ logger.warning("No timestamps in hourly response")
90
+ return None
91
+
92
+ # Extract time series data from GeoJSON response
93
+ # New API format has timestamps at top level and data as arrays
94
+ feature = data["features"][0]
95
+ props = feature.get("properties", {})
96
+ params_data = props.get("parameters", {})
97
+
98
+ if not params_data:
99
+ logger.info(f"No parameter data returned for station {station_id}")
100
+ return None
101
+
102
+ # Build DataFrame from timestamps and parameter arrays
103
+ df_dict = {}
104
+ for param in parameters:
105
+ if param in params_data:
106
+ param_info = params_data[param]
107
+ if "data" in param_info:
108
+ df_dict[param] = param_info["data"]
109
+
110
+ if not df_dict:
111
+ return None
112
+
113
+ # Create DataFrame with timestamps as index
114
+ df = pd.DataFrame(df_dict)
115
+ dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
116
+ df.index = dt_index.tz_localize(None)
117
+ df.index.name = "time"
118
+
119
+ # Sort by time
120
+ df = df.sort_index()
121
+
122
+ # Rename columns to Meteostat parameter names
123
+ rename_map = {}
124
+ for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
125
+ if gsadh_param in df.columns:
126
+ rename_map[gsadh_param] = meteostat_param
127
+
128
+ df = df.rename(columns=rename_map)
129
+
130
+ # Convert units where necessary
131
+ if Parameter.WSPD in df.columns:
132
+ df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
133
+
134
+ if Parameter.WPGT in df.columns:
135
+ df[Parameter.WPGT] = df[Parameter.WPGT].apply(ms_to_kmh)
136
+
137
+ if Parameter.TSUN in df.columns:
138
+ df[Parameter.TSUN] = df[Parameter.TSUN].apply(hours_to_minutes)
139
+
140
+ # Round values
141
+ df = df.round(1)
142
+
143
+ return df
144
+
145
+ except Exception as error:
146
+ logger.warning(f"Error parsing response: {error}", exc_info=True)
147
+ return None
148
+
149
+
150
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
151
+ """
152
+ Fetch hourly data from GeoSphere Austria Data Hub
153
+ """
154
+ if "national" not in req.station.identifiers:
155
+ return None
156
+
157
+ station_id = req.station.identifiers["national"]
158
+
159
+ # Map Meteostat parameters to GeoSphere Austria parameters
160
+ gsa_params = []
161
+ for param in req.parameters:
162
+ if param in METEOSTAT_TO_GSA:
163
+ gsa_params.append(METEOSTAT_TO_GSA[param])
164
+
165
+ if not gsa_params:
166
+ logger.info("No mappable parameters for GeoSphere Austria hourly data")
167
+ return None
168
+
169
+ # Fetch data
170
+ df = get_data(station_id, gsa_params, req.start, req.end)
171
+
172
+ if df is None or df.empty:
173
+ return None
174
+
175
+ return df
@@ -0,0 +1,192 @@
1
+ """
2
+ GeoSphere Austria Data Hub monthly data import routine
3
+
4
+ Get monthly climate data for weather stations in Austria.
5
+
6
+ License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
7
+ """
8
+
9
+ from datetime import datetime
10
+ from typing import Dict, Optional
11
+
12
+ import pandas as pd
13
+
14
+ from meteostat.enumerations import TTL, Parameter
15
+ from meteostat.core.logger import logger
16
+ from meteostat.typing import ProviderRequest
17
+ from meteostat.core.cache import cache_service
18
+ from meteostat.api.config import config
19
+ from meteostat.core.network import network_service
20
+ from meteostat.utils.conversions import (
21
+ hours_to_minutes,
22
+ ms_to_kmh,
23
+ percentage_to_okta,
24
+ pres_to_msl,
25
+ )
26
+
27
+
28
+ RESOURCE_ID = "klima-v2-1m"
29
+
30
+ # Mapping from GeoSphere Austria parameter names to Meteostat parameters
31
+ # See: https://dataset.api.hub.geosphere.at/v1/station/historical/klima-v2-1m/metadata
32
+ PARAMETER_MAPPING: Dict[str, Parameter] = {
33
+ "tl_mittel": Parameter.TEMP, # Mean air temperature (°C) - Lufttemperatur 2m Mittelwert
34
+ "tlmin": Parameter.TXMN, # Minimum air temperature (°C) - Lufttemperatur 2m Minimalwert
35
+ "tlmax": Parameter.TXMX, # Maximum air temperature (°C) - Lufttemperatur 2m Maximalwert
36
+ "tlmin_mittel": Parameter.TMIN, # Mean minimum air temperature (°C) - Lufttemperatur 2m Mittelwert der Minima
37
+ "tlmax_mittel": Parameter.TMAX, # Mean maximum air temperature (°C) - Lufttemperatur 2m Mittelwert der Maxima
38
+ "rf_mittel": Parameter.RHUM, # Mean relative humidity (%) - Relative Feuchte Mittelwert
39
+ "rr": Parameter.PRCP, # Precipitation sum (mm) - Niederschlag Summe der 24h-Summen
40
+ "vv_mittel": Parameter.WSPD, # Mean wind speed (m/s) - Windgeschwindigkeit Mittelwert
41
+ "p": Parameter.PRES, # Mean air pressure (hPa) - Luftdruck Mittelwert
42
+ "so_h": Parameter.TSUN, # Sunshine duration (h) - Sonnenscheindauer
43
+ "bewm_mittel": Parameter.CLDC, # Mean cloud cover (%) - Bedeckung Mittelwert
44
+ }
45
+
46
+ # Inverse mapping
47
+ METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
48
+
49
+
50
+ @cache_service.cache(TTL.MONTH, "pickle")
51
+ def get_data(
52
+ station_id: str,
53
+ elevation: int | None,
54
+ parameters: list[str],
55
+ start: datetime,
56
+ end: datetime,
57
+ ) -> Optional[pd.DataFrame]:
58
+ """
59
+ Fetch data from GeoSphere Austria Data Hub API
60
+ """
61
+ logger.debug(
62
+ f"Fetching monthly data for station '{station_id}' from {start} to {end}"
63
+ )
64
+
65
+ # Format dates as ISO 8601 (full date for monthly data)
66
+ start_str = start.strftime("%Y-%m-%d")
67
+ end_str = end.strftime("%Y-%m-%d")
68
+
69
+ # Build URL
70
+ url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
71
+
72
+ # Make request
73
+ response = network_service.get(
74
+ url,
75
+ params={
76
+ "parameters": ",".join(parameters),
77
+ "station_ids": station_id,
78
+ "start": start_str,
79
+ "end": end_str,
80
+ "output_format": "geojson",
81
+ },
82
+ )
83
+
84
+ if response.status_code != 200:
85
+ logger.warning(
86
+ f"Failed to fetch monthly data for station {station_id} (status: {response.status_code}): {response.json()}",
87
+ )
88
+ return None
89
+
90
+ try:
91
+ data = response.json()
92
+
93
+ if not data.get("features"):
94
+ logger.info(f"No monthly data returned for station {station_id}")
95
+ return None
96
+
97
+ # Get timestamps array
98
+ timestamps = data.get("timestamps")
99
+ if not timestamps:
100
+ logger.warning("No timestamps in monthly response")
101
+ return None
102
+
103
+ # Extract time series data from GeoJSON response
104
+ # New API format has timestamps at top level and data as arrays
105
+ feature = data["features"][0]
106
+ props = feature.get("properties", {})
107
+ params_data = props.get("parameters", {})
108
+
109
+ if not params_data:
110
+ logger.info(f"No parameter data returned for station {station_id}")
111
+ return None
112
+
113
+ # Build DataFrame from timestamps and parameter arrays
114
+ df_dict = {}
115
+ for param in parameters:
116
+ if param in params_data:
117
+ param_info = params_data[param]
118
+ if "data" in param_info:
119
+ df_dict[param] = param_info["data"]
120
+
121
+ if not df_dict:
122
+ return None
123
+
124
+ # Create DataFrame with timestamps as index
125
+ df = pd.DataFrame(df_dict)
126
+ dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
127
+ df.index = dt_index.tz_localize(None)
128
+ df.index.name = "time"
129
+
130
+ # Sort by time
131
+ df = df.sort_index()
132
+
133
+ # Rename columns to Meteostat parameter names
134
+ rename_map = {}
135
+ for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
136
+ if gsadh_param in df.columns:
137
+ rename_map[gsadh_param] = meteostat_param
138
+
139
+ df = df.rename(columns=rename_map)
140
+
141
+ # Convert units where necessary
142
+ if Parameter.WSPD in df.columns:
143
+ df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
144
+
145
+ if Parameter.TSUN in df.columns:
146
+ df[Parameter.TSUN] = df[Parameter.TSUN].apply(hours_to_minutes)
147
+
148
+ if Parameter.CLDC in df.columns:
149
+ # Convert cloud cover from % to okta
150
+ df[Parameter.CLDC] = df[Parameter.CLDC].apply(percentage_to_okta)
151
+
152
+ if Parameter.PRES in df.columns:
153
+ df[Parameter.PRES] = df.apply(
154
+ lambda row: pres_to_msl(row, elevation), axis=1
155
+ )
156
+
157
+ # Round values
158
+ df = df.round(1)
159
+
160
+ return df
161
+
162
+ except Exception as error:
163
+ logger.warning(f"Error parsing monthly response: {error}", exc_info=True)
164
+ return None
165
+
166
+
167
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
168
+ """
169
+ Fetch monthly data from GeoSphere Austria Data Hub
170
+ """
171
+ if "national" not in req.station.identifiers:
172
+ return None
173
+
174
+ station_id = req.station.identifiers["national"]
175
+
176
+ # Map Meteostat parameters to GeoSphere Austria parameters
177
+ gsa_params = []
178
+ for param in req.parameters:
179
+ if param in METEOSTAT_TO_GSA:
180
+ gsa_params.append(METEOSTAT_TO_GSA[param])
181
+
182
+ if not gsa_params:
183
+ logger.info("No mappable parameters for GeoSphere Austria monthly data")
184
+ return None
185
+
186
+ # Fetch data
187
+ df = get_data(station_id, req.station.elevation, gsa_params, req.start, req.end)
188
+
189
+ if df is None or df.empty:
190
+ return None
191
+
192
+ return df