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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnet-api-client
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Cloudnet API client
5
5
  Author-email: Simo Tukiainen <simo.tukiainen@fmi.fi>
6
6
  License-File: LICENSE
@@ -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 = _parse_date(date)
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"] = _parse_date(date_from)[0].isoformat()
277
+ params["dateFrom"] = _parse_date_param(date_from)[0].isoformat()
278
278
  if date_to is not None:
279
- params["dateTo"] = _parse_date(date_to)[1].isoformat()
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 = _parse_datetime(updated_at)
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"] = _parse_datetime(updated_at_from)[0].isoformat()
291
+ params["updatedAtFrom"] = _parse_datetime_param(updated_at_from)[0].isoformat()
292
292
  if updated_at_to is not None:
293
- params["updatedAtTo"] = _parse_datetime(updated_at_to)[1].isoformat()
293
+ params["updatedAtTo"] = _parse_datetime_param(updated_at_to)[1].isoformat()
294
294
 
295
295
 
296
- def _parse_date(date: DateParam) -> tuple[datetime.date, datetime.date]:
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 _parse_datetime(dt: DateTimeParam) -> tuple[datetime.datetime, datetime.datetime]:
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 = {f.name for f in fields(ProductMetadata)} - CONVERTED - {"product"}
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=[obj["product"]["type"][1:-1]],
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=datetime.datetime.fromisoformat(obj["createdAt"]),
391
- updated_at=datetime.datetime.fromisoformat(obj["updatedAt"]),
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=datetime.datetime.fromisoformat(obj["createdAt"]),
416
- updated_at=datetime.datetime.fromisoformat(obj["updatedAt"]),
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=datetime.datetime.fromisoformat(obj["createdAt"]),
443
- updated_at=datetime.datetime.fromisoformat(obj["updatedAt"]),
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")
@@ -83,3 +83,5 @@ class RawModelMetadata(Metadata):
83
83
  @dataclass(frozen=True, slots=True)
84
84
  class ProductMetadata(Metadata):
85
85
  product: Product
86
+ instrument: Instrument | None
87
+ model: Model | None
@@ -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.info(f"Already downloaded: {destination}")
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.info(f"Downloaded: {destination}")
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"