cloudnet-api-client 0.12.2__tar.gz → 0.12.4__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.
Files changed (31) hide show
  1. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/CHANGELOG.md +11 -0
  2. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/PKG-INFO +9 -5
  3. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/README.md +8 -4
  4. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/cloudnet_api_client/client.py +26 -15
  5. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/cloudnet_api_client/containers.py +16 -2
  6. cloudnet_api_client-0.12.4/cloudnet_api_client/version.py +1 -0
  7. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/tests/test_client.py +46 -0
  8. cloudnet_api_client-0.12.2/cloudnet_api_client/version.py +0 -1
  9. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/.github/dataportal.env +0 -0
  10. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/.github/db.env +0 -0
  11. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/.github/docker-compose.yml +0 -0
  12. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/.github/initdb.d/init-dbs.sh +0 -0
  13. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/.github/ss.env +0 -0
  14. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/.github/workflows/publish.yml +0 -0
  15. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/.github/workflows/test.yml +0 -0
  16. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/.gitignore +0 -0
  17. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/.pre-commit-config.yaml +0 -0
  18. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/LICENSE +0 -0
  19. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/cloudnet_api_client/__init__.py +0 -0
  20. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/cloudnet_api_client/dl.py +0 -0
  21. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/cloudnet_api_client/py.typed +0 -0
  22. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/cloudnet_api_client/utils.py +0 -0
  23. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/pyproject.toml +0 -0
  24. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/tests/data/20140205_hyytiala_classification.nc +0 -0
  25. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/tests/data/20250801_Magurele_CHM170137_000.nc +0 -0
  26. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/tests/data/20250803_JOYCE_WST_01m.dat +0 -0
  27. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/tests/data/20250808_Granada_CHM170119_0045_000.nc +0 -0
  28. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/tests/data/20250808_hyytiala_iwc-Z-T-method.nc +0 -0
  29. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/tests/data/20250814_bucharest_classification.nc +0 -0
  30. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/tests/data/20250821_limassol_parsivel_41582c49.nc +0 -0
  31. {cloudnet_api_client-0.12.2 → cloudnet_api_client-0.12.4}/tests/data/20250822_leipzig-lim_ecmwf-open.nc +0 -0
@@ -5,6 +5,17 @@ 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.12.4 – 2025-08-26
9
+
10
+ - Add `software` attribute to `ProductMetadata`
11
+ - Fix hashability of `Product` in `ProductMetadata`
12
+ - Add possible timeliness values
13
+ - Use aware datetimes
14
+
15
+ ## 0.12.3 – 2025-08-26
16
+
17
+ - Fix owners type
18
+
8
19
  ## 0.12.2 – 2025-08-25
9
20
 
10
21
  - Fix product type filter bug
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnet-api-client
3
- Version: 0.12.2
3
+ Version: 0.12.4
4
4
  Summary: Cloudnet API client
5
5
  Author-email: Simo Tukiainen <simo.tukiainen@fmi.fi>
6
6
  License-File: LICENSE
@@ -195,21 +195,25 @@ Parameters:
195
195
 
196
196
  Fetch all instruments.
197
197
 
198
+ ### `APIClient().instrument_ids()` &rarr; `frozenset[str]`
199
+
200
+ Fetch all instrument identifiers.
201
+
198
202
  ### `APIClient().instrument()` &rarr; `ExtendedInstrument`
199
203
 
200
204
  Fetch a single instruments.
201
205
 
202
206
  Parameters:
203
207
 
204
- | name | type |
205
- | ---- | ----- |
206
- | uuid | `str` |
208
+ | name | type |
209
+ | ---- | -------------------- |
210
+ | uuid | `str` or `uuid.UUID` |
207
211
 
208
212
  ### `APIClient().models()` &rarr; `list[Model]`
209
213
 
210
214
  Fetch all models.
211
215
 
212
- ### `APIClient().instrument()` &rarr; `Model`
216
+ ### `APIClient().model()` &rarr; `Model`
213
217
 
214
218
  Fetch a single model.
215
219
 
@@ -166,21 +166,25 @@ Parameters:
166
166
 
167
167
  Fetch all instruments.
168
168
 
169
+ ### `APIClient().instrument_ids()` &rarr; `frozenset[str]`
170
+
171
+ Fetch all instrument identifiers.
172
+
169
173
  ### `APIClient().instrument()` &rarr; `ExtendedInstrument`
170
174
 
171
175
  Fetch a single instruments.
172
176
 
173
177
  Parameters:
174
178
 
175
- | name | type |
176
- | ---- | ----- |
177
- | uuid | `str` |
179
+ | name | type |
180
+ | ---- | -------------------- |
181
+ | uuid | `str` or `uuid.UUID` |
178
182
 
179
183
  ### `APIClient().models()` &rarr; `list[Model]`
180
184
 
181
185
  Fetch all models.
182
186
 
183
- ### `APIClient().instrument()` &rarr; `Model`
187
+ ### `APIClient().model()` &rarr; `Model`
184
188
 
185
189
  Fetch a single model.
186
190
 
@@ -22,6 +22,7 @@ from cloudnet_api_client.containers import (
22
22
  STATUS,
23
23
  ExtendedInstrument,
24
24
  ExtendedProduct,
25
+ ExtendedProductMetadata,
25
26
  Instrument,
26
27
  Location,
27
28
  Model,
@@ -30,6 +31,7 @@ from cloudnet_api_client.containers import (
30
31
  RawMetadata,
31
32
  RawModelMetadata,
32
33
  Site,
34
+ Software,
33
35
  VersionMetadata,
34
36
  )
35
37
  from cloudnet_api_client.dl import download_files
@@ -60,7 +62,7 @@ class APIClient:
60
62
  self,
61
63
  type: SITE_TYPE | list[SITE_TYPE] | None = None,
62
64
  ) -> list[Site]:
63
- type = validate_type(type, SITE_TYPE)
65
+ type = _validate_type(type, SITE_TYPE)
64
66
  res = self._get("sites", {"type": type})
65
67
  return _build_objects(res, Site)
66
68
 
@@ -71,7 +73,7 @@ class APIClient:
71
73
  def products(
72
74
  self, type: PRODUCT_TYPE | list[PRODUCT_TYPE] | None = None
73
75
  ) -> list[Product]:
74
- type = validate_type(type, PRODUCT_TYPE)
76
+ type = _validate_type(type, PRODUCT_TYPE)
75
77
  data = self._get("products")
76
78
  if type is not None:
77
79
  data = [obj for obj in data if any(t in obj["type"] for t in type)]
@@ -121,14 +123,18 @@ class APIClient:
121
123
  def file(
122
124
  self,
123
125
  uuid: str | UUID,
124
- ) -> ProductMetadata:
126
+ ) -> ExtendedProductMetadata:
125
127
  file_res = self._get(f"files/{uuid}")[0]
126
128
  if file_res.get("instrument") is not None:
127
129
  instrument_uuid = file_res["instrument"]["uuid"]
128
130
  instrument_res = self._get(f"instrument-pids/{instrument_uuid}")[0]
129
131
  else:
130
132
  instrument_res = None
131
- return _build_meta_objects([file_res], instrument_res)[0]
133
+ obj = _build_meta_objects([file_res], instrument_res)[0]
134
+ return ExtendedProductMetadata(
135
+ **_asdict_shallow(obj),
136
+ software=tuple(_build_objects(file_res["software"], Software)),
137
+ )
132
138
 
133
139
  def versions(self, uuid: str | UUID) -> list[VersionMetadata]:
134
140
  payload = {"properties": ["pid", "dvasId", "legacy", "size", "checksum"]}
@@ -482,7 +488,9 @@ def _parse_datetime_param(
482
488
  }
483
489
  for fmt, unit in patterns:
484
490
  try:
485
- start_date = datetime.datetime.strptime(dt, fmt)
491
+ start_date = datetime.datetime.strptime(dt, fmt).replace(
492
+ tzinfo=datetime.timezone.utc
493
+ )
486
494
  except ValueError:
487
495
  continue
488
496
  if unit == "years":
@@ -524,17 +532,12 @@ def _build_meta_objects(
524
532
  field_names = (
525
533
  {f.name for f in fields(ProductMetadata)}
526
534
  - CONVERTED
527
- - {"product", "instrument", "model", "site"}
535
+ - {"product", "instrument", "model", "site", "software"}
528
536
  )
529
537
  return [
530
538
  ProductMetadata(
531
539
  **{_to_snake(k): v for k, v in obj.items() if _to_snake(k) in field_names},
532
- product=Product(
533
- id=obj["product"]["id"],
534
- human_readable_name=obj["product"]["humanReadableName"],
535
- type=obj["product"]["type"],
536
- experimental=obj["product"]["experimental"],
537
- ),
540
+ product=_build_object(obj["product"], Product),
538
541
  instrument=_create_instrument_object(instrument_meta or obj["instrument"])
539
542
  if instrument_meta or "instrument" in obj and obj["instrument"]
540
543
  else None,
@@ -664,9 +667,13 @@ def _make_session() -> requests.Session:
664
667
 
665
668
  def _parse_datetime(dt: str) -> datetime.datetime:
666
669
  try:
667
- return datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S.%fZ")
670
+ return datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S.%fZ").replace(
671
+ tzinfo=datetime.timezone.utc
672
+ )
668
673
  except ValueError:
669
- return datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%SZ")
674
+ return datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%SZ").replace(
675
+ tzinfo=datetime.timezone.utc
676
+ )
670
677
 
671
678
 
672
679
  def _check_params(params: dict, ignore: tuple = ()) -> None:
@@ -674,7 +681,11 @@ def _check_params(params: dict, ignore: tuple = ()) -> None:
674
681
  raise TypeError("At least one of the parameters must be set.")
675
682
 
676
683
 
677
- def validate_type(type, values) -> list | None:
684
+ def _asdict_shallow(obj) -> dict:
685
+ return dict((field.name, getattr(obj, field.name)) for field in fields(obj))
686
+
687
+
688
+ def _validate_type(type, values) -> list | None:
678
689
  if type is not None:
679
690
  if not isinstance(type, str | list):
680
691
  raise ValueError(f"Invalid type: {type}")
@@ -6,6 +6,7 @@ from typing import Literal
6
6
  SITE_TYPE = Literal["cloudnet", "model", "hidden", "campaign"]
7
7
  PRODUCT_TYPE = Literal["instrument", "geophysical", "evaluation", "model"]
8
8
  STATUS = Literal["created", "uploaded", "processed", "invalid"]
9
+ TIMELINESS = Literal["rrt", "nrt", "scheduled"]
9
10
 
10
11
 
11
12
  @dataclass(frozen=True, slots=True)
@@ -55,7 +56,7 @@ class Instrument:
55
56
  name: str # e.g. "FMI RPG-FMCW-94 (Pallas)"
56
57
  uuid: uuid.UUID
57
58
  pid: str
58
- owners: tuple[str] # could be ordered
59
+ owners: tuple[str, ...] # could be ordered
59
60
  serial_number: str | None
60
61
 
61
62
 
@@ -74,6 +75,14 @@ class Model:
74
75
  forecast_end: int | None
75
76
 
76
77
 
78
+ @dataclass(frozen=True, slots=True)
79
+ class Software:
80
+ id: str
81
+ version: str
82
+ title: str
83
+ url: str
84
+
85
+
77
86
  @dataclass(frozen=True, slots=True)
78
87
  class Metadata:
79
88
  uuid: uuid.UUID
@@ -111,12 +120,17 @@ class ProductMetadata(Metadata):
111
120
  dvas_id: str | None
112
121
  error_level: str | None
113
122
  coverage: float
114
- timeliness: str
123
+ timeliness: TIMELINESS
115
124
  format: str
116
125
  start_time: datetime.datetime | None
117
126
  stop_time: datetime.datetime | None
118
127
 
119
128
 
129
+ @dataclass(frozen=True, slots=True)
130
+ class ExtendedProductMetadata(ProductMetadata):
131
+ software: tuple[Software, ...]
132
+
133
+
120
134
  @dataclass(frozen=True, slots=True)
121
135
  class VersionMetadata:
122
136
  uuid: uuid.UUID
@@ -0,0 +1 @@
1
+ __version__ = "0.12.4"
@@ -142,6 +142,15 @@ class TestSites:
142
142
  with pytest.raises(CloudnetAPIError):
143
143
  client.site("invalid-site-id")
144
144
 
145
+ def test_sites_are_hashable(self, client: APIClient):
146
+ sites = client.sites()
147
+ for site in sites:
148
+ hash(site)
149
+
150
+ def test_site_is_hashable(self, client: APIClient):
151
+ site = client.site("bucharest")
152
+ hash(site)
153
+
145
154
 
146
155
  class TestProducts:
147
156
  def test_products_route(self, client: APIClient):
@@ -200,6 +209,15 @@ class TestProducts:
200
209
  with pytest.raises(CloudnetAPIError):
201
210
  client.product("invalid-product-id")
202
211
 
212
+ def test_products_are_hashable(self, client: APIClient):
213
+ products = client.products()
214
+ for product in products:
215
+ hash(product)
216
+
217
+ def test_product_is_hashable(self, client: APIClient):
218
+ product = client.product("categorize")
219
+ hash(product)
220
+
203
221
 
204
222
  class TestModels:
205
223
  def test_models_route(self, client: APIClient):
@@ -216,6 +234,15 @@ class TestModels:
216
234
  with pytest.raises(CloudnetAPIError):
217
235
  client.model("invalid-model-id")
218
236
 
237
+ def test_models_are_hashable(self, client: APIClient):
238
+ models = client.models()
239
+ for model in models:
240
+ hash(model)
241
+
242
+ def test_model_is_hashable(self, client: APIClient):
243
+ model = client.model("arpege-12-23")
244
+ hash(model)
245
+
219
246
 
220
247
  class TestInstruments:
221
248
  def test_instruments_route(self, client: APIClient):
@@ -241,6 +268,15 @@ class TestInstruments:
241
268
  with pytest.raises(CloudnetAPIError):
242
269
  client.instrument("invalid-instrument-id")
243
270
 
271
+ def test_instruments_are_hashable(self, client: APIClient):
272
+ instruments = client.instruments()
273
+ for instrument in instruments:
274
+ hash(instrument)
275
+
276
+ def test_instrument_is_hashable(self, client: APIClient):
277
+ instrument = client.instrument("12da536f-0d07-41ea-9ced-f6cdeb97198b")
278
+ hash(instrument)
279
+
244
280
 
245
281
  class TestProductFiles:
246
282
  def test_file_route_with_geophysical_product(self, client: APIClient):
@@ -295,6 +331,11 @@ class TestProductFiles:
295
331
  with pytest.raises(CloudnetAPIError):
296
332
  client.files(site_id="invalid-site")
297
333
 
334
+ def test_file_is_hashable(self, client: APIClient):
335
+ uuid = "8dcc865c-6920-49ce-a627-de045ec896e8"
336
+ meta = client.file(uuid)
337
+ hash(meta)
338
+
298
339
 
299
340
  class TestRawFiles:
300
341
  def test_filter_by_site_and_date(self, client: APIClient):
@@ -356,6 +397,11 @@ class TestRawFiles:
356
397
  with pytest.raises(CloudnetAPIError):
357
398
  client.raw_files(site_id="invalid-site")
358
399
 
400
+ def test_raw_files_are_hashable(self, client: APIClient):
401
+ meta = client.raw_files(date_from="2025-08-01", date_to="2025-08-08")
402
+ for m in meta:
403
+ hash(m)
404
+
359
405
 
360
406
  class TestDateParameterHandling:
361
407
  """Test various date parameter formats and edge cases."""
@@ -1 +0,0 @@
1
- __version__ = "0.12.2"