cloudnet-api-client 0.5.0__tar.gz → 0.6.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.0 → cloudnet_api_client-0.6.0}/CHANGELOG.md +10 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/PKG-INFO +1 -1
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/cloudnet_api_client/client.py +52 -16
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/cloudnet_api_client/containers.py +2 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/cloudnet_api_client/dl.py +3 -2
- cloudnet_api_client-0.6.0/cloudnet_api_client/version.py +1 -0
- cloudnet_api_client-0.5.0/cloudnet_api_client/version.py +0 -1
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/.github/workflows/publish.yml +0 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/.github/workflows/test.yml +0 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/.gitignore +0 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/.pre-commit-config.yaml +0 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/LICENSE +0 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/README.md +0 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/cloudnet_api_client/__init__.py +0 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/cloudnet_api_client/py.typed +0 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.0}/cloudnet_api_client/utils.py +0 -0
- {cloudnet_api_client-0.5.0 → cloudnet_api_client-0.6.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.6.0 – 2025-04-22
|
|
9
|
+
|
|
10
|
+
- Fix datetime parsing for Python 3.10
|
|
11
|
+
- Add Instrument and Model objects to metadata
|
|
12
|
+
|
|
13
|
+
## 0.5.1 – 2025-04-04
|
|
14
|
+
|
|
15
|
+
- Raise if downloading failed after retries
|
|
16
|
+
- Adjust logging
|
|
17
|
+
|
|
8
18
|
## 0.5.0 – 2025-04-04
|
|
9
19
|
|
|
10
20
|
- Add option to query one site
|
|
@@ -270,13 +270,13 @@ def _add_date_params(
|
|
|
270
270
|
msg = "Cannot use 'date' with 'date_from' and 'date_to'"
|
|
271
271
|
raise ValueError(msg)
|
|
272
272
|
if date is not None:
|
|
273
|
-
start, stop =
|
|
273
|
+
start, stop = _parse_date_param(date)
|
|
274
274
|
params["dateFrom"] = start.isoformat()
|
|
275
275
|
params["dateTo"] = stop.isoformat()
|
|
276
276
|
if date_from is not None:
|
|
277
|
-
params["dateFrom"] =
|
|
277
|
+
params["dateFrom"] = _parse_date_param(date_from)[0].isoformat()
|
|
278
278
|
if date_to is not None:
|
|
279
|
-
params["dateTo"] =
|
|
279
|
+
params["dateTo"] = _parse_date_param(date_to)[1].isoformat()
|
|
280
280
|
|
|
281
281
|
if updated_at is not None and (
|
|
282
282
|
updated_at_from is not None or updated_at_to is not None
|
|
@@ -284,16 +284,16 @@ def _add_date_params(
|
|
|
284
284
|
msg = "Cannot use 'updated_at' with 'updated_at_from' and 'updated_at_to'"
|
|
285
285
|
raise ValueError(msg)
|
|
286
286
|
if updated_at is not None:
|
|
287
|
-
start, stop =
|
|
287
|
+
start, stop = _parse_datetime_param(updated_at)
|
|
288
288
|
params["updatedAtFrom"] = start.isoformat()
|
|
289
289
|
params["updatedAtTo"] = stop.isoformat()
|
|
290
290
|
if updated_at_from is not None:
|
|
291
|
-
params["updatedAtFrom"] =
|
|
291
|
+
params["updatedAtFrom"] = _parse_datetime_param(updated_at_from)[0].isoformat()
|
|
292
292
|
if updated_at_to is not None:
|
|
293
|
-
params["updatedAtTo"] =
|
|
293
|
+
params["updatedAtTo"] = _parse_datetime_param(updated_at_to)[1].isoformat()
|
|
294
294
|
|
|
295
295
|
|
|
296
|
-
def
|
|
296
|
+
def _parse_date_param(date: DateParam) -> tuple[datetime.date, datetime.date]:
|
|
297
297
|
if isinstance(date, datetime.date):
|
|
298
298
|
return date, date
|
|
299
299
|
error = ValueError(f"Invalid date format: {date}")
|
|
@@ -316,7 +316,9 @@ def _parse_date(date: DateParam) -> tuple[datetime.date, datetime.date]:
|
|
|
316
316
|
raise error
|
|
317
317
|
|
|
318
318
|
|
|
319
|
-
def
|
|
319
|
+
def _parse_datetime_param(
|
|
320
|
+
dt: DateTimeParam,
|
|
321
|
+
) -> tuple[datetime.datetime, datetime.datetime]:
|
|
320
322
|
if isinstance(dt, datetime.datetime):
|
|
321
323
|
return dt, dt
|
|
322
324
|
if isinstance(dt, datetime.date):
|
|
@@ -376,19 +378,49 @@ CONVERTED = {"measurement_date", "created_at", "updated_at", "size", "uuid"}
|
|
|
376
378
|
|
|
377
379
|
|
|
378
380
|
def _build_meta_objects(res: list[dict]) -> list[ProductMetadata]:
|
|
379
|
-
field_names =
|
|
381
|
+
field_names = (
|
|
382
|
+
{f.name for f in fields(ProductMetadata)}
|
|
383
|
+
- CONVERTED
|
|
384
|
+
- {"product", "instrument", "model"}
|
|
385
|
+
)
|
|
380
386
|
return [
|
|
381
387
|
ProductMetadata(
|
|
382
388
|
**{_to_snake(k): v for k, v in obj.items() if _to_snake(k) in field_names},
|
|
383
389
|
product=Product(
|
|
384
390
|
id=obj["product"]["id"],
|
|
385
391
|
human_readable_name=obj["product"]["humanReadableName"],
|
|
386
|
-
type=
|
|
392
|
+
type=obj["product"]["type"],
|
|
387
393
|
experimental=obj["product"]["experimental"],
|
|
388
394
|
),
|
|
395
|
+
instrument=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
|
+
)
|
|
405
|
+
if "instrument" in obj and obj["instrument"] is not None
|
|
406
|
+
else None,
|
|
407
|
+
model=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
|
+
)
|
|
419
|
+
if "model" in obj and obj["model"] is not None
|
|
420
|
+
else None,
|
|
389
421
|
measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
|
|
390
|
-
created_at=
|
|
391
|
-
updated_at=
|
|
422
|
+
created_at=_parse_datetime(obj["createdAt"]),
|
|
423
|
+
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
392
424
|
size=int(obj["size"]),
|
|
393
425
|
uuid=uuid.UUID(obj["uuid"]),
|
|
394
426
|
)
|
|
@@ -412,8 +444,8 @@ def _build_raw_meta_objects(res: list[dict]) -> list[RawMetadata]:
|
|
|
412
444
|
name=obj["instrumentInfo"]["name"],
|
|
413
445
|
),
|
|
414
446
|
measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
|
|
415
|
-
created_at=
|
|
416
|
-
updated_at=
|
|
447
|
+
created_at=_parse_datetime(obj["createdAt"]),
|
|
448
|
+
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
417
449
|
size=int(obj["size"]),
|
|
418
450
|
uuid=uuid.UUID(obj["uuid"]),
|
|
419
451
|
)
|
|
@@ -439,8 +471,8 @@ def _build_raw_model_meta_objects(res: list[dict]) -> list[RawModelMetadata]:
|
|
|
439
471
|
else None,
|
|
440
472
|
),
|
|
441
473
|
measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
|
|
442
|
-
created_at=
|
|
443
|
-
updated_at=
|
|
474
|
+
created_at=_parse_datetime(obj["createdAt"]),
|
|
475
|
+
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
444
476
|
size=int(obj["size"]),
|
|
445
477
|
uuid=uuid.UUID(obj["uuid"]),
|
|
446
478
|
)
|
|
@@ -459,3 +491,7 @@ def _make_session() -> requests.Session:
|
|
|
459
491
|
session.mount("https://", adapter)
|
|
460
492
|
session.mount("http://", adapter)
|
|
461
493
|
return session
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _parse_datetime(dt: str) -> datetime.datetime:
|
|
497
|
+
return datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
@@ -30,7 +30,7 @@ async def download_files(
|
|
|
30
30
|
destination = output_path / meta.download_url.split("/")[-1]
|
|
31
31
|
full_paths.append(destination)
|
|
32
32
|
if destination.exists() and _file_checksum_matches(meta, destination):
|
|
33
|
-
logging.
|
|
33
|
+
logging.debug(f"Already downloaded: {destination}")
|
|
34
34
|
continue
|
|
35
35
|
task = asyncio.create_task(
|
|
36
36
|
_download_file_with_retries(
|
|
@@ -61,6 +61,7 @@ async def _download_file_with_retries(
|
|
|
61
61
|
logging.warning(f"Attempt {attempt} failed for {url}: {e}")
|
|
62
62
|
if attempt == max_retries:
|
|
63
63
|
logging.error(f"Giving up on {url} after {max_retries} attempts.")
|
|
64
|
+
raise e
|
|
64
65
|
else:
|
|
65
66
|
# Exponential backoff before retrying
|
|
66
67
|
await asyncio.sleep(2**attempt)
|
|
@@ -93,7 +94,7 @@ async def _download_file(
|
|
|
93
94
|
break
|
|
94
95
|
file_out.write(chunk)
|
|
95
96
|
bar.update(len(chunk))
|
|
96
|
-
logging.
|
|
97
|
+
logging.debug(f"Downloaded: {destination}")
|
|
97
98
|
|
|
98
99
|
|
|
99
100
|
def _file_checksum_matches(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.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
|
|
File without changes
|