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,184 @@
1
+ """
2
+ GeoSphere Austria Data Hub SYNOP hourly data import routine
3
+
4
+ Get SYNOP (synoptic observation) hourly 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 ms_to_kmh, pres_to_msl
21
+
22
+
23
+ RESOURCE_ID = "synop-v1-1h"
24
+
25
+ # Mapping from GeoSphere Austria SYNOP parameter names to Meteostat parameters
26
+ # See: https://dataset.api.hub.geosphere.at/v1/station/historical/synop-v1-1h/metadata
27
+ PARAMETER_MAPPING: Dict[str, Parameter] = {
28
+ "T": Parameter.TEMP, # Air temperature (°C) - Lufttemperatur
29
+ "Pg": Parameter.PRES, # Air pressure (hPa) - Luftdruck auf Stationshöhe
30
+ "rel": Parameter.RHUM, # Relative humidity (%) - Relative Feuchte
31
+ "ff": Parameter.WSPD, # Wind speed (m/s) - Windgeschwindigkeit
32
+ "boe": Parameter.WPGT, # Wind gust (m/s) - Windböe
33
+ "dd": Parameter.WDIR, # Wind direction (°) - Windrichtung
34
+ "RRR": Parameter.PRCP, # Precipitation (mm) - Niederschlagsmenge
35
+ "N": Parameter.CLDC, # Cloud cover (okta) - Bedeckungsgrad
36
+ }
37
+
38
+ # Inverse mapping
39
+ METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
40
+
41
+
42
+ @cache_service.cache(TTL.DAY, "pickle")
43
+ def get_data(
44
+ station_id: str,
45
+ elevation: int | None,
46
+ parameters: list[str],
47
+ start: datetime,
48
+ end: datetime,
49
+ ) -> Optional[pd.DataFrame]:
50
+ """
51
+ Fetch SYNOP data from GeoSphere Austria Data Hub API
52
+ """
53
+ logger.debug(
54
+ f"Fetching SYNOP hourly data for station '{station_id}' from {start} to {end}"
55
+ )
56
+
57
+ # Format dates as ISO 8601
58
+ start_str = start.strftime("%Y-%m-%dT%H:%M")
59
+ end_str = end.strftime("%Y-%m-%dT%H:%M")
60
+
61
+ # Build URL
62
+ url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
63
+
64
+ # Make request
65
+ response = network_service.get(
66
+ url,
67
+ params={
68
+ "parameters": ",".join(parameters),
69
+ "station_ids": station_id,
70
+ "start": start_str,
71
+ "end": end_str,
72
+ "output_format": "geojson",
73
+ },
74
+ )
75
+
76
+ if response.status_code != 200:
77
+ logger.warning(
78
+ f"Failed to fetch SYNOP data for station {station_id} (status: {response.status_code})"
79
+ )
80
+ return None
81
+
82
+ try:
83
+ data = response.json()
84
+
85
+ if not data.get("features"):
86
+ logger.info(f"No SYNOP data returned for station {station_id}")
87
+ return None
88
+
89
+ # Get timestamps array
90
+ timestamps = data.get("timestamps")
91
+ if not timestamps:
92
+ logger.warning("No timestamps in SYNOP response")
93
+ return None
94
+
95
+ # Extract time series data from GeoJSON response
96
+ # New API format has timestamps at top level and data as arrays
97
+ feature = data["features"][0]
98
+ props = feature.get("properties", {})
99
+ params_data = props.get("parameters", {})
100
+
101
+ if not params_data:
102
+ logger.info(f"No parameter data returned for station {station_id}")
103
+ return None
104
+
105
+ # Build DataFrame from timestamps and parameter arrays
106
+ df_dict = {}
107
+ for param in parameters:
108
+ if param in params_data:
109
+ param_info = params_data[param]
110
+ if "data" in param_info:
111
+ df_dict[param] = param_info["data"]
112
+
113
+ if not df_dict:
114
+ return None
115
+
116
+ # Create DataFrame with timestamps as index
117
+ df = pd.DataFrame(df_dict)
118
+ dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
119
+ df.index = dt_index.tz_localize(None)
120
+ df.index.name = "time"
121
+
122
+ # Sort by time
123
+ df = df.sort_index()
124
+
125
+ # Rename columns to Meteostat parameter names
126
+ rename_map = {}
127
+ for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
128
+ if gsadh_param in df.columns:
129
+ rename_map[gsadh_param] = meteostat_param
130
+
131
+ df = df.rename(columns=rename_map)
132
+
133
+ # Convert units where necessary
134
+ if Parameter.WSPD in df.columns:
135
+ df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
136
+
137
+ if Parameter.WPGT in df.columns:
138
+ df[Parameter.WPGT] = df[Parameter.WPGT].apply(ms_to_kmh)
139
+
140
+ # RRR returns -1 for no precipitation; convert to 0
141
+ if Parameter.PRCP in df.columns:
142
+ df[Parameter.PRCP] = df[Parameter.PRCP].replace(-1, 0)
143
+
144
+ if Parameter.PRES in df.columns:
145
+ df[Parameter.PRES] = df.apply(
146
+ lambda row: pres_to_msl(row, elevation), axis=1
147
+ )
148
+
149
+ # Round values
150
+ df = df.round(1)
151
+
152
+ return df
153
+
154
+ except Exception as error:
155
+ logger.warning(f"Error parsing SYNOP response: {error}", exc_info=True)
156
+ return None
157
+
158
+
159
+ def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
160
+ """
161
+ Fetch SYNOP hourly data from GeoSphere Austria Data Hub
162
+ """
163
+ if "wmo" not in req.station.identifiers:
164
+ return None
165
+
166
+ station_id = req.station.identifiers["wmo"]
167
+
168
+ # Map Meteostat parameters to GeoSphere Austria SYNOP parameters
169
+ gsa_params = []
170
+ for param in req.parameters:
171
+ if param in METEOSTAT_TO_GSA:
172
+ gsa_params.append(METEOSTAT_TO_GSA[param])
173
+
174
+ if not gsa_params:
175
+ logger.info("No mappable parameters for GeoSphere Austria SYNOP data")
176
+ return None
177
+
178
+ # Fetch data
179
+ df = get_data(station_id, req.station.elevation, gsa_params, req.start, req.end)
180
+
181
+ if df is None or df.empty:
182
+ return None
183
+
184
+ return df
@@ -473,6 +473,113 @@ PROVIDER_METNO_FORECAST = ProviderSpec(
473
473
  module="meteostat.providers.metno.forecast",
474
474
  )
475
475
 
476
+ PROVIDER_GSA_HOURLY = ProviderSpec(
477
+ id=Provider.GSA_HOURLY,
478
+ name="GeoSphere Austria Hourly",
479
+ granularity=Granularity.HOURLY,
480
+ priority=Priority.HIGHEST,
481
+ grade=Grade.RECORD,
482
+ license=License(
483
+ commercial=True,
484
+ attribution="GeoSphere Austria",
485
+ name="CC BY 4.0",
486
+ url="https://creativecommons.org/licenses/by/4.0/",
487
+ ),
488
+ requires=["identifiers"],
489
+ countries=["AT"],
490
+ parameters=[
491
+ Parameter.TEMP,
492
+ Parameter.PRCP,
493
+ Parameter.PRES,
494
+ Parameter.WSPD,
495
+ Parameter.WDIR,
496
+ Parameter.RHUM,
497
+ Parameter.TSUN,
498
+ ],
499
+ start=date(1880, 4, 1),
500
+ module="meteostat.providers.gsa.hourly",
501
+ )
502
+
503
+ PROVIDER_GSA_SYNOP = ProviderSpec(
504
+ id=Provider.GSA_SYNOP,
505
+ name="GeoSphere Austria SYNOP",
506
+ granularity=Granularity.HOURLY,
507
+ priority=Priority.HIGH,
508
+ grade=Grade.OBSERVATION,
509
+ license=License(
510
+ commercial=True,
511
+ attribution="GeoSphere Austria",
512
+ name="CC BY 4.0",
513
+ url="https://creativecommons.org/licenses/by/4.0/",
514
+ ),
515
+ requires=["identifiers"],
516
+ countries=["AT"],
517
+ parameters=[
518
+ Parameter.TEMP,
519
+ Parameter.PRES,
520
+ Parameter.RHUM,
521
+ Parameter.WSPD,
522
+ Parameter.WDIR,
523
+ Parameter.PRCP,
524
+ ],
525
+ start=date(2000, 1, 1),
526
+ module="meteostat.providers.gsa.synop",
527
+ )
528
+
529
+ PROVIDER_GSA_DAILY = ProviderSpec(
530
+ id=Provider.GSA_DAILY,
531
+ name="GeoSphere Austria Daily",
532
+ granularity=Granularity.DAILY,
533
+ priority=Priority.HIGHEST,
534
+ grade=Grade.RECORD,
535
+ license=License(
536
+ commercial=True,
537
+ attribution="GeoSphere Austria",
538
+ name="CC BY 4.0",
539
+ url="https://creativecommons.org/licenses/by/4.0/",
540
+ ),
541
+ requires=["identifiers"],
542
+ countries=["AT"],
543
+ parameters=[
544
+ Parameter.TEMP,
545
+ Parameter.TMIN,
546
+ Parameter.TMAX,
547
+ Parameter.PRCP,
548
+ Parameter.PRES,
549
+ Parameter.RHUM,
550
+ Parameter.WSPD,
551
+ Parameter.TSUN,
552
+ ],
553
+ start=date(1880, 4, 1),
554
+ module="meteostat.providers.gsa.daily",
555
+ )
556
+
557
+ PROVIDER_GSA_MONTHLY = ProviderSpec(
558
+ id=Provider.GSA_MONTHLY,
559
+ name="GeoSphere Austria Monthly",
560
+ granularity=Granularity.MONTHLY,
561
+ priority=Priority.HIGHEST,
562
+ grade=Grade.RECORD,
563
+ license=License(
564
+ commercial=True,
565
+ attribution="GeoSphere Austria",
566
+ name="CC BY 4.0",
567
+ url="https://creativecommons.org/licenses/by/4.0/",
568
+ ),
569
+ requires=["identifiers"],
570
+ countries=["AT"],
571
+ parameters=[
572
+ Parameter.TEMP,
573
+ Parameter.TMIN,
574
+ Parameter.TMAX,
575
+ Parameter.PRCP,
576
+ Parameter.PRES,
577
+ Parameter.TSUN,
578
+ ],
579
+ start=date(1880, 4, 1),
580
+ module="meteostat.providers.gsa.monthly",
581
+ )
582
+
476
583
 
477
584
  DEFAULT_PROVIDERS = [
478
585
  PROVIDER_HOURLY,
@@ -493,4 +600,8 @@ DEFAULT_PROVIDERS = [
493
600
  PROVIDER_CLIMAT,
494
601
  PROVIDER_METAR,
495
602
  PROVIDER_METNO_FORECAST,
603
+ PROVIDER_GSA_HOURLY,
604
+ PROVIDER_GSA_SYNOP,
605
+ PROVIDER_GSA_DAILY,
606
+ PROVIDER_GSA_MONTHLY,
496
607
  ]
@@ -10,7 +10,7 @@ import pandas as pd
10
10
  from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
11
11
  from meteostat.typing import ProviderRequest
12
12
  from meteostat.core.cache import cache_service
13
- from meteostat.core.config import config
13
+ from meteostat.api.config import config
14
14
  from meteostat.utils.data import reshape_by_source
15
15
 
16
16
  ENDPOINT = config.daily_endpoint
@@ -9,7 +9,7 @@ import pandas as pd
9
9
 
10
10
  from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
11
11
  from meteostat.typing import ProviderRequest
12
- from meteostat.core.config import config
12
+ from meteostat.api.config import config
13
13
  from meteostat.core.cache import cache_service
14
14
  from meteostat.utils.data import reshape_by_source
15
15
 
@@ -6,7 +6,7 @@ from typing import Optional
6
6
 
7
7
  import pandas as pd
8
8
 
9
- from meteostat.core.config import config
9
+ from meteostat.api.config import config
10
10
  from meteostat.enumerations import TTL
11
11
  from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
12
12
  from meteostat.typing import ProviderRequest
@@ -4,7 +4,7 @@ from typing import Optional, Callable, TypeVar
4
4
 
5
5
  import pandas as pd
6
6
 
7
- from meteostat.core.config import config
7
+ from meteostat.api.config import config
8
8
  from meteostat.core.logger import logger
9
9
  from meteostat.core.providers import provider_service
10
10
  from meteostat.enumerations import Grade
@@ -3,7 +3,7 @@ from urllib.error import HTTPError
3
3
 
4
4
  import pandas as pd
5
5
 
6
- from meteostat.core.config import config
6
+ from meteostat.api.config import config
7
7
  from meteostat.enumerations import TTL, Parameter
8
8
  from meteostat.core.logger import logger
9
9
  from meteostat.core.network import network_service
@@ -11,9 +11,6 @@ from meteostat.typing import ProviderRequest
11
11
  from meteostat.utils.conversions import percentage_to_okta
12
12
  from meteostat.core.cache import cache_service
13
13
 
14
-
15
- ENDPOINT = config.metno_forecast_endpoint
16
- USER_AGENT = config.metno_user_agent
17
14
  CONDICODES = {
18
15
  "clearsky": 1,
19
16
  "cloudy": 3,
@@ -130,13 +127,26 @@ def map_data(record):
130
127
 
131
128
  @cache_service.cache(TTL.HOUR, "pickle")
132
129
  def get_df(latitude: float, longitude: float, elevation: int) -> Optional[pd.DataFrame]:
133
- file_url = ENDPOINT.format(
130
+ endpoint = config.metno_forecast_endpoint
131
+ user_agent = config.metno_user_agent
132
+
133
+ if not endpoint:
134
+ logger.warning("MET Norway forecast endpoint is not configured.")
135
+ return None
136
+
137
+ if not user_agent:
138
+ logger.warning(
139
+ "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."
140
+ )
141
+ return None
142
+
143
+ file_url = endpoint.format(
134
144
  latitude=latitude,
135
145
  longitude=longitude,
136
146
  elevation=elevation,
137
147
  )
138
148
 
139
- headers = {"User-Agent": USER_AGENT}
149
+ headers = {"User-Agent": user_agent}
140
150
 
141
151
  try:
142
152
  response = network_service.get(file_url, headers=headers)
@@ -173,12 +183,6 @@ def get_df(latitude: float, longitude: float, elevation: int) -> Optional[pd.Dat
173
183
 
174
184
 
175
185
  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
186
  return get_df(
183
187
  req.station.latitude,
184
188
  req.station.longitude,
@@ -82,7 +82,7 @@ def create_df(element, dict_element):
82
82
  df_element = move_col_to_front(col, df_element)
83
83
 
84
84
  # Convert numerical values to float
85
- df_element.loc[:, element] = df_element.loc[:, element].astype(float)
85
+ df_element[element] = df_element[element].astype(float)
86
86
 
87
87
  return df_element
88
88
 
@@ -65,13 +65,26 @@ def get_df(usaf: str, wban: str, year: int) -> Optional[pd.DataFrame]:
65
65
  try:
66
66
  df = pd.read_fwf(
67
67
  f"{ISD_LITE_ENDPOINT}{year}/{filename}",
68
- parse_dates={"time": [0, 1, 2, 3]},
69
68
  na_values=["-9999", -9999],
70
69
  header=None,
71
70
  colspecs=COLSPECS,
72
71
  compression="gzip",
73
72
  )
74
73
 
74
+ # Parse datetime from first 4 columns (year, month, day, hour)
75
+ df[0] = pd.to_datetime(
76
+ df[0].astype(str)
77
+ + "-"
78
+ + df[1].astype(str).str.zfill(2)
79
+ + "-"
80
+ + df[2].astype(str).str.zfill(2)
81
+ + " "
82
+ + df[3].astype(str).str.zfill(2)
83
+ + ":00",
84
+ format="%Y-%m-%d %H:%M",
85
+ )
86
+ df = df.drop(columns=[1, 2, 3])
87
+
75
88
  # Rename columns
76
89
  df.columns = COLUMN_NAMES
77
90
 
@@ -8,7 +8,7 @@ import pandas as pd
8
8
  from metar import Metar
9
9
 
10
10
  from meteostat.core.logger import logger
11
- from meteostat.core.config import config
11
+ from meteostat.api.config import config
12
12
  from meteostat.enumerations import TTL, Frequency, Parameter
13
13
  from meteostat.typing import ProviderRequest
14
14
  from meteostat.utils.conversions import temp_dwpt_to_rhum
@@ -8,7 +8,7 @@ The code is licensed under the MIT license.
8
8
  import math
9
9
  from typing import Optional
10
10
 
11
- from numpy import isnan
11
+ import pandas as pd
12
12
 
13
13
  from meteostat.enumerations import Parameter, Unit, UnitSystem
14
14
 
@@ -73,14 +73,21 @@ def kelvin_to_celsius(value):
73
73
  """
74
74
  Convert Kelvin to Celsius
75
75
  """
76
- return value - 273.15 if value is not None and not isnan(value) else None
76
+ return value - 273.15 if not pd.isna(value) else None
77
77
 
78
78
 
79
79
  def ms_to_kmh(value):
80
80
  """
81
81
  Convert m/s to km/h
82
82
  """
83
- return value * 3.6 if value is not None and not isnan(value) else None
83
+ return value * 3.6 if not pd.isna(value) else None
84
+
85
+
86
+ def hours_to_minutes(value):
87
+ """
88
+ Convert duration from hours to minutes
89
+ """
90
+ return value * 60 if not pd.isna(value) else None
84
91
 
85
92
 
86
93
  def temp_dwpt_to_rhum(row: dict):
@@ -93,38 +100,30 @@ def temp_dwpt_to_rhum(row: dict):
93
100
  math.exp((17.625 * row["dwpt"]) / (243.04 + row["dwpt"]))
94
101
  / math.exp((17.625 * row["temp"]) / (243.04 + row["temp"]))
95
102
  )
96
- if row["temp"] is not None and row["dwpt"] is not None
103
+ if not pd.isna(row["temp"]) and not pd.isna(row["dwpt"])
97
104
  else None
98
105
  )
99
106
 
100
107
 
101
108
  def pres_to_msl(row: dict, altitude: Optional[int] = None, temp: str = Parameter.TEMP):
102
- """
103
- Convert local air pressure to MSL
104
- """
105
109
  try:
106
- return (
107
- None
108
- if (
109
- not row[Parameter.PRES]
110
- or not row[temp]
111
- or not altitude
112
- or row[Parameter.PRES] == -999
113
- )
114
- else round(
115
- row[Parameter.PRES]
116
- * math.pow(
117
- (
118
- 1
119
- - (
120
- (0.0065 * altitude)
121
- / (row[temp] + 0.0065 * altitude + 273.15)
122
- )
123
- ),
124
- -5.257,
125
- ),
126
- 1,
127
- )
110
+ pres = row.get(Parameter.PRES)
111
+ t = row.get(temp)
112
+
113
+ if pd.isna(pres) or pd.isna(t) or altitude is None or pres == -999:
114
+ return None
115
+
116
+ # Type narrowing for arithmetic operations
117
+ if not isinstance(pres, (int, float)) or not isinstance(t, (int, float)):
118
+ return None
119
+
120
+ return round(
121
+ pres
122
+ * math.pow(
123
+ 1 - (0.0065 * altitude) / (t + 0.0065 * altitude + 273.15),
124
+ -5.257,
125
+ ),
126
+ 1,
128
127
  )
129
128
  except Exception:
130
129
  return None
@@ -134,14 +133,14 @@ def percentage_to_okta(value):
134
133
  """
135
134
  Convert cloud cover percentage to oktas
136
135
  """
137
- return round(value / 12.5) if value is not None and not isnan(value) else None
136
+ return round(value / 12.5) if not pd.isna(value) else None
138
137
 
139
138
 
140
139
  def jcm2_to_wm2(value):
141
140
  """
142
141
  Convert Joule/CM^2 to Watt/M^2
143
142
  """
144
- return round(value * 2.78) if value is not None and not isnan(value) else None
143
+ return round(value * 2.78) if not pd.isna(value) else None
145
144
 
146
145
 
147
146
  def to_direction(value):
@@ -0,0 +1,51 @@
1
+ """
2
+ Guard functions for Meteostat.
3
+
4
+ The code is licensed under the MIT license.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from meteostat.api.config import config
9
+ from meteostat.core.logger import logger
10
+ from meteostat.enumerations import Granularity
11
+ from meteostat.typing import Request
12
+
13
+
14
+ def request_size_guard(req: Request) -> None:
15
+ """
16
+ Guard to block large requests
17
+ """
18
+ if not config.block_large_requests:
19
+ logger.debug("Large request blocking is disabled.")
20
+ return
21
+
22
+ if isinstance(req.station, list) and len(req.station) > 10:
23
+ raise ValueError(
24
+ "Requests with more than 10 stations are blocked by default. "
25
+ "To enable large requests, set `config.block_large_requests = False`."
26
+ )
27
+
28
+ if req.granularity not in [Granularity.HOURLY, Granularity.DAILY]:
29
+ return
30
+
31
+ if req.start is None:
32
+ raise ValueError(
33
+ "Hourly and daily requests without a start date are blocked by default. "
34
+ "To enable large requests, set `config.block_large_requests = False`."
35
+ )
36
+
37
+ time_diff_years = abs((req.end or datetime.now()).year - req.start.year)
38
+
39
+ logger.debug(f"Request time range: {time_diff_years} years.")
40
+
41
+ if req.granularity is Granularity.HOURLY and time_diff_years > 3:
42
+ raise ValueError(
43
+ "Hourly requests longer than 3 years are blocked by default. "
44
+ "To enable large requests, set `config.block_large_requests = False`."
45
+ )
46
+
47
+ if req.granularity is Granularity.DAILY and time_diff_years > 30:
48
+ raise ValueError(
49
+ "Daily requests longer than 30 years are blocked by default. "
50
+ "To enable large requests, set `config.block_large_requests = False`."
51
+ )
@@ -13,7 +13,6 @@ import pytz
13
13
 
14
14
  from meteostat.api.stations import stations as stations_service
15
15
  from meteostat.api.point import Point
16
- from meteostat.core.config import config
17
16
  from meteostat.typing import Station
18
17
 
19
18
 
@@ -54,12 +53,6 @@ def parse_station(
54
53
  # It's a list
55
54
  stations = station
56
55
 
57
- if config.block_large_requests and len(stations) > 10:
58
- raise ValueError(
59
- "Requests with more than 10 stations are blocked by default. "
60
- "To enable large requests, set `config.block_large_requests = False`."
61
- )
62
-
63
56
  # Get station meta data
64
57
  data = []
65
58
  point_counter = 0