meteostat 1.7.6__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meteostat/__init__.py +38 -19
- meteostat/api/config.py +158 -0
- meteostat/api/daily.py +76 -0
- meteostat/api/hourly.py +80 -0
- meteostat/api/interpolate.py +378 -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/data.py +203 -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/guards.py +51 -0
- meteostat/utils/parsers.py +161 -0
- meteostat/utils/types.py +113 -0
- meteostat/utils/validators.py +31 -0
- meteostat-2.0.1.dist-info/METADATA +130 -0
- meteostat-2.0.1.dist-info/RECORD +64 -0
- {meteostat-1.7.6.dist-info → meteostat-2.0.1.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.1.dist-info/licenses}/LICENSE +0 -0
meteostat/api/normals.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Climate Normals
|
|
3
|
+
|
|
4
|
+
Access climate normals data for one or multiple weather stations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from meteostat.enumerations import Parameter, Provider, Granularity
|
|
13
|
+
from meteostat.core.schema import schema_service
|
|
14
|
+
from meteostat.api.monthly import DEFAULT_PARAMETERS, monthly
|
|
15
|
+
from meteostat.api.timeseries import TimeSeries
|
|
16
|
+
from meteostat.typing import Station
|
|
17
|
+
from meteostat.api.point import Point
|
|
18
|
+
from meteostat.utils.data import reshape_by_source
|
|
19
|
+
from meteostat.utils.parsers import parse_year
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def normals(
|
|
23
|
+
station: str | Station | Point | List[str | Station | Point] | pd.DataFrame,
|
|
24
|
+
start: int = 1961,
|
|
25
|
+
end: int = 1990,
|
|
26
|
+
parameters: Optional[List[Parameter]] = None,
|
|
27
|
+
providers: Optional[List[Provider]] = None,
|
|
28
|
+
max_missing: int = 3,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Access climate normals data.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
station : str, Station, Point, List[str | Station | Point], pd.Index, pd.Series
|
|
36
|
+
Weather station(s) or Point(s) to query data for. Can be a single station/point or a list.
|
|
37
|
+
Points are converted to virtual stations with IDs like $0001, $0002, etc.
|
|
38
|
+
start : int, optional
|
|
39
|
+
Start year for the data query. Defaults to 1961.
|
|
40
|
+
end : int, optional
|
|
41
|
+
End year for the data query. Defaults to 1990.
|
|
42
|
+
parameters : List[Parameter], optional
|
|
43
|
+
List of parameters to include in the data query. Defaults to a set of common parameters.
|
|
44
|
+
providers : List[Provider], optional
|
|
45
|
+
List of data providers to use for the query. Defaults to the monthly provider.
|
|
46
|
+
max_missing : int, optional
|
|
47
|
+
Maximum number of missing values allowed in a month to calculate the mean. Defaults to 3.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
TimeSeries
|
|
52
|
+
A TimeSeries object containing the climate normals data for the specified
|
|
53
|
+
stations and parameters.
|
|
54
|
+
"""
|
|
55
|
+
if parameters is None:
|
|
56
|
+
parameters = DEFAULT_PARAMETERS
|
|
57
|
+
if providers is None:
|
|
58
|
+
providers = [Provider.MONTHLY]
|
|
59
|
+
|
|
60
|
+
def _mean(group: pd.Series):
|
|
61
|
+
"""
|
|
62
|
+
Calculate the monthly mean from multiple years of monthly data
|
|
63
|
+
"""
|
|
64
|
+
if group.isna().sum() > max_missing:
|
|
65
|
+
return np.nan
|
|
66
|
+
return group.mean()
|
|
67
|
+
|
|
68
|
+
# Fetch monthly data for the specified stations and parameters
|
|
69
|
+
ts = monthly(
|
|
70
|
+
station,
|
|
71
|
+
parse_year(start),
|
|
72
|
+
parse_year(end, True),
|
|
73
|
+
parameters=parameters,
|
|
74
|
+
providers=providers,
|
|
75
|
+
)
|
|
76
|
+
df = ts.fetch(sources=True)
|
|
77
|
+
|
|
78
|
+
# Process data if available
|
|
79
|
+
if df is not None and not df.empty:
|
|
80
|
+
# Add station level if only a single station is provided
|
|
81
|
+
if not ts._multi_station:
|
|
82
|
+
df = pd.concat(
|
|
83
|
+
[df],
|
|
84
|
+
keys=[ts.stations.index.get_level_values("id")[0]],
|
|
85
|
+
names=["station"],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Extract month from time index
|
|
89
|
+
df["month"] = df.index.get_level_values("time").month
|
|
90
|
+
|
|
91
|
+
# Create aggregation functions for different column types
|
|
92
|
+
agg_funcs = {}
|
|
93
|
+
for col in df.columns:
|
|
94
|
+
if col.endswith("_source"):
|
|
95
|
+
# For source columns, concatenate unique values
|
|
96
|
+
agg_funcs[col] = lambda x: ", ".join(x.dropna().unique())
|
|
97
|
+
elif col == "txmx":
|
|
98
|
+
# For temperature maximum, use max function
|
|
99
|
+
agg_funcs[col] = lambda x: x.max() if not x.isna().all() else np.nan
|
|
100
|
+
elif col == "txmn":
|
|
101
|
+
# For temperature minimum, use min function
|
|
102
|
+
agg_funcs[col] = lambda x: x.min() if not x.isna().all() else np.nan
|
|
103
|
+
else:
|
|
104
|
+
# For data columns, use the mean function
|
|
105
|
+
agg_funcs[col] = _mean
|
|
106
|
+
|
|
107
|
+
# Apply aggregation functions
|
|
108
|
+
df = df.groupby(["station", "month"]).agg(agg_funcs)
|
|
109
|
+
df = df.rename_axis(index={"month": "time"})
|
|
110
|
+
|
|
111
|
+
# Separate parameter columns from source columns for formatting
|
|
112
|
+
param_cols = [str(param) for param in parameters if str(param) in df.columns]
|
|
113
|
+
source_cols = [col for col in df.columns if col.endswith("_source")]
|
|
114
|
+
|
|
115
|
+
# Format only the parameter columns
|
|
116
|
+
if param_cols:
|
|
117
|
+
df_data = schema_service.format(df[param_cols], Granularity.NORMALS)
|
|
118
|
+
# Combine formatted parameter columns with unformatted source columns
|
|
119
|
+
if source_cols:
|
|
120
|
+
df = pd.concat([df_data, df[source_cols]], axis=1)
|
|
121
|
+
else:
|
|
122
|
+
df = df_data
|
|
123
|
+
|
|
124
|
+
# Reshape data by source for each station
|
|
125
|
+
df_fragments = []
|
|
126
|
+
for s in df.index.get_level_values("station").unique():
|
|
127
|
+
station_df = df.loc[s]
|
|
128
|
+
fragment = reshape_by_source(station_df)
|
|
129
|
+
fragment = pd.concat([fragment], keys=[s], names=["station"])
|
|
130
|
+
df_fragments.append(fragment)
|
|
131
|
+
|
|
132
|
+
df = pd.concat(df_fragments)
|
|
133
|
+
else:
|
|
134
|
+
df = None
|
|
135
|
+
|
|
136
|
+
# Return the final TimeSeries object
|
|
137
|
+
return TimeSeries(
|
|
138
|
+
granularity=Granularity.NORMALS,
|
|
139
|
+
stations=ts.stations,
|
|
140
|
+
df=df,
|
|
141
|
+
start=ts.start,
|
|
142
|
+
end=ts.end,
|
|
143
|
+
multi_station=ts._multi_station,
|
|
144
|
+
)
|
meteostat/api/point.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Point Class
|
|
3
|
+
|
|
4
|
+
A geographical point used for querying nearby weather stations and spatial interpolation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Point:
|
|
11
|
+
"""
|
|
12
|
+
A geographical point
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
latitude: float
|
|
16
|
+
longitude: float
|
|
17
|
+
elevation: Optional[int]
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self, latitude: float, longitude: float, elevation: Optional[int] = None
|
|
21
|
+
) -> None:
|
|
22
|
+
if latitude < -90 or latitude > 90:
|
|
23
|
+
raise ValueError("Latitude must be between -90 and 90")
|
|
24
|
+
|
|
25
|
+
if longitude < -180 or longitude > 180:
|
|
26
|
+
raise ValueError("Longitude must be between -180 and 180")
|
|
27
|
+
|
|
28
|
+
self.latitude = latitude
|
|
29
|
+
self.longitude = longitude
|
|
30
|
+
self.elevation = elevation
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stations Module
|
|
3
|
+
|
|
4
|
+
Provides the Stations class for working with weather station metadata.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from io import BytesIO
|
|
10
|
+
import sqlite3
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from requests import Response
|
|
15
|
+
from meteostat.api.inventory import Inventory
|
|
16
|
+
from meteostat.api.point import Point
|
|
17
|
+
from meteostat.core.cache import cache_service
|
|
18
|
+
from meteostat.api.config import config
|
|
19
|
+
from meteostat.core.logger import logger
|
|
20
|
+
from meteostat.core.network import network_service
|
|
21
|
+
from meteostat.enumerations import Provider
|
|
22
|
+
from meteostat.typing import Station
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Stations:
|
|
26
|
+
"""
|
|
27
|
+
Stations Database
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def _fetch_file(self, stream=False) -> Response:
|
|
31
|
+
"""
|
|
32
|
+
Download the SQLite database file from the configured URL
|
|
33
|
+
"""
|
|
34
|
+
urls = config.stations_db_endpoints
|
|
35
|
+
|
|
36
|
+
if not urls:
|
|
37
|
+
raise Exception("No stations database URLs configured")
|
|
38
|
+
|
|
39
|
+
response = network_service.get_from_mirrors(urls, stream=stream)
|
|
40
|
+
|
|
41
|
+
if response is None:
|
|
42
|
+
raise Exception("Failed to download the database file")
|
|
43
|
+
|
|
44
|
+
return response
|
|
45
|
+
|
|
46
|
+
def _get_file_path(self) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Get the file path for the SQLite database
|
|
49
|
+
"""
|
|
50
|
+
filepath = config.stations_db_file
|
|
51
|
+
ttl = config.stations_db_ttl
|
|
52
|
+
|
|
53
|
+
if os.path.exists(filepath) and not cache_service.is_stale(filepath, ttl):
|
|
54
|
+
return filepath
|
|
55
|
+
|
|
56
|
+
# Download the database file
|
|
57
|
+
response = self._fetch_file(stream=True)
|
|
58
|
+
|
|
59
|
+
# Create cache directory if it doesn't exist
|
|
60
|
+
cache_service.create_cache_dir()
|
|
61
|
+
|
|
62
|
+
with open(filepath, "wb") as file:
|
|
63
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
64
|
+
file.write(chunk)
|
|
65
|
+
|
|
66
|
+
return filepath
|
|
67
|
+
|
|
68
|
+
def _connect_memory(self) -> sqlite3.Connection:
|
|
69
|
+
"""
|
|
70
|
+
Create an in-memory SQLite database and load the downloaded database file into it
|
|
71
|
+
"""
|
|
72
|
+
# Download the database file
|
|
73
|
+
response = self._fetch_file()
|
|
74
|
+
|
|
75
|
+
# Create an in-memory SQLite database
|
|
76
|
+
conn = sqlite3.connect(":memory:")
|
|
77
|
+
|
|
78
|
+
# Read the downloaded database file into memory
|
|
79
|
+
content = BytesIO(response.content)
|
|
80
|
+
|
|
81
|
+
# Convert bytes to string and write the content to the in-memory database
|
|
82
|
+
conn.deserialize(content.read())
|
|
83
|
+
|
|
84
|
+
return conn
|
|
85
|
+
|
|
86
|
+
def _connect_fs(self) -> sqlite3.Connection:
|
|
87
|
+
"""
|
|
88
|
+
Connect to the SQLite database file on the filesystem
|
|
89
|
+
"""
|
|
90
|
+
file = self._get_file_path()
|
|
91
|
+
|
|
92
|
+
if not file:
|
|
93
|
+
raise FileNotFoundError("SQLite database file not found")
|
|
94
|
+
|
|
95
|
+
conn = sqlite3.connect(file)
|
|
96
|
+
|
|
97
|
+
return conn
|
|
98
|
+
|
|
99
|
+
def connect(self, in_memory: Optional[bool] = None) -> sqlite3.Connection:
|
|
100
|
+
"""
|
|
101
|
+
Connect to the database
|
|
102
|
+
"""
|
|
103
|
+
if in_memory is None:
|
|
104
|
+
in_memory = config.stations_db_file is None
|
|
105
|
+
|
|
106
|
+
logger.info("Connecting to stations database (in_memory=%s)", in_memory)
|
|
107
|
+
|
|
108
|
+
if in_memory:
|
|
109
|
+
return self._connect_memory()
|
|
110
|
+
|
|
111
|
+
return self._connect_fs()
|
|
112
|
+
|
|
113
|
+
def query(
|
|
114
|
+
self,
|
|
115
|
+
sql: str,
|
|
116
|
+
index_col: Optional[str | list] = None,
|
|
117
|
+
params: Optional[tuple | dict] = None,
|
|
118
|
+
) -> pd.DataFrame:
|
|
119
|
+
"""
|
|
120
|
+
Execute a SQL query and return the result as a DataFrame
|
|
121
|
+
"""
|
|
122
|
+
with self.connect() as conn:
|
|
123
|
+
df = pd.read_sql(sql, conn, index_col=index_col, params=params)
|
|
124
|
+
|
|
125
|
+
return df
|
|
126
|
+
|
|
127
|
+
def meta(self, station: str) -> Station:
|
|
128
|
+
"""
|
|
129
|
+
Get meta data for a specific weather station
|
|
130
|
+
"""
|
|
131
|
+
meta = self.query(
|
|
132
|
+
"""
|
|
133
|
+
SELECT
|
|
134
|
+
`stations`.*,
|
|
135
|
+
`names`.`name` as `name`
|
|
136
|
+
FROM `stations`
|
|
137
|
+
LEFT JOIN `names` ON `stations`.`id` = `names`.`station`
|
|
138
|
+
AND `names`.`language` = 'en'
|
|
139
|
+
WHERE `stations`.`id` LIKE ?
|
|
140
|
+
""",
|
|
141
|
+
index_col="id",
|
|
142
|
+
params=(station,),
|
|
143
|
+
).to_dict("records")[0]
|
|
144
|
+
|
|
145
|
+
identifiers = self.query(
|
|
146
|
+
"SELECT `key`, `value` FROM `identifiers` WHERE `station` LIKE ?",
|
|
147
|
+
params=(station,),
|
|
148
|
+
).to_dict("records")
|
|
149
|
+
|
|
150
|
+
return Station(
|
|
151
|
+
id=station,
|
|
152
|
+
**meta,
|
|
153
|
+
identifiers={
|
|
154
|
+
identifier["key"]: identifier["value"] for identifier in identifiers
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def inventory(
|
|
159
|
+
self, station: str | List[str], providers: Optional[List[Provider]] = None
|
|
160
|
+
) -> Inventory:
|
|
161
|
+
"""
|
|
162
|
+
Get inventory records for a single weather station
|
|
163
|
+
"""
|
|
164
|
+
query = "SELECT station, provider, parameter, start, end, completeness FROM `inventory`"
|
|
165
|
+
station_list = station if isinstance(station, list) else [station]
|
|
166
|
+
|
|
167
|
+
# Generate the right number of placeholders (?, ?, ?, ...)
|
|
168
|
+
placeholders = ", ".join(["?"] * len(station_list))
|
|
169
|
+
# Add the placeholders to the query
|
|
170
|
+
query += f" WHERE `station` IN ({placeholders})"
|
|
171
|
+
# Add the stations to the params
|
|
172
|
+
params = tuple(station_list)
|
|
173
|
+
|
|
174
|
+
if providers:
|
|
175
|
+
# Generate the right number of placeholders (?, ?, ?, ...)
|
|
176
|
+
placeholders = ", ".join(["?"] * len(providers))
|
|
177
|
+
# Add the placeholders to the query
|
|
178
|
+
query += f" AND provider IN ({placeholders})"
|
|
179
|
+
# Add the providers to the params
|
|
180
|
+
params += tuple(providers)
|
|
181
|
+
|
|
182
|
+
df = self.query(
|
|
183
|
+
query, index_col=["station", "provider", "parameter"], params=params
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return Inventory(df)
|
|
187
|
+
|
|
188
|
+
def nearby(self, point: Point, radius=50000, limit=100) -> pd.DataFrame:
|
|
189
|
+
"""
|
|
190
|
+
Get a list of weather station IDs ordered by distance
|
|
191
|
+
"""
|
|
192
|
+
return self.query(
|
|
193
|
+
"""
|
|
194
|
+
SELECT
|
|
195
|
+
`id`,
|
|
196
|
+
`names`.`name` as `name`,
|
|
197
|
+
`country`,
|
|
198
|
+
`region`,
|
|
199
|
+
`latitude`,
|
|
200
|
+
`longitude`,
|
|
201
|
+
`elevation`,
|
|
202
|
+
`timezone`,
|
|
203
|
+
ROUND(
|
|
204
|
+
(
|
|
205
|
+
6371000 * acos(
|
|
206
|
+
cos(radians(:lat)) * cos(radians(`latitude`)) *
|
|
207
|
+
cos(radians(`longitude`) - radians(:lon)) +
|
|
208
|
+
sin(radians(:lat)) * sin(radians(`latitude`))
|
|
209
|
+
)
|
|
210
|
+
),
|
|
211
|
+
1
|
|
212
|
+
) AS `distance`
|
|
213
|
+
FROM
|
|
214
|
+
`stations`
|
|
215
|
+
INNER JOIN `names` ON `stations`.`id` = `names`.`station`
|
|
216
|
+
AND `names`.`language` = "en"
|
|
217
|
+
WHERE
|
|
218
|
+
`distance` <= :radius
|
|
219
|
+
ORDER BY
|
|
220
|
+
`distance`
|
|
221
|
+
LIMIT
|
|
222
|
+
:limit
|
|
223
|
+
""",
|
|
224
|
+
index_col="id",
|
|
225
|
+
params={
|
|
226
|
+
"lat": point.latitude,
|
|
227
|
+
"lon": point.longitude,
|
|
228
|
+
"radius": radius,
|
|
229
|
+
"limit": limit,
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
stations = Stations()
|