canairy 0.3.0rc1__tar.gz

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 (37) hide show
  1. canairy-0.3.0rc1/LICENSE +28 -0
  2. canairy-0.3.0rc1/PKG-INFO +71 -0
  3. canairy-0.3.0rc1/README.md +44 -0
  4. canairy-0.3.0rc1/pyproject.toml +58 -0
  5. canairy-0.3.0rc1/setup.cfg +4 -0
  6. canairy-0.3.0rc1/src/canairy/__init__.py +0 -0
  7. canairy-0.3.0rc1/src/canairy/hh/__init__.py +2 -0
  8. canairy-0.3.0rc1/src/canairy/hh/_client.py +150 -0
  9. canairy-0.3.0rc1/src/canairy/hh/_models.py +126 -0
  10. canairy-0.3.0rc1/src/canairy/hh/_typing.py +22 -0
  11. canairy-0.3.0rc1/src/canairy/hh/aqi.py +29 -0
  12. canairy-0.3.0rc1/src/canairy/py.typed +0 -0
  13. canairy-0.3.0rc1/src/canairy/scripts/__init__.py +0 -0
  14. canairy-0.3.0rc1/src/canairy/scripts/downloader.py +87 -0
  15. canairy-0.3.0rc1/src/canairy/uba/__init__.py +1 -0
  16. canairy-0.3.0rc1/src/canairy/uba/_client.py +138 -0
  17. canairy-0.3.0rc1/src/canairy/uba/_models.py +33 -0
  18. canairy-0.3.0rc1/src/canairy/uba/_typing.py +8 -0
  19. canairy-0.3.0rc1/src/canairy/uba/components/__init__.py +2 -0
  20. canairy-0.3.0rc1/src/canairy/uba/components/_models.py +53 -0
  21. canairy-0.3.0rc1/src/canairy/uba/measures/__init__.py +1 -0
  22. canairy-0.3.0rc1/src/canairy/uba/measures/_models.py +128 -0
  23. canairy-0.3.0rc1/src/canairy/uba/networks/__init__.py +3 -0
  24. canairy-0.3.0rc1/src/canairy/uba/networks/_models.py +34 -0
  25. canairy-0.3.0rc1/src/canairy/uba/scopes/__init__.py +3 -0
  26. canairy-0.3.0rc1/src/canairy/uba/scopes/_models.py +55 -0
  27. canairy-0.3.0rc1/src/canairy/uba/stations/__init__.py +3 -0
  28. canairy-0.3.0rc1/src/canairy/uba/stations/_models.py +132 -0
  29. canairy-0.3.0rc1/src/canairy/uba/thresholds/__init__.py +3 -0
  30. canairy-0.3.0rc1/src/canairy/uba/thresholds/_models.py +42 -0
  31. canairy-0.3.0rc1/src/canairy/uba/utils.py +57 -0
  32. canairy-0.3.0rc1/src/canairy.egg-info/PKG-INFO +71 -0
  33. canairy-0.3.0rc1/src/canairy.egg-info/SOURCES.txt +35 -0
  34. canairy-0.3.0rc1/src/canairy.egg-info/dependency_links.txt +1 -0
  35. canairy-0.3.0rc1/src/canairy.egg-info/entry_points.txt +2 -0
  36. canairy-0.3.0rc1/src/canairy.egg-info/requires.txt +5 -0
  37. canairy-0.3.0rc1/src/canairy.egg-info/top_level.txt +1 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Institut für Angewandte Medizininformatik
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: canairy
3
+ Version: 0.3.0rc1
4
+ Summary: Client for German public air-quality APIs (UBA, Hamburger Luftmessnetz)
5
+ Author-email: Michael Blaß <m.blass@uke.de>
6
+ Classifier: License :: OSI Approved :: BSD License
7
+ Classifier: Programming Language :: Python :: 3.11
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: Implementation :: CPython
11
+ Classifier: Framework :: Pydantic :: 2
12
+ Classifier: Framework :: Pytest
13
+ Classifier: Typing :: Typed
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
16
+ Classifier: Intended Audience :: Healthcare Industry
17
+ Classifier: Intended Audience :: Science/Research
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: hishel>=1.0
22
+ Requires-Dist: httpx-limiter>=0.2.0
23
+ Requires-Dist: httpx>=0.28.1
24
+ Requires-Dist: pydantic>=2.11.3
25
+ Requires-Dist: ratelimit>=2.2.1
26
+ Dynamic: license-file
27
+
28
+ # canairy
29
+ Python client for public air-quality APIs: Umweltbundesamt (UBA) and Hamburger Luftmessnetz.
30
+
31
+ ## Example 1
32
+ Use data from the Umwelt Bundesamt (UBA)
33
+
34
+ ```python
35
+ from datetime import datetime
36
+
37
+ from canairy.uba import UBAClient
38
+ import polars as pl
39
+
40
+
41
+ client = UBAClient()
42
+
43
+ # plot hourly NO2 values measured at station 'DENW124' between January and February 2019
44
+ rsp = client.get_measures(
45
+ datetime(2019, 1, 1, 0, 0),
46
+ datetime(2019, 2, 1, 0, 0),
47
+ "DENW134",
48
+ 5,
49
+ 2
50
+ )
51
+ df = pl.DataFrame(rsp.data)
52
+ df.plot.line(x='date_start', y='value').properties(width=600)
53
+ ```
54
+
55
+ ## Example 2
56
+ Use data from the Hamburger Luftmessnetz
57
+
58
+ ```python
59
+ Display monthly PM10 data.
60
+
61
+ import canairy.hh as hh
62
+ import polars as pl
63
+
64
+
65
+ client = hh.HHClient()
66
+
67
+ res = client.get_component_data("pm10_1m")
68
+ res = pl.DataFrame(res)
69
+
70
+ res.plot.line(x='datetime', y='value', color='station').properties(width=800, height=400)
71
+ ```
@@ -0,0 +1,44 @@
1
+ # canairy
2
+ Python client for public air-quality APIs: Umweltbundesamt (UBA) and Hamburger Luftmessnetz.
3
+
4
+ ## Example 1
5
+ Use data from the Umwelt Bundesamt (UBA)
6
+
7
+ ```python
8
+ from datetime import datetime
9
+
10
+ from canairy.uba import UBAClient
11
+ import polars as pl
12
+
13
+
14
+ client = UBAClient()
15
+
16
+ # plot hourly NO2 values measured at station 'DENW124' between January and February 2019
17
+ rsp = client.get_measures(
18
+ datetime(2019, 1, 1, 0, 0),
19
+ datetime(2019, 2, 1, 0, 0),
20
+ "DENW134",
21
+ 5,
22
+ 2
23
+ )
24
+ df = pl.DataFrame(rsp.data)
25
+ df.plot.line(x='date_start', y='value').properties(width=600)
26
+ ```
27
+
28
+ ## Example 2
29
+ Use data from the Hamburger Luftmessnetz
30
+
31
+ ```python
32
+ Display monthly PM10 data.
33
+
34
+ import canairy.hh as hh
35
+ import polars as pl
36
+
37
+
38
+ client = hh.HHClient()
39
+
40
+ res = client.get_component_data("pm10_1m")
41
+ res = pl.DataFrame(res)
42
+
43
+ res.plot.line(x='datetime', y='value', color='station').properties(width=800, height=400)
44
+ ```
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "canairy"
3
+ version = "0.3.0rc1"
4
+ description = "Client for German public air-quality APIs (UBA, Hamburger Luftmessnetz)"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Michael Blaß", email = "m.blass@uke.de" }
8
+ ]
9
+
10
+ classifiers = [
11
+ "License :: OSI Approved :: BSD License",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Programming Language :: Python :: Implementation :: CPython",
16
+ "Framework :: Pydantic :: 2",
17
+ "Framework :: Pytest",
18
+ "Typing :: Typed",
19
+ "Topic :: Scientific/Engineering",
20
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
21
+ "Intended Audience :: Healthcare Industry",
22
+ "Intended Audience :: Science/Research"
23
+ ]
24
+
25
+ requires-python = ">=3.11"
26
+ dependencies = [
27
+ "hishel>=1.0",
28
+ "httpx-limiter>=0.2.0",
29
+ "httpx>=0.28.1",
30
+ "pydantic>=2.11.3",
31
+ "ratelimit>=2.2.1",
32
+ ]
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "ipykernel>=6.29.5",
37
+ "mypy>=1.15.0",
38
+ "pytest>=8.3.5",
39
+ "ruff>=0.11.5",
40
+ ]
41
+
42
+ [build-system]
43
+ requires = ["setuptools>=79"]
44
+ build-backend = "setuptools.build_meta"
45
+
46
+
47
+ [project.scripts]
48
+ aq-downloader = "canairy.scripts.downloader:main"
49
+
50
+ [[tool.mypy.overrides]]
51
+ module = "ratelimit"
52
+ ignore_missing_imports = true
53
+
54
+ [tool.mypy]
55
+ plugins = ["pydantic.mypy"]
56
+
57
+ [tool.ruff.lint]
58
+ extend-select = ["B"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,2 @@
1
+ from . import aqi as aqi
2
+ from ._client import HHClient as HHClient
@@ -0,0 +1,150 @@
1
+ from datetime import datetime
2
+ import json
3
+ import time
4
+ from typing import Any
5
+ import logging
6
+
7
+ from hishel.httpx import SyncCacheClient
8
+ import httpx
9
+ from ratelimit import limits, sleep_and_retry
10
+
11
+ from httpx._types import QueryParamTypes
12
+
13
+ from ._models import (
14
+ BaseParams,
15
+ HHComponent,
16
+ HHComponentParams,
17
+ HHComponentData,
18
+ HHComponentDataParams,
19
+ HHCore,
20
+ HHStation,
21
+ HHStationDataParams,
22
+ HHThreshold,
23
+ HHListParams,
24
+ HHStationData,
25
+ )
26
+
27
+ from ._typing import GroupCodes_T, TimeSpans_T, Periods_T
28
+
29
+ __all__ = ["HHClient"]
30
+
31
+ logger = logging.getLogger(__name__)
32
+ LOG_FMT = "%(levelname)-10s %(message)s"
33
+ MAX_RETRIES = 8
34
+
35
+
36
+ class HHClient:
37
+ BASE_URL = "https://hamburg.luftmessnetz.de/api"
38
+
39
+ def __init__(self) -> None:
40
+ self._client = SyncCacheClient(timeout=60)
41
+
42
+ def get_components(
43
+ self,
44
+ *,
45
+ active: bool | None = None,
46
+ group_code: GroupCodes_T | None = None,
47
+ ) -> list[HHComponent]:
48
+ """Retrieve component's metadata."""
49
+ params = HHComponentParams(active=active, group_code=group_code)
50
+ return HHComponent.from_response(self._get("components", params))
51
+
52
+ def get_component_data(
53
+ self,
54
+ code: str,
55
+ *,
56
+ stationgroup: str | None = None,
57
+ timespan: TimeSpans_T | None = None,
58
+ start: datetime | None = None,
59
+ end: datetime | None = None,
60
+ ) -> list[HHComponentData]:
61
+ params = HHComponentDataParams(
62
+ stationgroup=stationgroup, timespan=timespan, start=start, end=end
63
+ )
64
+ result = self._get(f"components/{code}/data", params)
65
+ return HHComponentData.from_response(result)
66
+
67
+ def get_cores(self) -> list[HHCore]:
68
+ """Retrieve core metadata."""
69
+ return HHCore.from_response(self._get("cores"))
70
+
71
+ def get_thresholds(self) -> list[HHThreshold]:
72
+ """Retrieve the thresholds for each each core."""
73
+ return HHThreshold.from_response(self._get("exceeds"))
74
+
75
+ def get_stations(
76
+ self,
77
+ *,
78
+ active: bool | None = None,
79
+ include_hidden: bool | None = None,
80
+ ) -> list[HHStation]:
81
+ """Retrieve a list of all measuring stations and their metadata."""
82
+ params = HHListParams(active=active, include_hidden=include_hidden)
83
+ return HHStation.from_response(self._get("stations", params=params))
84
+
85
+ def get_station(self, code: str) -> HHStation:
86
+ """Retrieve metadata of a single station."""
87
+ return HHStation(**self._get(f"stations/{code}"))
88
+
89
+ def get_station_data(
90
+ self,
91
+ station_code: str,
92
+ *,
93
+ period: Periods_T,
94
+ timespan: TimeSpans_T,
95
+ start: datetime | None = None,
96
+ end: datetime | None = None,
97
+ core: str | None = None,
98
+ ) -> list[HHStationData]:
99
+ params = HHStationDataParams(
100
+ period=period, timespan=timespan, start=start, end=end, core=core
101
+ )
102
+ return HHStationData.from_response(
103
+ self._get(f"stations/{station_code}/data", params)
104
+ )
105
+
106
+ @sleep_and_retry
107
+ @limits(120, period=60)
108
+ def _get(self, endpoint: str, params: BaseParams | None = None) -> Any:
109
+ params_ = params.model_dump(exclude_none=True) if params else None
110
+ meta = self._client.head(self._url(endpoint), params=params_).raise_for_status()
111
+
112
+ if (meta.headers.get("Transfer-Encoding") == "chunked") or (
113
+ "content-length" not in meta.headers
114
+ ):
115
+ logging.debug("Streaming response")
116
+ return backoff(self._stream, self._url(endpoint), params=params_)
117
+
118
+ logging.debug("Non-streaming response")
119
+ return (
120
+ backoff(self._client.get, self._url(endpoint), params=params_)
121
+ .raise_for_status()
122
+ .json()
123
+ )
124
+
125
+ def _stream(
126
+ self,
127
+ endpoint: str,
128
+ *,
129
+ params: QueryParamTypes | None = None,
130
+ method: str = "GET",
131
+ ) -> Any:
132
+ with self._client.stream(method, endpoint, params=params) as stream:
133
+ stream.raise_for_status()
134
+ buff = json.loads("".join(data for data in stream.iter_text()))
135
+ return buff
136
+
137
+ def _url(self, endpoint: str) -> str:
138
+ return f"{HHClient.BASE_URL}/{endpoint}"
139
+
140
+
141
+
142
+ def backoff(func, *args, **kwargs) -> Any: # type: ignore
143
+ base = 2
144
+ for i in range(1, MAX_RETRIES + 1):
145
+ try:
146
+ return func(*args, **kwargs)
147
+ except httpx.RemoteProtocolError:
148
+ logging.debug(f"RemoteProtoclolError: retry {i}")
149
+ timeout = base**i
150
+ time.sleep(timeout)
@@ -0,0 +1,126 @@
1
+ from datetime import datetime
2
+ from typing import Annotated, Any, Self
3
+
4
+ from pydantic import BaseModel, field_serializer, Field
5
+
6
+ from ._typing import GroupCodes_T, Periods_T, TimeSpans_T, StationGroup_T
7
+
8
+
9
+ class BaseParams(BaseModel):
10
+ pass
11
+
12
+ class _ActiveParam(BaseParams):
13
+ active: bool | None = None
14
+
15
+ class _HiddenParam(BaseParams):
16
+ include_hidden: bool | None = None
17
+
18
+ class HHListParams(_ActiveParam, _HiddenParam):
19
+ pass
20
+
21
+ class ItemListModel(BaseParams):
22
+ @classmethod
23
+ def from_response(cls, result: list[Any]) -> list[Self]:
24
+ return [cls(**item) for item in result]
25
+
26
+
27
+
28
+ class HHCore(ItemListModel):
29
+ code: str
30
+ active: bool
31
+ group: StationGroup_T
32
+ name: str
33
+ short_name: Annotated[str, Field(validation_alias="shortName")]
34
+ description: str | None
35
+ default_period: Annotated[str, Field(validation_alias="defaultPeriod")]
36
+ periods: list[str]
37
+ decimal_points: Annotated[int, Field(validation_alias="decimalPoints")]
38
+ components: list[str]
39
+
40
+
41
+ class HHThreshold(ItemListModel):
42
+ active: bool
43
+ code: str
44
+ component: str
45
+ core: str
46
+ name: str
47
+ description: str | None
48
+ value: float
49
+ max_count: Annotated[int | None, Field(validation_alias="maxCount")]
50
+ start: datetime | None
51
+
52
+
53
+ class DatetimeRangeParams(BaseParams):
54
+ start: datetime | None = None
55
+ end: datetime | None = None
56
+
57
+ @field_serializer("start", "end")
58
+ def custom_dt(self, dt: datetime) -> str:
59
+ return dt.strftime("%d.%m.%Y %H:%M")
60
+
61
+
62
+ class HHComponentParams(_ActiveParam):
63
+ group_code: GroupCodes_T | None
64
+
65
+
66
+ class HHStation(ItemListModel):
67
+ name: str
68
+ code: str
69
+ code_eu: Annotated[str, Field(validation_alias="codeEu")]
70
+ address: str
71
+ lat: str
72
+ lng: str
73
+ active: bool
74
+ stationgroups: list[str]
75
+ measuring_start: Annotated[
76
+ datetime | None, Field(validation_alias="measuringStart")
77
+ ]
78
+ measuring_end: Annotated[datetime | None, Field(validation_alias="measuringEnd")]
79
+ measuring_height: Annotated[int | None, Field(validation_alias="measuringHeight")]
80
+ url: str
81
+ information: str | None
82
+ components: list[str]
83
+ active_components: Annotated[list[str], Field(validation_alias="activeComponents")]
84
+ partials: list
85
+ lqis: list[str]
86
+ exceeds: list[str]
87
+
88
+
89
+ class HHStationData(ItemListModel):
90
+ datetime: datetime
91
+ station: str
92
+ core: str
93
+ component: str
94
+ period: Periods_T
95
+ value: int
96
+
97
+
98
+ class HHComponentDataParams(DatetimeRangeParams):
99
+ stationgroup: str | None = None
100
+ timespan: TimeSpans_T | None = None
101
+
102
+
103
+ class HHComponentData(ItemListModel):
104
+ datetime: datetime
105
+ station: str
106
+ core: str
107
+ component: str
108
+ period: Periods_T
109
+ value: float | None
110
+
111
+
112
+ class HHComponent(ItemListModel):
113
+ code: str
114
+ core: str
115
+ period: str
116
+ description: str | None
117
+ unit: str
118
+ min: int
119
+ max: int
120
+ decimal_points: Annotated[int, Field(validation_alias="decimalPoints")]
121
+
122
+
123
+ class HHStationDataParams(DatetimeRangeParams):
124
+ period: Periods_T
125
+ timespan: TimeSpans_T
126
+ core: str | None
@@ -0,0 +1,22 @@
1
+ from typing import Literal
2
+
3
+
4
+ Periods_T = Literal["1h", "24h", "8hg", "24hg", "1m", "1y", "1yaot40", "5yaot40"]
5
+
6
+ TimeSpans_T = Literal[
7
+ "currentday",
8
+ "currentmonth",
9
+ "currentyear",
10
+ "lastday",
11
+ "lastweek",
12
+ "lastmonth",
13
+ "lastyear",
14
+ "all",
15
+ "custom",
16
+ ]
17
+
18
+ StationGroup_T = Literal[
19
+ "all", "background", "industrial", "meteorology", "ozone", "special", "traffic", "pollution"
20
+ ]
21
+
22
+ GroupCodes_T = Literal["meteorology", "pollution"]
@@ -0,0 +1,29 @@
1
+ __all__ = ["components", "clf_bins", "clf_thresholds", "ratings"]
2
+
3
+
4
+ components = ("no2_1h", "so2_1h", "o3_1h", "co_8hg", "pm10_24hg")
5
+
6
+ ratings = {
7
+ 1: "sehr gut",
8
+ 2: "gut",
9
+ 3: "befriedigend",
10
+ 4: "ausreichend",
11
+ 5: "schlecht",
12
+ 6: "sehr schlecht",
13
+ }
14
+
15
+ clf_bins = {
16
+ "no2_1h": [25, 50, 100, 200, 500],
17
+ "so2_1h": [25, 50, 120, 350, 1000],
18
+ "o3_1h": [33, 65, 120, 180, 240],
19
+ "co_8hg": [1, 2, 4, 10, 30],
20
+ "pm10_24hg": [10, 20, 35, 50, 100],
21
+ }
22
+
23
+ clf_thresholds = {
24
+ "no2_1h": 200,
25
+ "so2_1h": 350,
26
+ "o3_1h": 180,
27
+ "co_8hg": 10,
28
+ "pm10_24hg": 50,
29
+ }
File without changes
File without changes
@@ -0,0 +1,87 @@
1
+ import argparse
2
+ import datetime
3
+ import pathlib
4
+ import pickle
5
+ import logging
6
+ import sys
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from canairy.uba import UBAClient
11
+
12
+
13
+ class Component(BaseModel):
14
+ id: int
15
+ code: str
16
+ scope: int
17
+
18
+
19
+ DEFAULT_COMPONENTS = [
20
+ Component(id=1, code="PM10", scope=2),
21
+ Component(id=2, code="CO", scope=4),
22
+ Component(id=3, code="O3", scope=2),
23
+ Component(id=4, code="SO2", scope=2),
24
+ Component(id=5, code="NO2", scope=2),
25
+ Component(id=9, code="PM2", scope=2),
26
+ ]
27
+
28
+
29
+ def main(argv=None) -> int:
30
+ if argv is None:
31
+ argv = sys.argv
32
+ args = _parse_cl(argv)
33
+
34
+ if not args.outpath.exists():
35
+ args.outpath.mkdir(parents=True)
36
+
37
+ logging.basicConfig(
38
+ filename=str(args.outpath.joinpath("download.log")),
39
+ level=logging.INFO,
40
+ format="%(asctime)s - %(levelname)s - %(message)s"
41
+ )
42
+
43
+ client = UBAClient()
44
+ download_multi_station(client, **vars(args))
45
+
46
+ return 0
47
+
48
+
49
+ def download_multi_station(client, year_start: int, outpath: pathlib.Path) -> None:
50
+
51
+ for year in range(year_start, datetime.date.today().year):
52
+ logging.info(f"New year: {year}")
53
+ date_dir = outpath.joinpath(str(year))
54
+ if not date_dir.exists():
55
+ date_dir.mkdir(parents=True)
56
+
57
+ for station in client.get_stations().data:
58
+ logging.info(f"Station: {station.code}")
59
+ station_dir = date_dir.joinpath(f"{station.code}")
60
+ if not station_dir.exists():
61
+ station_dir.mkdir()
62
+ for component in DEFAULT_COMPONENTS:
63
+ logging.info(f"Component: {component.code}")
64
+ res = client.get_measures(
65
+ datetime.datetime(year, 1, 1, 0, 0),
66
+ datetime.datetime(year+1, 12, 31, 23, 0),
67
+ station.id,
68
+ component.id,
69
+ component.scope,
70
+ )
71
+ fname = station_dir.joinpath(f"{component.code}_{component.scope}.pkl")
72
+ with fname.open("wb") as file:
73
+ pickle.dump(res.model_dump(mode="json"), file)
74
+
75
+
76
+ def _parse_cl(argv) -> argparse.Namespace:
77
+ parser = argparse.ArgumentParser()
78
+ parser.add_argument("-y", "--year-start", type=int, default=2016)
79
+ parser.add_argument("outpath", type=pathlib.Path)
80
+ return parser.parse_args(argv[1:])
81
+
82
+
83
+ def _int_or_str(val: str) -> int | str:
84
+ try:
85
+ return int(val)
86
+ except ValueError:
87
+ return val
@@ -0,0 +1 @@
1
+ from ._client import UBAClient as UBAClient