meteostat 1.7.6__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.
- meteostat/__init__.py +32 -19
- meteostat/api/daily.py +76 -0
- meteostat/api/hourly.py +80 -0
- meteostat/api/interpolate.py +240 -0
- meteostat/api/inventory.py +59 -0
- meteostat/api/merge.py +103 -0
- meteostat/api/monthly.py +73 -0
- meteostat/api/normals.py +144 -0
- meteostat/api/point.py +30 -0
- meteostat/api/stations.py +234 -0
- meteostat/api/timeseries.py +334 -0
- meteostat/core/cache.py +212 -59
- meteostat/core/config.py +158 -0
- meteostat/core/data.py +199 -0
- meteostat/core/logger.py +9 -0
- meteostat/core/network.py +82 -0
- meteostat/core/parameters.py +112 -0
- meteostat/core/providers.py +184 -0
- meteostat/core/schema.py +170 -0
- meteostat/core/validator.py +38 -0
- meteostat/enumerations.py +149 -0
- meteostat/interpolation/idw.py +120 -0
- meteostat/interpolation/lapserate.py +91 -0
- meteostat/interpolation/nearest.py +31 -0
- meteostat/parameters.py +354 -0
- meteostat/providers/dwd/climat.py +166 -0
- meteostat/providers/dwd/daily.py +144 -0
- meteostat/providers/dwd/hourly.py +218 -0
- meteostat/providers/dwd/monthly.py +138 -0
- meteostat/providers/dwd/mosmix.py +351 -0
- meteostat/providers/dwd/poi.py +117 -0
- meteostat/providers/dwd/shared.py +155 -0
- meteostat/providers/eccc/daily.py +87 -0
- meteostat/providers/eccc/hourly.py +104 -0
- meteostat/providers/eccc/monthly.py +66 -0
- meteostat/providers/eccc/shared.py +45 -0
- meteostat/providers/index.py +496 -0
- meteostat/providers/meteostat/daily.py +65 -0
- meteostat/providers/meteostat/daily_derived.py +110 -0
- meteostat/providers/meteostat/hourly.py +66 -0
- meteostat/providers/meteostat/monthly.py +45 -0
- meteostat/providers/meteostat/monthly_derived.py +106 -0
- meteostat/providers/meteostat/shared.py +93 -0
- meteostat/providers/metno/forecast.py +186 -0
- meteostat/providers/noaa/ghcnd.py +228 -0
- meteostat/providers/noaa/isd_lite.py +142 -0
- meteostat/providers/noaa/metar.py +163 -0
- meteostat/typing.py +113 -0
- meteostat/utils/conversions.py +231 -0
- meteostat/utils/data.py +194 -0
- meteostat/utils/geo.py +28 -0
- meteostat/utils/parsers.py +168 -0
- meteostat/utils/types.py +113 -0
- meteostat/utils/validators.py +31 -0
- meteostat-2.0.0.dist-info/METADATA +134 -0
- meteostat-2.0.0.dist-info/RECORD +63 -0
- {meteostat-1.7.6.dist-info → meteostat-2.0.0.dist-info}/WHEEL +1 -2
- meteostat/core/loader.py +0 -103
- meteostat/core/warn.py +0 -34
- meteostat/enumerations/granularity.py +0 -22
- meteostat/interface/base.py +0 -39
- meteostat/interface/daily.py +0 -118
- meteostat/interface/hourly.py +0 -154
- meteostat/interface/meteodata.py +0 -210
- meteostat/interface/monthly.py +0 -109
- meteostat/interface/normals.py +0 -245
- meteostat/interface/point.py +0 -143
- meteostat/interface/stations.py +0 -252
- meteostat/interface/timeseries.py +0 -237
- meteostat/series/aggregate.py +0 -48
- meteostat/series/convert.py +0 -28
- meteostat/series/count.py +0 -17
- meteostat/series/coverage.py +0 -20
- meteostat/series/fetch.py +0 -28
- meteostat/series/interpolate.py +0 -47
- meteostat/series/normalize.py +0 -76
- meteostat/series/stations.py +0 -22
- meteostat/units.py +0 -149
- meteostat/utilities/__init__.py +0 -0
- meteostat/utilities/aggregations.py +0 -37
- meteostat/utilities/endpoint.py +0 -33
- meteostat/utilities/helpers.py +0 -70
- meteostat/utilities/mutations.py +0 -89
- meteostat/utilities/validations.py +0 -30
- meteostat-1.7.6.dist-info/METADATA +0 -112
- meteostat-1.7.6.dist-info/RECORD +0 -39
- meteostat-1.7.6.dist-info/top_level.txt +0 -1
- /meteostat/{core → api}/__init__.py +0 -0
- /meteostat/{enumerations → interpolation}/__init__.py +0 -0
- /meteostat/{interface → providers}/__init__.py +0 -0
- /meteostat/{interface/interpolate.py → py.typed} +0 -0
- /meteostat/{series → utils}/__init__.py +0 -0
- {meteostat-1.7.6.dist-info → meteostat-2.0.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
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.core.cache import cache_service
|
|
13
|
+
from meteostat.core.config import config
|
|
14
|
+
from meteostat.utils.data import reshape_by_source
|
|
15
|
+
|
|
16
|
+
ENDPOINT = config.daily_endpoint
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_ttl(_station: str, year: int) -> int:
|
|
20
|
+
"""
|
|
21
|
+
Get TTL based on year
|
|
22
|
+
|
|
23
|
+
Current + previous year = one day
|
|
24
|
+
Else = 30 days
|
|
25
|
+
"""
|
|
26
|
+
current_year = datetime.now().year
|
|
27
|
+
return 60 * 60 * 24 if current_year - year < 2 else 60 * 60 * 24 * 30
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@cache_service.cache(get_ttl, "pickle")
|
|
31
|
+
@handle_exceptions
|
|
32
|
+
def get_df(station: str, year: int) -> Optional[pd.DataFrame]:
|
|
33
|
+
"""
|
|
34
|
+
Get CSV file from Meteostat and convert to DataFrame
|
|
35
|
+
"""
|
|
36
|
+
file_url = ENDPOINT.format(station=station, year=str(year))
|
|
37
|
+
|
|
38
|
+
df = pd.read_csv(file_url, sep=",", compression="gzip")
|
|
39
|
+
|
|
40
|
+
time_cols = df.columns[0:3]
|
|
41
|
+
df["time"] = pd.to_datetime(df[time_cols])
|
|
42
|
+
df = df.drop(time_cols, axis=1).set_index("time")
|
|
43
|
+
|
|
44
|
+
return reshape_by_source(df)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@filter_model_data
|
|
48
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
49
|
+
"""
|
|
50
|
+
Fetch daily weather data from Meteostat's central data repository
|
|
51
|
+
"""
|
|
52
|
+
if req.start is None or req.end is None:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
# Get a list of relevant years
|
|
56
|
+
years = range(req.start.year, req.end.year + 1)
|
|
57
|
+
# Get list of annual DataFrames
|
|
58
|
+
df_yearly = [get_df(req.station.id, year) for year in years]
|
|
59
|
+
# Concatenate into a single DataFrame
|
|
60
|
+
df = (
|
|
61
|
+
pd.concat(df_yearly)
|
|
62
|
+
if len(df_yearly) and not all(d is None for d in df_yearly)
|
|
63
|
+
else None
|
|
64
|
+
)
|
|
65
|
+
return df
|
|
@@ -0,0 +1,110 @@
|
|
|
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.hourly import hourly
|
|
8
|
+
from meteostat.typing import ProviderRequest
|
|
9
|
+
from meteostat.utils.data import aggregate_sources, reshape_by_source
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def daily_mean(group: pd.Series):
|
|
13
|
+
"""
|
|
14
|
+
Calculate the daily mean from a series of hourly data
|
|
15
|
+
"""
|
|
16
|
+
if group.isna().sum() > 3:
|
|
17
|
+
return np.nan
|
|
18
|
+
return group.interpolate(axis=0).mean()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def daily_min(group: pd.Series):
|
|
22
|
+
"""
|
|
23
|
+
Calculate the daily minimum from a series of hourly data
|
|
24
|
+
"""
|
|
25
|
+
if group.isna().sum() > 0:
|
|
26
|
+
return np.nan
|
|
27
|
+
return group.min()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def daily_max(group: pd.Series):
|
|
31
|
+
"""
|
|
32
|
+
Calculate the daily maximum from a series of hourly data
|
|
33
|
+
"""
|
|
34
|
+
if group.isna().sum() > 0:
|
|
35
|
+
return np.nan
|
|
36
|
+
return group.max()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def daily_sum(group: pd.Series):
|
|
40
|
+
"""
|
|
41
|
+
Calculate the daily sum from a series of hourly data
|
|
42
|
+
"""
|
|
43
|
+
if group.isna().sum() > 0:
|
|
44
|
+
return np.nan
|
|
45
|
+
return group.sum()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Available parameters with source column and aggregation method
|
|
49
|
+
PARAMETER_AGGS = {
|
|
50
|
+
Parameter.TEMP: (Parameter.TEMP, daily_mean),
|
|
51
|
+
Parameter.TMIN: (Parameter.TEMP, daily_min),
|
|
52
|
+
Parameter.TMAX: (Parameter.TEMP, daily_max),
|
|
53
|
+
Parameter.RHUM: (Parameter.RHUM, daily_mean),
|
|
54
|
+
Parameter.DWPT: (Parameter.DWPT, daily_mean),
|
|
55
|
+
Parameter.PRCP: (Parameter.PRCP, daily_sum),
|
|
56
|
+
Parameter.SNWD: (Parameter.SNWD, daily_max),
|
|
57
|
+
Parameter.WSPD: (Parameter.WSPD, daily_mean),
|
|
58
|
+
Parameter.WPGT: (Parameter.WPGT, daily_max),
|
|
59
|
+
Parameter.PRES: (Parameter.PRES, daily_mean),
|
|
60
|
+
Parameter.TSUN: (Parameter.TSUN, daily_sum),
|
|
61
|
+
Parameter.CLDC: (Parameter.CLDC, daily_mean),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
66
|
+
"""
|
|
67
|
+
Fetch hourly weather data from Meteostat's central data
|
|
68
|
+
repository and aggregate to daily granularity
|
|
69
|
+
"""
|
|
70
|
+
# Get all source columns
|
|
71
|
+
source_cols = list(dict.fromkeys([PARAMETER_AGGS[p][0] for p in req.parameters]))
|
|
72
|
+
|
|
73
|
+
# Get hourly DataFrame
|
|
74
|
+
ts_hourly = hourly(
|
|
75
|
+
req.station.id,
|
|
76
|
+
req.start,
|
|
77
|
+
req.end,
|
|
78
|
+
parameters=source_cols,
|
|
79
|
+
providers=[Provider.HOURLY],
|
|
80
|
+
timezone=req.station.timezone,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
df_hourly = ts_hourly.fetch(fill=True, sources=True)
|
|
84
|
+
|
|
85
|
+
# If no hourly data is available, exit
|
|
86
|
+
if df_hourly is None:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# Create daily aggregations
|
|
90
|
+
df = pd.DataFrame()
|
|
91
|
+
for parameter in req.parameters:
|
|
92
|
+
[hourly_param_name, agg_func] = PARAMETER_AGGS[parameter]
|
|
93
|
+
df[parameter] = (
|
|
94
|
+
df_hourly[hourly_param_name]
|
|
95
|
+
.groupby(pd.Grouper(level="time", freq="1D"))
|
|
96
|
+
.agg(agg_func)
|
|
97
|
+
)
|
|
98
|
+
df[f"{parameter}_source"] = (
|
|
99
|
+
df_hourly[f"{hourly_param_name}_source"]
|
|
100
|
+
.groupby(pd.Grouper(level="time", freq="1D"))
|
|
101
|
+
.agg(aggregate_sources)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Adjust DataFrame and add index
|
|
105
|
+
df = df.round(1)
|
|
106
|
+
df.index = pd.to_datetime(df.index.astype("datetime64").dt.date) # type: ignore[union-attr]
|
|
107
|
+
df.index.name = "time"
|
|
108
|
+
|
|
109
|
+
# Return reshaped DataFrame
|
|
110
|
+
return reshape_by_source(df)
|
|
@@ -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.core.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.core.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.core.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.core.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
|
+
)
|