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.
- canairy-0.3.0rc1/LICENSE +28 -0
- canairy-0.3.0rc1/PKG-INFO +71 -0
- canairy-0.3.0rc1/README.md +44 -0
- canairy-0.3.0rc1/pyproject.toml +58 -0
- canairy-0.3.0rc1/setup.cfg +4 -0
- canairy-0.3.0rc1/src/canairy/__init__.py +0 -0
- canairy-0.3.0rc1/src/canairy/hh/__init__.py +2 -0
- canairy-0.3.0rc1/src/canairy/hh/_client.py +150 -0
- canairy-0.3.0rc1/src/canairy/hh/_models.py +126 -0
- canairy-0.3.0rc1/src/canairy/hh/_typing.py +22 -0
- canairy-0.3.0rc1/src/canairy/hh/aqi.py +29 -0
- canairy-0.3.0rc1/src/canairy/py.typed +0 -0
- canairy-0.3.0rc1/src/canairy/scripts/__init__.py +0 -0
- canairy-0.3.0rc1/src/canairy/scripts/downloader.py +87 -0
- canairy-0.3.0rc1/src/canairy/uba/__init__.py +1 -0
- canairy-0.3.0rc1/src/canairy/uba/_client.py +138 -0
- canairy-0.3.0rc1/src/canairy/uba/_models.py +33 -0
- canairy-0.3.0rc1/src/canairy/uba/_typing.py +8 -0
- canairy-0.3.0rc1/src/canairy/uba/components/__init__.py +2 -0
- canairy-0.3.0rc1/src/canairy/uba/components/_models.py +53 -0
- canairy-0.3.0rc1/src/canairy/uba/measures/__init__.py +1 -0
- canairy-0.3.0rc1/src/canairy/uba/measures/_models.py +128 -0
- canairy-0.3.0rc1/src/canairy/uba/networks/__init__.py +3 -0
- canairy-0.3.0rc1/src/canairy/uba/networks/_models.py +34 -0
- canairy-0.3.0rc1/src/canairy/uba/scopes/__init__.py +3 -0
- canairy-0.3.0rc1/src/canairy/uba/scopes/_models.py +55 -0
- canairy-0.3.0rc1/src/canairy/uba/stations/__init__.py +3 -0
- canairy-0.3.0rc1/src/canairy/uba/stations/_models.py +132 -0
- canairy-0.3.0rc1/src/canairy/uba/thresholds/__init__.py +3 -0
- canairy-0.3.0rc1/src/canairy/uba/thresholds/_models.py +42 -0
- canairy-0.3.0rc1/src/canairy/uba/utils.py +57 -0
- canairy-0.3.0rc1/src/canairy.egg-info/PKG-INFO +71 -0
- canairy-0.3.0rc1/src/canairy.egg-info/SOURCES.txt +35 -0
- canairy-0.3.0rc1/src/canairy.egg-info/dependency_links.txt +1 -0
- canairy-0.3.0rc1/src/canairy.egg-info/entry_points.txt +2 -0
- canairy-0.3.0rc1/src/canairy.egg-info/requires.txt +5 -0
- canairy-0.3.0rc1/src/canairy.egg-info/top_level.txt +1 -0
canairy-0.3.0rc1/LICENSE
ADDED
|
@@ -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"]
|
|
File without changes
|
|
@@ -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
|