cloudnet-api-client 0.6.0__tar.gz → 0.8.0__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.6.0 → cloudnet_api_client-0.8.0}/CHANGELOG.md +9 -0
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/PKG-INFO +3 -2
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/README.md +2 -1
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/cloudnet_api_client/client.py +84 -51
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/cloudnet_api_client/containers.py +1 -0
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/cloudnet_api_client/dl.py +13 -2
- cloudnet_api_client-0.8.0/cloudnet_api_client/version.py +1 -0
- cloudnet_api_client-0.6.0/cloudnet_api_client/version.py +0 -1
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/.github/workflows/publish.yml +0 -0
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/.github/workflows/test.yml +0 -0
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/.gitignore +0 -0
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/.pre-commit-config.yaml +0 -0
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/LICENSE +0 -0
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/cloudnet_api_client/__init__.py +0 -0
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/cloudnet_api_client/py.typed +0 -0
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/cloudnet_api_client/utils.py +0 -0
- {cloudnet_api_client-0.6.0 → cloudnet_api_client-0.8.0}/pyproject.toml +0 -0
|
@@ -5,6 +5,15 @@ 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.8.0 – 2025-05-14
|
|
9
|
+
|
|
10
|
+
- Add site to metadata responses
|
|
11
|
+
|
|
12
|
+
## 0.7.0 – 2025-05-06
|
|
13
|
+
|
|
14
|
+
- Validate checksum optionally
|
|
15
|
+
- Make site optional
|
|
16
|
+
|
|
8
17
|
## 0.6.0 – 2025-04-22
|
|
9
18
|
|
|
10
19
|
- Fix datetime parsing for Python 3.10
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudnet-api-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Cloudnet API client
|
|
5
5
|
Author-email: Simo Tukiainen <simo.tukiainen@fmi.fi>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -65,7 +65,7 @@ Parameters:
|
|
|
65
65
|
|
|
66
66
|
| name | type | default | example |
|
|
67
67
|
| ------------------- | --------------------------- | ------- | ---------------------------------------------------- |
|
|
68
|
-
| site_id | `str`
|
|
68
|
+
| site_id | `str` or `list[str]` | `None` | "hyytiala" |
|
|
69
69
|
| date | `str` or `date` | `None` | "2024-01-01" |
|
|
70
70
|
| date_from | `str` or `date` | `None` | "2025-01-01" |
|
|
71
71
|
| date_to | `str` or `date` | `None` | "2025-01-01" |
|
|
@@ -158,6 +158,7 @@ Parameters:
|
|
|
158
158
|
| output_directory | `PathLike` or `str` | |
|
|
159
159
|
| concurrency_limit | `int` | 5 |
|
|
160
160
|
| progress | `bool` or `None` | `None` |
|
|
161
|
+
| validate_checksum | `bool` | `False` |
|
|
161
162
|
|
|
162
163
|
There's also an asynchronous version of this function:
|
|
163
164
|
`cloudnet_api_client.adownload`. It's useful for usage inside Jupyter notebook.
|
|
@@ -38,7 +38,7 @@ Parameters:
|
|
|
38
38
|
|
|
39
39
|
| name | type | default | example |
|
|
40
40
|
| ------------------- | --------------------------- | ------- | ---------------------------------------------------- |
|
|
41
|
-
| site_id | `str`
|
|
41
|
+
| site_id | `str` or `list[str]` | `None` | "hyytiala" |
|
|
42
42
|
| date | `str` or `date` | `None` | "2024-01-01" |
|
|
43
43
|
| date_from | `str` or `date` | `None` | "2025-01-01" |
|
|
44
44
|
| date_to | `str` or `date` | `None` | "2025-01-01" |
|
|
@@ -131,6 +131,7 @@ Parameters:
|
|
|
131
131
|
| output_directory | `PathLike` or `str` | |
|
|
132
132
|
| concurrency_limit | `int` | 5 |
|
|
133
133
|
| progress | `bool` or `None` | `None` |
|
|
134
|
+
| validate_checksum | `bool` | `False` |
|
|
134
135
|
|
|
135
136
|
There's also an asynchronous version of this function:
|
|
136
137
|
`cloudnet_api_client.adownload`. It's useful for usage inside Jupyter notebook.
|
|
@@ -85,7 +85,7 @@ class APIClient:
|
|
|
85
85
|
|
|
86
86
|
def metadata(
|
|
87
87
|
self,
|
|
88
|
-
site_id:
|
|
88
|
+
site_id: QueryParam = None,
|
|
89
89
|
date: DateParam = None,
|
|
90
90
|
date_from: DateParam = None,
|
|
91
91
|
date_to: DateParam = None,
|
|
@@ -108,6 +108,9 @@ class APIClient:
|
|
|
108
108
|
_add_date_params(
|
|
109
109
|
params, date, date_from, date_to, updated_at, updated_at_from, updated_at_to
|
|
110
110
|
)
|
|
111
|
+
|
|
112
|
+
_check_params(params, ("showLegacy",))
|
|
113
|
+
|
|
111
114
|
no_instrument = not instrument_id or instrument_pid
|
|
112
115
|
if no_instrument and (not product and model_id):
|
|
113
116
|
files_res = []
|
|
@@ -129,7 +132,7 @@ class APIClient:
|
|
|
129
132
|
|
|
130
133
|
def raw_metadata(
|
|
131
134
|
self,
|
|
132
|
-
site_id:
|
|
135
|
+
site_id: QueryParam = None,
|
|
133
136
|
date: DateParam = None,
|
|
134
137
|
date_from: DateParam = None,
|
|
135
138
|
date_to: DateParam = None,
|
|
@@ -158,7 +161,7 @@ class APIClient:
|
|
|
158
161
|
|
|
159
162
|
def raw_model_metadata(
|
|
160
163
|
self,
|
|
161
|
-
site_id:
|
|
164
|
+
site_id: QueryParam = None,
|
|
162
165
|
model_id: QueryParam = None,
|
|
163
166
|
date: DateParam = None,
|
|
164
167
|
date_from: DateParam = None,
|
|
@@ -181,6 +184,9 @@ class APIClient:
|
|
|
181
184
|
_add_date_params(
|
|
182
185
|
params, date, date_from, date_to, updated_at, updated_at_from, updated_at_to
|
|
183
186
|
)
|
|
187
|
+
|
|
188
|
+
_check_params(params)
|
|
189
|
+
|
|
184
190
|
res = self._get_response("raw-model-files", params)
|
|
185
191
|
return _build_raw_model_meta_objects(res)
|
|
186
192
|
|
|
@@ -190,9 +196,16 @@ class APIClient:
|
|
|
190
196
|
output_directory: str | PathLike,
|
|
191
197
|
concurrency_limit: int = 5,
|
|
192
198
|
progress: bool | None = None,
|
|
199
|
+
validate_checksum: bool = False,
|
|
193
200
|
) -> list[Path]:
|
|
194
201
|
return asyncio.run(
|
|
195
|
-
self.adownload(
|
|
202
|
+
self.adownload(
|
|
203
|
+
metadata,
|
|
204
|
+
output_directory,
|
|
205
|
+
concurrency_limit,
|
|
206
|
+
progress,
|
|
207
|
+
validate_checksum,
|
|
208
|
+
)
|
|
196
209
|
)
|
|
197
210
|
|
|
198
211
|
async def adownload(
|
|
@@ -201,6 +214,7 @@ class APIClient:
|
|
|
201
214
|
output_directory: str | PathLike,
|
|
202
215
|
concurrency_limit: int = 5,
|
|
203
216
|
progress: bool | None = None,
|
|
217
|
+
validate_checksum: bool = False,
|
|
204
218
|
) -> list[Path]:
|
|
205
219
|
disable_progress = not progress if progress is not None else None
|
|
206
220
|
output_directory = Path(output_directory).resolve()
|
|
@@ -211,6 +225,7 @@ class APIClient:
|
|
|
211
225
|
output_directory,
|
|
212
226
|
concurrency_limit,
|
|
213
227
|
disable_progress,
|
|
228
|
+
validate_checksum,
|
|
214
229
|
)
|
|
215
230
|
|
|
216
231
|
@staticmethod
|
|
@@ -381,7 +396,7 @@ def _build_meta_objects(res: list[dict]) -> list[ProductMetadata]:
|
|
|
381
396
|
field_names = (
|
|
382
397
|
{f.name for f in fields(ProductMetadata)}
|
|
383
398
|
- CONVERTED
|
|
384
|
-
- {"product", "instrument", "model"}
|
|
399
|
+
- {"product", "instrument", "model", "site"}
|
|
385
400
|
)
|
|
386
401
|
return [
|
|
387
402
|
ProductMetadata(
|
|
@@ -392,30 +407,10 @@ def _build_meta_objects(res: list[dict]) -> list[ProductMetadata]:
|
|
|
392
407
|
type=obj["product"]["type"],
|
|
393
408
|
experimental=obj["product"]["experimental"],
|
|
394
409
|
),
|
|
395
|
-
instrument=
|
|
396
|
-
instrument_id=obj["instrument"]["instrumentId"],
|
|
397
|
-
model=obj["instrument"]["model"],
|
|
398
|
-
type=obj["instrument"]["type"],
|
|
399
|
-
uuid=uuid.UUID(obj["instrument"]["uuid"]),
|
|
400
|
-
pid=obj["instrument"]["pid"],
|
|
401
|
-
owners=obj["instrument"]["owners"],
|
|
402
|
-
serial_number=obj["instrument"]["serialNumber"],
|
|
403
|
-
name=obj["instrument"]["name"],
|
|
404
|
-
)
|
|
410
|
+
instrument=_create_instrument_object(obj["instrument"])
|
|
405
411
|
if "instrument" in obj and obj["instrument"] is not None
|
|
406
412
|
else None,
|
|
407
|
-
model=
|
|
408
|
-
model_id=obj["model"]["id"],
|
|
409
|
-
name=obj["model"]["humanReadableName"],
|
|
410
|
-
optimum_order=obj["model"]["optimumOrder"],
|
|
411
|
-
source_model_id=obj["model"]["sourceModelId"],
|
|
412
|
-
forecast_start=obj["model"]["forecastStart"]
|
|
413
|
-
if obj["model"]["forecastStart"] is not None
|
|
414
|
-
else None,
|
|
415
|
-
forecast_end=obj["model"]["forecastEnd"]
|
|
416
|
-
if obj["model"]["forecastEnd"] is not None
|
|
417
|
-
else None,
|
|
418
|
-
)
|
|
413
|
+
model=_create_model_object(obj["model"])
|
|
419
414
|
if "model" in obj and obj["model"] is not None
|
|
420
415
|
else None,
|
|
421
416
|
measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
|
|
@@ -423,63 +418,96 @@ def _build_meta_objects(res: list[dict]) -> list[ProductMetadata]:
|
|
|
423
418
|
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
424
419
|
size=int(obj["size"]),
|
|
425
420
|
uuid=uuid.UUID(obj["uuid"]),
|
|
421
|
+
site=_create_site_object(obj["site"]),
|
|
426
422
|
)
|
|
427
423
|
for obj in res
|
|
428
424
|
]
|
|
429
425
|
|
|
430
426
|
|
|
431
427
|
def _build_raw_meta_objects(res: list[dict]) -> list[RawMetadata]:
|
|
432
|
-
field_names =
|
|
428
|
+
field_names = (
|
|
429
|
+
{f.name for f in fields(RawMetadata)} - CONVERTED - {"instrument", "site"}
|
|
430
|
+
)
|
|
433
431
|
return [
|
|
434
432
|
RawMetadata(
|
|
435
433
|
**{_to_snake(k): v for k, v in obj.items() if _to_snake(k) in field_names},
|
|
436
|
-
instrument=
|
|
437
|
-
instrument_id=obj["instrumentInfo"]["instrumentId"],
|
|
438
|
-
model=obj["instrumentInfo"]["model"],
|
|
439
|
-
type=obj["instrumentInfo"]["type"],
|
|
440
|
-
uuid=uuid.UUID(obj["instrumentInfo"]["uuid"]),
|
|
441
|
-
pid=obj["instrumentInfo"]["pid"],
|
|
442
|
-
owners=obj["instrumentInfo"]["owners"],
|
|
443
|
-
serial_number=obj["instrumentInfo"]["serialNumber"],
|
|
444
|
-
name=obj["instrumentInfo"]["name"],
|
|
445
|
-
),
|
|
434
|
+
instrument=_create_instrument_object(obj["instrumentInfo"]),
|
|
446
435
|
measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
|
|
447
436
|
created_at=_parse_datetime(obj["createdAt"]),
|
|
448
437
|
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
449
438
|
size=int(obj["size"]),
|
|
450
439
|
uuid=uuid.UUID(obj["uuid"]),
|
|
440
|
+
site=_create_site_object(obj["site"]),
|
|
451
441
|
)
|
|
452
442
|
for obj in res
|
|
453
443
|
]
|
|
454
444
|
|
|
455
445
|
|
|
456
446
|
def _build_raw_model_meta_objects(res: list[dict]) -> list[RawModelMetadata]:
|
|
457
|
-
field_names =
|
|
447
|
+
field_names = (
|
|
448
|
+
{f.name for f in fields(RawModelMetadata)} - CONVERTED - {"model", "site"}
|
|
449
|
+
)
|
|
458
450
|
return [
|
|
459
451
|
RawModelMetadata(
|
|
460
452
|
**{_to_snake(k): v for k, v in obj.items() if _to_snake(k) in field_names},
|
|
461
|
-
model=
|
|
462
|
-
model_id=obj["model"]["id"],
|
|
463
|
-
name=obj["model"]["humanReadableName"],
|
|
464
|
-
optimum_order=int(obj["model"]["optimumOrder"]),
|
|
465
|
-
source_model_id=obj["model"]["sourceModelId"],
|
|
466
|
-
forecast_start=int(obj["model"]["forecastStart"])
|
|
467
|
-
if obj["model"]["forecastStart"] is not None
|
|
468
|
-
else None,
|
|
469
|
-
forecast_end=int(obj["model"]["forecastEnd"])
|
|
470
|
-
if obj["model"]["forecastEnd"] is not None
|
|
471
|
-
else None,
|
|
472
|
-
),
|
|
453
|
+
model=_create_model_object(obj["model"]),
|
|
473
454
|
measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
|
|
474
455
|
created_at=_parse_datetime(obj["createdAt"]),
|
|
475
456
|
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
476
457
|
size=int(obj["size"]),
|
|
477
458
|
uuid=uuid.UUID(obj["uuid"]),
|
|
459
|
+
site=_create_site_object(obj["site"]),
|
|
478
460
|
)
|
|
479
461
|
for obj in res
|
|
480
462
|
]
|
|
481
463
|
|
|
482
464
|
|
|
465
|
+
def _create_model_object(metadata: dict) -> Model:
|
|
466
|
+
return Model(
|
|
467
|
+
model_id=metadata["id"],
|
|
468
|
+
name=metadata["humanReadableName"],
|
|
469
|
+
optimum_order=int(metadata["optimumOrder"]),
|
|
470
|
+
source_model_id=metadata["sourceModelId"],
|
|
471
|
+
forecast_start=int(metadata["forecastStart"])
|
|
472
|
+
if metadata["forecastStart"] is not None
|
|
473
|
+
else None,
|
|
474
|
+
forecast_end=int(metadata["forecastEnd"])
|
|
475
|
+
if metadata["forecastEnd"] is not None
|
|
476
|
+
else None,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _create_site_object(metadata: dict) -> Site:
|
|
481
|
+
return Site(
|
|
482
|
+
id=metadata["id"],
|
|
483
|
+
human_readable_name=metadata["humanReadableName"],
|
|
484
|
+
station_name=metadata["stationName"],
|
|
485
|
+
latitude=float(metadata["latitude"]),
|
|
486
|
+
longitude=float(metadata["longitude"]),
|
|
487
|
+
altitude=float(metadata["altitude"]),
|
|
488
|
+
dvas_id=metadata["dvasId"],
|
|
489
|
+
actris_id=metadata["actrisId"],
|
|
490
|
+
country=metadata["country"],
|
|
491
|
+
country_code=metadata["countryCode"],
|
|
492
|
+
country_subdivision_code=metadata["countrySubdivisionCode"],
|
|
493
|
+
type=metadata["type"],
|
|
494
|
+
gaw=metadata["gaw"],
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _create_instrument_object(metadata: dict) -> Instrument:
|
|
499
|
+
return Instrument(
|
|
500
|
+
instrument_id=metadata["instrumentId"],
|
|
501
|
+
model=metadata["model"],
|
|
502
|
+
type=metadata["type"],
|
|
503
|
+
uuid=uuid.UUID(metadata["uuid"]),
|
|
504
|
+
pid=metadata["pid"],
|
|
505
|
+
owners=metadata["owners"],
|
|
506
|
+
serial_number=metadata["serialNumber"],
|
|
507
|
+
name=metadata["name"],
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
483
511
|
def _to_snake(name: str) -> str:
|
|
484
512
|
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
|
|
485
513
|
|
|
@@ -495,3 +523,8 @@ def _make_session() -> requests.Session:
|
|
|
495
523
|
|
|
496
524
|
def _parse_datetime(dt: str) -> datetime.datetime:
|
|
497
525
|
return datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _check_params(params: dict, ignore: tuple = ()) -> None:
|
|
529
|
+
if sum(1 for key, value in params.items() if key not in ignore and value) == 0:
|
|
530
|
+
raise TypeError("At least one of the parameters must be set.")
|
|
@@ -20,7 +20,9 @@ async def download_files(
|
|
|
20
20
|
output_path: Path,
|
|
21
21
|
concurrency_limit: int,
|
|
22
22
|
disable_progress: bool | None,
|
|
23
|
+
validate_checksum: bool = False,
|
|
23
24
|
) -> list[Path]:
|
|
25
|
+
file_exists = _checksum_matches if validate_checksum else _size_and_name_matches
|
|
24
26
|
semaphore = asyncio.Semaphore(concurrency_limit)
|
|
25
27
|
full_paths = []
|
|
26
28
|
async with aiohttp.ClientSession() as session:
|
|
@@ -29,7 +31,7 @@ async def download_files(
|
|
|
29
31
|
download_url = f"{base_url}{meta.download_url.split('/api/')[-1]}"
|
|
30
32
|
destination = output_path / meta.download_url.split("/")[-1]
|
|
31
33
|
full_paths.append(destination)
|
|
32
|
-
if destination.exists() and
|
|
34
|
+
if destination.exists() and file_exists(meta, destination):
|
|
33
35
|
logging.debug(f"Already downloaded: {destination}")
|
|
34
36
|
continue
|
|
35
37
|
task = asyncio.create_task(
|
|
@@ -97,8 +99,17 @@ async def _download_file(
|
|
|
97
99
|
logging.debug(f"Downloaded: {destination}")
|
|
98
100
|
|
|
99
101
|
|
|
100
|
-
def
|
|
102
|
+
def _checksum_matches(
|
|
101
103
|
meta: ProductMetadata | RawMetadata | RawModelMetadata, destination: Path
|
|
102
104
|
) -> bool:
|
|
103
105
|
fun = utils.sha256sum if isinstance(meta, ProductMetadata) else utils.md5sum
|
|
104
106
|
return fun(destination) == meta.checksum
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _size_and_name_matches(
|
|
110
|
+
meta: ProductMetadata | RawMetadata | RawModelMetadata, destination: Path
|
|
111
|
+
) -> bool:
|
|
112
|
+
return (
|
|
113
|
+
destination.stat().st_size == meta.size
|
|
114
|
+
and destination.name == meta.download_url.split("/")[-1]
|
|
115
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.6.0"
|
|
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
|