cloudnet-api-client 0.5.1__tar.gz → 0.7.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.5.1 → cloudnet_api_client-0.7.0}/CHANGELOG.md +10 -0
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/PKG-INFO +3 -2
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/README.md +2 -1
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/cloudnet_api_client/client.py +75 -19
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/cloudnet_api_client/containers.py +2 -0
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/cloudnet_api_client/dl.py +13 -2
- cloudnet_api_client-0.7.0/cloudnet_api_client/version.py +1 -0
- cloudnet_api_client-0.5.1/cloudnet_api_client/version.py +0 -1
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/.github/workflows/publish.yml +0 -0
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/.github/workflows/test.yml +0 -0
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/.gitignore +0 -0
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/.pre-commit-config.yaml +0 -0
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/LICENSE +0 -0
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/cloudnet_api_client/__init__.py +0 -0
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/cloudnet_api_client/py.typed +0 -0
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/cloudnet_api_client/utils.py +0 -0
- {cloudnet_api_client-0.5.1 → cloudnet_api_client-0.7.0}/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.7.0 – 2025-05-06
|
|
9
|
+
|
|
10
|
+
- Validate checksum optionally
|
|
11
|
+
- Make site optional
|
|
12
|
+
|
|
13
|
+
## 0.6.0 – 2025-04-22
|
|
14
|
+
|
|
15
|
+
- Fix datetime parsing for Python 3.10
|
|
16
|
+
- Add Instrument and Model objects to metadata
|
|
17
|
+
|
|
8
18
|
## 0.5.1 – 2025-04-04
|
|
9
19
|
|
|
10
20
|
- Raise if downloading failed after retries
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudnet-api-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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,
|
|
@@ -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
|
|
@@ -270,13 +285,13 @@ def _add_date_params(
|
|
|
270
285
|
msg = "Cannot use 'date' with 'date_from' and 'date_to'"
|
|
271
286
|
raise ValueError(msg)
|
|
272
287
|
if date is not None:
|
|
273
|
-
start, stop =
|
|
288
|
+
start, stop = _parse_date_param(date)
|
|
274
289
|
params["dateFrom"] = start.isoformat()
|
|
275
290
|
params["dateTo"] = stop.isoformat()
|
|
276
291
|
if date_from is not None:
|
|
277
|
-
params["dateFrom"] =
|
|
292
|
+
params["dateFrom"] = _parse_date_param(date_from)[0].isoformat()
|
|
278
293
|
if date_to is not None:
|
|
279
|
-
params["dateTo"] =
|
|
294
|
+
params["dateTo"] = _parse_date_param(date_to)[1].isoformat()
|
|
280
295
|
|
|
281
296
|
if updated_at is not None and (
|
|
282
297
|
updated_at_from is not None or updated_at_to is not None
|
|
@@ -284,16 +299,16 @@ def _add_date_params(
|
|
|
284
299
|
msg = "Cannot use 'updated_at' with 'updated_at_from' and 'updated_at_to'"
|
|
285
300
|
raise ValueError(msg)
|
|
286
301
|
if updated_at is not None:
|
|
287
|
-
start, stop =
|
|
302
|
+
start, stop = _parse_datetime_param(updated_at)
|
|
288
303
|
params["updatedAtFrom"] = start.isoformat()
|
|
289
304
|
params["updatedAtTo"] = stop.isoformat()
|
|
290
305
|
if updated_at_from is not None:
|
|
291
|
-
params["updatedAtFrom"] =
|
|
306
|
+
params["updatedAtFrom"] = _parse_datetime_param(updated_at_from)[0].isoformat()
|
|
292
307
|
if updated_at_to is not None:
|
|
293
|
-
params["updatedAtTo"] =
|
|
308
|
+
params["updatedAtTo"] = _parse_datetime_param(updated_at_to)[1].isoformat()
|
|
294
309
|
|
|
295
310
|
|
|
296
|
-
def
|
|
311
|
+
def _parse_date_param(date: DateParam) -> tuple[datetime.date, datetime.date]:
|
|
297
312
|
if isinstance(date, datetime.date):
|
|
298
313
|
return date, date
|
|
299
314
|
error = ValueError(f"Invalid date format: {date}")
|
|
@@ -316,7 +331,9 @@ def _parse_date(date: DateParam) -> tuple[datetime.date, datetime.date]:
|
|
|
316
331
|
raise error
|
|
317
332
|
|
|
318
333
|
|
|
319
|
-
def
|
|
334
|
+
def _parse_datetime_param(
|
|
335
|
+
dt: DateTimeParam,
|
|
336
|
+
) -> tuple[datetime.datetime, datetime.datetime]:
|
|
320
337
|
if isinstance(dt, datetime.datetime):
|
|
321
338
|
return dt, dt
|
|
322
339
|
if isinstance(dt, datetime.date):
|
|
@@ -376,19 +393,49 @@ CONVERTED = {"measurement_date", "created_at", "updated_at", "size", "uuid"}
|
|
|
376
393
|
|
|
377
394
|
|
|
378
395
|
def _build_meta_objects(res: list[dict]) -> list[ProductMetadata]:
|
|
379
|
-
field_names =
|
|
396
|
+
field_names = (
|
|
397
|
+
{f.name for f in fields(ProductMetadata)}
|
|
398
|
+
- CONVERTED
|
|
399
|
+
- {"product", "instrument", "model"}
|
|
400
|
+
)
|
|
380
401
|
return [
|
|
381
402
|
ProductMetadata(
|
|
382
403
|
**{_to_snake(k): v for k, v in obj.items() if _to_snake(k) in field_names},
|
|
383
404
|
product=Product(
|
|
384
405
|
id=obj["product"]["id"],
|
|
385
406
|
human_readable_name=obj["product"]["humanReadableName"],
|
|
386
|
-
type=
|
|
407
|
+
type=obj["product"]["type"],
|
|
387
408
|
experimental=obj["product"]["experimental"],
|
|
388
409
|
),
|
|
410
|
+
instrument=Instrument(
|
|
411
|
+
instrument_id=obj["instrument"]["instrumentId"],
|
|
412
|
+
model=obj["instrument"]["model"],
|
|
413
|
+
type=obj["instrument"]["type"],
|
|
414
|
+
uuid=uuid.UUID(obj["instrument"]["uuid"]),
|
|
415
|
+
pid=obj["instrument"]["pid"],
|
|
416
|
+
owners=obj["instrument"]["owners"],
|
|
417
|
+
serial_number=obj["instrument"]["serialNumber"],
|
|
418
|
+
name=obj["instrument"]["name"],
|
|
419
|
+
)
|
|
420
|
+
if "instrument" in obj and obj["instrument"] is not None
|
|
421
|
+
else None,
|
|
422
|
+
model=Model(
|
|
423
|
+
model_id=obj["model"]["id"],
|
|
424
|
+
name=obj["model"]["humanReadableName"],
|
|
425
|
+
optimum_order=obj["model"]["optimumOrder"],
|
|
426
|
+
source_model_id=obj["model"]["sourceModelId"],
|
|
427
|
+
forecast_start=obj["model"]["forecastStart"]
|
|
428
|
+
if obj["model"]["forecastStart"] is not None
|
|
429
|
+
else None,
|
|
430
|
+
forecast_end=obj["model"]["forecastEnd"]
|
|
431
|
+
if obj["model"]["forecastEnd"] is not None
|
|
432
|
+
else None,
|
|
433
|
+
)
|
|
434
|
+
if "model" in obj and obj["model"] is not None
|
|
435
|
+
else None,
|
|
389
436
|
measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
|
|
390
|
-
created_at=
|
|
391
|
-
updated_at=
|
|
437
|
+
created_at=_parse_datetime(obj["createdAt"]),
|
|
438
|
+
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
392
439
|
size=int(obj["size"]),
|
|
393
440
|
uuid=uuid.UUID(obj["uuid"]),
|
|
394
441
|
)
|
|
@@ -412,8 +459,8 @@ def _build_raw_meta_objects(res: list[dict]) -> list[RawMetadata]:
|
|
|
412
459
|
name=obj["instrumentInfo"]["name"],
|
|
413
460
|
),
|
|
414
461
|
measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
|
|
415
|
-
created_at=
|
|
416
|
-
updated_at=
|
|
462
|
+
created_at=_parse_datetime(obj["createdAt"]),
|
|
463
|
+
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
417
464
|
size=int(obj["size"]),
|
|
418
465
|
uuid=uuid.UUID(obj["uuid"]),
|
|
419
466
|
)
|
|
@@ -439,8 +486,8 @@ def _build_raw_model_meta_objects(res: list[dict]) -> list[RawModelMetadata]:
|
|
|
439
486
|
else None,
|
|
440
487
|
),
|
|
441
488
|
measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
|
|
442
|
-
created_at=
|
|
443
|
-
updated_at=
|
|
489
|
+
created_at=_parse_datetime(obj["createdAt"]),
|
|
490
|
+
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
444
491
|
size=int(obj["size"]),
|
|
445
492
|
uuid=uuid.UUID(obj["uuid"]),
|
|
446
493
|
)
|
|
@@ -459,3 +506,12 @@ def _make_session() -> requests.Session:
|
|
|
459
506
|
session.mount("https://", adapter)
|
|
460
507
|
session.mount("http://", adapter)
|
|
461
508
|
return session
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _parse_datetime(dt: str) -> datetime.datetime:
|
|
512
|
+
return datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _check_params(params: dict, ignore: tuple = ()) -> None:
|
|
516
|
+
if sum(1 for key, value in params.items() if key not in ignore and value) == 0:
|
|
517
|
+
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.7.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.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
|