cloudnet-api-client 0.4.1__tar.gz → 0.5.1__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.
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/CHANGELOG.md +10 -0
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/PKG-INFO +7 -6
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/README.md +6 -5
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/cloudnet_api_client/client.py +72 -6
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/cloudnet_api_client/containers.py +16 -1
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/cloudnet_api_client/dl.py +11 -6
- cloudnet_api_client-0.5.1/cloudnet_api_client/version.py +1 -0
- cloudnet_api_client-0.4.1/cloudnet_api_client/version.py +0 -1
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/.github/workflows/publish.yml +0 -0
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/.github/workflows/test.yml +0 -0
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/.gitignore +0 -0
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/.pre-commit-config.yaml +0 -0
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/LICENSE +0 -0
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/cloudnet_api_client/__init__.py +0 -0
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/cloudnet_api_client/py.typed +0 -0
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/cloudnet_api_client/utils.py +0 -0
- {cloudnet_api_client-0.4.1 → cloudnet_api_client-0.5.1}/pyproject.toml +0 -0
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 0.5.1 – 2025-04-04
|
|
9
|
+
|
|
10
|
+
- Raise if downloading failed after retries
|
|
11
|
+
- Adjust logging
|
|
12
|
+
|
|
13
|
+
## 0.5.0 – 2025-04-04
|
|
14
|
+
|
|
15
|
+
- Add option to query one site
|
|
16
|
+
- Add undocumented `raw_model_metadata` function
|
|
17
|
+
|
|
8
18
|
## 0.4.1 – 2025-04-03
|
|
9
19
|
|
|
10
20
|
- Fix types
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudnet-api-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Cloudnet API client
|
|
5
5
|
Author-email: Simo Tukiainen <simo.tukiainen@fmi.fi>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -41,9 +41,9 @@ python3 -m pip install cloudnet-api-client
|
|
|
41
41
|
## Quickstart
|
|
42
42
|
|
|
43
43
|
```python
|
|
44
|
-
|
|
44
|
+
from cloudnet_api_client import APIClient
|
|
45
45
|
|
|
46
|
-
client =
|
|
46
|
+
client = APIClient()
|
|
47
47
|
|
|
48
48
|
sites = client.sites(type="cloudnet")
|
|
49
49
|
products = client.products()
|
|
@@ -127,9 +127,10 @@ Fetch cloudnet sites.
|
|
|
127
127
|
|
|
128
128
|
Parameters:
|
|
129
129
|
|
|
130
|
-
| name
|
|
131
|
-
|
|
|
132
|
-
|
|
|
130
|
+
| name | type | Choices | default |
|
|
131
|
+
| ------- | -------------------- | ----------------------------------------- | ------- |
|
|
132
|
+
| site_id | `str` | | `None` |
|
|
133
|
+
| type | `str` or `list[str]` | "cloudnet", "campaign", "model", "hidden" | `None` |
|
|
133
134
|
|
|
134
135
|
### `APIClient().products()` → `list[Product]`
|
|
135
136
|
|
|
@@ -14,9 +14,9 @@ python3 -m pip install cloudnet-api-client
|
|
|
14
14
|
## Quickstart
|
|
15
15
|
|
|
16
16
|
```python
|
|
17
|
-
|
|
17
|
+
from cloudnet_api_client import APIClient
|
|
18
18
|
|
|
19
|
-
client =
|
|
19
|
+
client = APIClient()
|
|
20
20
|
|
|
21
21
|
sites = client.sites(type="cloudnet")
|
|
22
22
|
products = client.products()
|
|
@@ -100,9 +100,10 @@ Fetch cloudnet sites.
|
|
|
100
100
|
|
|
101
101
|
Parameters:
|
|
102
102
|
|
|
103
|
-
| name
|
|
104
|
-
|
|
|
105
|
-
|
|
|
103
|
+
| name | type | Choices | default |
|
|
104
|
+
| ------- | -------------------- | ----------------------------------------- | ------- |
|
|
105
|
+
| site_id | `str` | | `None` |
|
|
106
|
+
| type | `str` or `list[str]` | "cloudnet", "campaign", "model", "hidden" | `None` |
|
|
106
107
|
|
|
107
108
|
### `APIClient().products()` → `list[Product]`
|
|
108
109
|
|
|
@@ -19,16 +19,18 @@ from cloudnet_api_client.containers import (
|
|
|
19
19
|
SITE_TYPE,
|
|
20
20
|
STATUS,
|
|
21
21
|
Instrument,
|
|
22
|
+
Model,
|
|
22
23
|
Product,
|
|
23
24
|
ProductMetadata,
|
|
24
25
|
RawMetadata,
|
|
26
|
+
RawModelMetadata,
|
|
25
27
|
Site,
|
|
26
28
|
)
|
|
27
29
|
from cloudnet_api_client.dl import download_files
|
|
28
30
|
|
|
29
31
|
T = TypeVar("T")
|
|
30
|
-
MetadataList = list[ProductMetadata] | list[RawMetadata]
|
|
31
|
-
TMetadata = TypeVar("TMetadata", ProductMetadata, RawMetadata)
|
|
32
|
+
MetadataList = list[ProductMetadata] | list[RawMetadata] | list[RawModelMetadata]
|
|
33
|
+
TMetadata = TypeVar("TMetadata", ProductMetadata, RawMetadata, RawModelMetadata)
|
|
32
34
|
DateParam = str | datetime.date | None
|
|
33
35
|
DateTimeParam = str | datetime.datetime | datetime.date | None
|
|
34
36
|
QueryParam = str | list[str] | None
|
|
@@ -43,9 +45,15 @@ class APIClient:
|
|
|
43
45
|
self.base_url = base_url
|
|
44
46
|
self.session = session or _make_session()
|
|
45
47
|
|
|
46
|
-
def sites(
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
def sites(
|
|
49
|
+
self,
|
|
50
|
+
site_id: str | None = None,
|
|
51
|
+
type: SITE_TYPE | list[SITE_TYPE] | None = None,
|
|
52
|
+
) -> list[Site]:
|
|
53
|
+
if site_id:
|
|
54
|
+
res = self._get_response(f"sites/{site_id}")
|
|
55
|
+
else:
|
|
56
|
+
res = self._get_response("sites", {"type": type})
|
|
49
57
|
return _build_objects(res, Site)
|
|
50
58
|
|
|
51
59
|
def products(
|
|
@@ -148,6 +156,34 @@ class APIClient:
|
|
|
148
156
|
res = self._get_response("raw-files", params)
|
|
149
157
|
return _build_raw_meta_objects(res)
|
|
150
158
|
|
|
159
|
+
def raw_model_metadata(
|
|
160
|
+
self,
|
|
161
|
+
site_id: str,
|
|
162
|
+
model_id: QueryParam = None,
|
|
163
|
+
date: DateParam = None,
|
|
164
|
+
date_from: DateParam = None,
|
|
165
|
+
date_to: DateParam = None,
|
|
166
|
+
updated_at: DateTimeParam = None,
|
|
167
|
+
updated_at_from: DateTimeParam = None,
|
|
168
|
+
updated_at_to: DateTimeParam = None,
|
|
169
|
+
filename_prefix: QueryParam = None,
|
|
170
|
+
filename_suffix: QueryParam = None,
|
|
171
|
+
status: STATUS | list[STATUS] | None = None,
|
|
172
|
+
) -> list[RawModelMetadata]:
|
|
173
|
+
"""For internal CLU use only. Will change in the future."""
|
|
174
|
+
params = {
|
|
175
|
+
"site": site_id,
|
|
176
|
+
"filenamePrefix": filename_prefix,
|
|
177
|
+
"filenameSuffix": filename_suffix,
|
|
178
|
+
"status": status,
|
|
179
|
+
"model": model_id,
|
|
180
|
+
}
|
|
181
|
+
_add_date_params(
|
|
182
|
+
params, date, date_from, date_to, updated_at, updated_at_from, updated_at_to
|
|
183
|
+
)
|
|
184
|
+
res = self._get_response("raw-model-files", params)
|
|
185
|
+
return _build_raw_model_meta_objects(res)
|
|
186
|
+
|
|
151
187
|
def download(
|
|
152
188
|
self,
|
|
153
189
|
metadata: MetadataList,
|
|
@@ -215,7 +251,10 @@ class APIClient:
|
|
|
215
251
|
url = urljoin(self.base_url, endpoint)
|
|
216
252
|
res = self.session.get(url, params=params, timeout=120)
|
|
217
253
|
res.raise_for_status()
|
|
218
|
-
|
|
254
|
+
data = res.json()
|
|
255
|
+
if isinstance(data, dict):
|
|
256
|
+
data = [data]
|
|
257
|
+
return data
|
|
219
258
|
|
|
220
259
|
|
|
221
260
|
def _add_date_params(
|
|
@@ -382,6 +421,33 @@ def _build_raw_meta_objects(res: list[dict]) -> list[RawMetadata]:
|
|
|
382
421
|
]
|
|
383
422
|
|
|
384
423
|
|
|
424
|
+
def _build_raw_model_meta_objects(res: list[dict]) -> list[RawModelMetadata]:
|
|
425
|
+
field_names = {f.name for f in fields(RawModelMetadata)} - CONVERTED - {"model"}
|
|
426
|
+
return [
|
|
427
|
+
RawModelMetadata(
|
|
428
|
+
**{_to_snake(k): v for k, v in obj.items() if _to_snake(k) in field_names},
|
|
429
|
+
model=Model(
|
|
430
|
+
model_id=obj["model"]["id"],
|
|
431
|
+
name=obj["model"]["humanReadableName"],
|
|
432
|
+
optimum_order=int(obj["model"]["optimumOrder"]),
|
|
433
|
+
source_model_id=obj["model"]["sourceModelId"],
|
|
434
|
+
forecast_start=int(obj["model"]["forecastStart"])
|
|
435
|
+
if obj["model"]["forecastStart"] is not None
|
|
436
|
+
else None,
|
|
437
|
+
forecast_end=int(obj["model"]["forecastEnd"])
|
|
438
|
+
if obj["model"]["forecastEnd"] is not None
|
|
439
|
+
else None,
|
|
440
|
+
),
|
|
441
|
+
measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
|
|
442
|
+
created_at=datetime.datetime.fromisoformat(obj["createdAt"]),
|
|
443
|
+
updated_at=datetime.datetime.fromisoformat(obj["updatedAt"]),
|
|
444
|
+
size=int(obj["size"]),
|
|
445
|
+
uuid=uuid.UUID(obj["uuid"]),
|
|
446
|
+
)
|
|
447
|
+
for obj in res
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
|
|
385
451
|
def _to_snake(name: str) -> str:
|
|
386
452
|
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
|
|
387
453
|
|
|
@@ -22,7 +22,6 @@ class Site:
|
|
|
22
22
|
country_code: str
|
|
23
23
|
country_subdivision_code: str | None
|
|
24
24
|
type: list[SITE_TYPE]
|
|
25
|
-
status: Literal["active", "inactive"]
|
|
26
25
|
gaw: str | None
|
|
27
26
|
|
|
28
27
|
|
|
@@ -46,6 +45,16 @@ class Instrument:
|
|
|
46
45
|
serial_number: str | None
|
|
47
46
|
|
|
48
47
|
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class Model:
|
|
50
|
+
model_id: str
|
|
51
|
+
name: str
|
|
52
|
+
optimum_order: int
|
|
53
|
+
source_model_id: str
|
|
54
|
+
forecast_start: int | None
|
|
55
|
+
forecast_end: int | None
|
|
56
|
+
|
|
57
|
+
|
|
49
58
|
@dataclass(frozen=True, slots=True)
|
|
50
59
|
class Metadata:
|
|
51
60
|
uuid: uuid.UUID
|
|
@@ -65,6 +74,12 @@ class RawMetadata(Metadata):
|
|
|
65
74
|
tags: list[str] | None
|
|
66
75
|
|
|
67
76
|
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class RawModelMetadata(Metadata):
|
|
79
|
+
status: STATUS
|
|
80
|
+
model: Model
|
|
81
|
+
|
|
82
|
+
|
|
68
83
|
@dataclass(frozen=True, slots=True)
|
|
69
84
|
class ProductMetadata(Metadata):
|
|
70
85
|
product: Product
|
|
@@ -7,12 +7,16 @@ from tqdm import tqdm
|
|
|
7
7
|
from tqdm.asyncio import tqdm_asyncio
|
|
8
8
|
|
|
9
9
|
from cloudnet_api_client import utils
|
|
10
|
-
from cloudnet_api_client.containers import
|
|
10
|
+
from cloudnet_api_client.containers import (
|
|
11
|
+
ProductMetadata,
|
|
12
|
+
RawMetadata,
|
|
13
|
+
RawModelMetadata,
|
|
14
|
+
)
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
async def download_files(
|
|
14
18
|
base_url: str,
|
|
15
|
-
metadata: list[ProductMetadata] | list[RawMetadata],
|
|
19
|
+
metadata: list[ProductMetadata] | list[RawMetadata] | list[RawModelMetadata],
|
|
16
20
|
output_path: Path,
|
|
17
21
|
concurrency_limit: int,
|
|
18
22
|
disable_progress: bool | None,
|
|
@@ -26,7 +30,7 @@ async def download_files(
|
|
|
26
30
|
destination = output_path / meta.download_url.split("/")[-1]
|
|
27
31
|
full_paths.append(destination)
|
|
28
32
|
if destination.exists() and _file_checksum_matches(meta, destination):
|
|
29
|
-
logging.
|
|
33
|
+
logging.debug(f"Already downloaded: {destination}")
|
|
30
34
|
continue
|
|
31
35
|
task = asyncio.create_task(
|
|
32
36
|
_download_file_with_retries(
|
|
@@ -57,6 +61,7 @@ async def _download_file_with_retries(
|
|
|
57
61
|
logging.warning(f"Attempt {attempt} failed for {url}: {e}")
|
|
58
62
|
if attempt == max_retries:
|
|
59
63
|
logging.error(f"Giving up on {url} after {max_retries} attempts.")
|
|
64
|
+
raise e
|
|
60
65
|
else:
|
|
61
66
|
# Exponential backoff before retrying
|
|
62
67
|
await asyncio.sleep(2**attempt)
|
|
@@ -89,11 +94,11 @@ async def _download_file(
|
|
|
89
94
|
break
|
|
90
95
|
file_out.write(chunk)
|
|
91
96
|
bar.update(len(chunk))
|
|
92
|
-
logging.
|
|
97
|
+
logging.debug(f"Downloaded: {destination}")
|
|
93
98
|
|
|
94
99
|
|
|
95
100
|
def _file_checksum_matches(
|
|
96
|
-
meta: ProductMetadata | RawMetadata, destination: Path
|
|
101
|
+
meta: ProductMetadata | RawMetadata | RawModelMetadata, destination: Path
|
|
97
102
|
) -> bool:
|
|
98
|
-
fun = utils.
|
|
103
|
+
fun = utils.sha256sum if isinstance(meta, ProductMetadata) else utils.md5sum
|
|
99
104
|
return fun(destination) == meta.checksum
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.1"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|