cloudnet-api-client 0.12.3__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.
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/CHANGELOG.md +7 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/PKG-INFO +9 -5
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/README.md +8 -4
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/cloudnet_api_client/client.py +26 -15
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/cloudnet_api_client/containers.py +15 -1
- cloudnet_api_client-0.12.4/cloudnet_api_client/version.py +1 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/tests/test_client.py +46 -0
- cloudnet_api_client-0.12.3/cloudnet_api_client/version.py +0 -1
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/.github/dataportal.env +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/.github/db.env +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/.github/docker-compose.yml +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/.github/initdb.d/init-dbs.sh +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/.github/ss.env +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/.github/workflows/publish.yml +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/.github/workflows/test.yml +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/.gitignore +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/.pre-commit-config.yaml +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/LICENSE +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/cloudnet_api_client/__init__.py +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/cloudnet_api_client/dl.py +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/cloudnet_api_client/py.typed +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/cloudnet_api_client/utils.py +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/pyproject.toml +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/tests/data/20140205_hyytiala_classification.nc +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/tests/data/20250801_Magurele_CHM170137_000.nc +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/tests/data/20250803_JOYCE_WST_01m.dat +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/tests/data/20250808_Granada_CHM170119_0045_000.nc +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/tests/data/20250808_hyytiala_iwc-Z-T-method.nc +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/tests/data/20250814_bucharest_classification.nc +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/tests/data/20250821_limassol_parsivel_41582c49.nc +0 -0
- {cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/tests/data/20250822_leipzig-lim_ecmwf-open.nc +0 -0
|
@@ -5,6 +5,13 @@ 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
|
+
|
|
8
15
|
## 0.12.3 – 2025-08-26
|
|
9
16
|
|
|
10
17
|
- Fix owners type
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudnet-api-client
|
|
3
|
-
Version: 0.12.
|
|
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()` → `frozenset[str]`
|
|
199
|
+
|
|
200
|
+
Fetch all instrument identifiers.
|
|
201
|
+
|
|
198
202
|
### `APIClient().instrument()` → `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()` → `list[Model]`
|
|
209
213
|
|
|
210
214
|
Fetch all models.
|
|
211
215
|
|
|
212
|
-
### `APIClient().
|
|
216
|
+
### `APIClient().model()` → `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()` → `frozenset[str]`
|
|
170
|
+
|
|
171
|
+
Fetch all instrument identifiers.
|
|
172
|
+
|
|
169
173
|
### `APIClient().instrument()` → `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()` → `list[Model]`
|
|
180
184
|
|
|
181
185
|
Fetch all models.
|
|
182
186
|
|
|
183
|
-
### `APIClient().
|
|
187
|
+
### `APIClient().model()` → `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 =
|
|
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 =
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
|
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)
|
|
@@ -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:
|
|
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.3"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudnet_api_client-0.12.3 → cloudnet_api_client-0.12.4}/tests/data/20250803_JOYCE_WST_01m.dat
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|