cloudnet-api-client 0.4.1__tar.gz → 0.5.1__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.5.1 – 2025-04-04
9
+
10
+ - Raise if downloading failed after retries
11
+ - Adjust logging
12
+
13
+ ## 0.5.0 – 2025-04-04
14
+
15
+ - Add option to query one site
16
+ - Add undocumented `raw_model_metadata` function
17
+
8
18
  ## 0.4.1 – 2025-04-03
9
19
 
10
20
  - Fix types
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnet-api-client
3
- Version: 0.4.1
3
+ Version: 0.5.1
4
4
  Summary: Cloudnet API client
5
5
  Author-email: Simo Tukiainen <simo.tukiainen@fmi.fi>
6
6
  License-File: LICENSE
@@ -41,9 +41,9 @@ python3 -m pip install cloudnet-api-client
41
41
  ## Quickstart
42
42
 
43
43
  ```python
44
- import cloudnet_api_client as cac
44
+ from cloudnet_api_client import APIClient
45
45
 
46
- client = cac.APIClient()
46
+ client = APIClient()
47
47
 
48
48
  sites = client.sites(type="cloudnet")
49
49
  products = client.products()
@@ -127,9 +127,10 @@ Fetch cloudnet sites.
127
127
 
128
128
  Parameters:
129
129
 
130
- | name | type | Choices | default |
131
- | ---- | -------------------- | ----------------------------------------- | ------- |
132
- | type | `str` or `list[str]` | "cloudnet", "campaign", "model", "hidden" | `None` |
130
+ | name | type | Choices | default |
131
+ | ------- | -------------------- | ----------------------------------------- | ------- |
132
+ | site_id | `str` | | `None` |
133
+ | type | `str` or `list[str]` | "cloudnet", "campaign", "model", "hidden" | `None` |
133
134
 
134
135
  ### `APIClient().products()` &rarr; `list[Product]`
135
136
 
@@ -14,9 +14,9 @@ python3 -m pip install cloudnet-api-client
14
14
  ## Quickstart
15
15
 
16
16
  ```python
17
- import cloudnet_api_client as cac
17
+ from cloudnet_api_client import APIClient
18
18
 
19
- client = cac.APIClient()
19
+ client = APIClient()
20
20
 
21
21
  sites = client.sites(type="cloudnet")
22
22
  products = client.products()
@@ -100,9 +100,10 @@ Fetch cloudnet sites.
100
100
 
101
101
  Parameters:
102
102
 
103
- | name | type | Choices | default |
104
- | ---- | -------------------- | ----------------------------------------- | ------- |
105
- | type | `str` or `list[str]` | "cloudnet", "campaign", "model", "hidden" | `None` |
103
+ | name | type | Choices | default |
104
+ | ------- | -------------------- | ----------------------------------------- | ------- |
105
+ | site_id | `str` | | `None` |
106
+ | type | `str` or `list[str]` | "cloudnet", "campaign", "model", "hidden" | `None` |
106
107
 
107
108
  ### `APIClient().products()` &rarr; `list[Product]`
108
109
 
@@ -19,16 +19,18 @@ from cloudnet_api_client.containers import (
19
19
  SITE_TYPE,
20
20
  STATUS,
21
21
  Instrument,
22
+ Model,
22
23
  Product,
23
24
  ProductMetadata,
24
25
  RawMetadata,
26
+ RawModelMetadata,
25
27
  Site,
26
28
  )
27
29
  from cloudnet_api_client.dl import download_files
28
30
 
29
31
  T = TypeVar("T")
30
- MetadataList = list[ProductMetadata] | list[RawMetadata]
31
- TMetadata = TypeVar("TMetadata", ProductMetadata, RawMetadata)
32
+ MetadataList = list[ProductMetadata] | list[RawMetadata] | list[RawModelMetadata]
33
+ TMetadata = TypeVar("TMetadata", ProductMetadata, RawMetadata, RawModelMetadata)
32
34
  DateParam = str | datetime.date | None
33
35
  DateTimeParam = str | datetime.datetime | datetime.date | None
34
36
  QueryParam = str | list[str] | None
@@ -43,9 +45,15 @@ class APIClient:
43
45
  self.base_url = base_url
44
46
  self.session = session or _make_session()
45
47
 
46
- def sites(self, type: SITE_TYPE | list[SITE_TYPE] | None = None) -> list[Site]:
47
- params = {"type": type}
48
- res = self._get_response("sites", params)
48
+ def sites(
49
+ self,
50
+ site_id: str | None = None,
51
+ type: SITE_TYPE | list[SITE_TYPE] | None = None,
52
+ ) -> list[Site]:
53
+ if site_id:
54
+ res = self._get_response(f"sites/{site_id}")
55
+ else:
56
+ res = self._get_response("sites", {"type": type})
49
57
  return _build_objects(res, Site)
50
58
 
51
59
  def products(
@@ -148,6 +156,34 @@ class APIClient:
148
156
  res = self._get_response("raw-files", params)
149
157
  return _build_raw_meta_objects(res)
150
158
 
159
+ def raw_model_metadata(
160
+ self,
161
+ site_id: str,
162
+ model_id: QueryParam = None,
163
+ date: DateParam = None,
164
+ date_from: DateParam = None,
165
+ date_to: DateParam = None,
166
+ updated_at: DateTimeParam = None,
167
+ updated_at_from: DateTimeParam = None,
168
+ updated_at_to: DateTimeParam = None,
169
+ filename_prefix: QueryParam = None,
170
+ filename_suffix: QueryParam = None,
171
+ status: STATUS | list[STATUS] | None = None,
172
+ ) -> list[RawModelMetadata]:
173
+ """For internal CLU use only. Will change in the future."""
174
+ params = {
175
+ "site": site_id,
176
+ "filenamePrefix": filename_prefix,
177
+ "filenameSuffix": filename_suffix,
178
+ "status": status,
179
+ "model": model_id,
180
+ }
181
+ _add_date_params(
182
+ params, date, date_from, date_to, updated_at, updated_at_from, updated_at_to
183
+ )
184
+ res = self._get_response("raw-model-files", params)
185
+ return _build_raw_model_meta_objects(res)
186
+
151
187
  def download(
152
188
  self,
153
189
  metadata: MetadataList,
@@ -215,7 +251,10 @@ class APIClient:
215
251
  url = urljoin(self.base_url, endpoint)
216
252
  res = self.session.get(url, params=params, timeout=120)
217
253
  res.raise_for_status()
218
- return res.json()
254
+ data = res.json()
255
+ if isinstance(data, dict):
256
+ data = [data]
257
+ return data
219
258
 
220
259
 
221
260
  def _add_date_params(
@@ -382,6 +421,33 @@ def _build_raw_meta_objects(res: list[dict]) -> list[RawMetadata]:
382
421
  ]
383
422
 
384
423
 
424
+ def _build_raw_model_meta_objects(res: list[dict]) -> list[RawModelMetadata]:
425
+ field_names = {f.name for f in fields(RawModelMetadata)} - CONVERTED - {"model"}
426
+ return [
427
+ RawModelMetadata(
428
+ **{_to_snake(k): v for k, v in obj.items() if _to_snake(k) in field_names},
429
+ model=Model(
430
+ model_id=obj["model"]["id"],
431
+ name=obj["model"]["humanReadableName"],
432
+ optimum_order=int(obj["model"]["optimumOrder"]),
433
+ source_model_id=obj["model"]["sourceModelId"],
434
+ forecast_start=int(obj["model"]["forecastStart"])
435
+ if obj["model"]["forecastStart"] is not None
436
+ else None,
437
+ forecast_end=int(obj["model"]["forecastEnd"])
438
+ if obj["model"]["forecastEnd"] is not None
439
+ else None,
440
+ ),
441
+ measurement_date=datetime.date.fromisoformat(obj["measurementDate"]),
442
+ created_at=datetime.datetime.fromisoformat(obj["createdAt"]),
443
+ updated_at=datetime.datetime.fromisoformat(obj["updatedAt"]),
444
+ size=int(obj["size"]),
445
+ uuid=uuid.UUID(obj["uuid"]),
446
+ )
447
+ for obj in res
448
+ ]
449
+
450
+
385
451
  def _to_snake(name: str) -> str:
386
452
  return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
387
453
 
@@ -22,7 +22,6 @@ class Site:
22
22
  country_code: str
23
23
  country_subdivision_code: str | None
24
24
  type: list[SITE_TYPE]
25
- status: Literal["active", "inactive"]
26
25
  gaw: str | None
27
26
 
28
27
 
@@ -46,6 +45,16 @@ class Instrument:
46
45
  serial_number: str | None
47
46
 
48
47
 
48
+ @dataclass(frozen=True, slots=True)
49
+ class Model:
50
+ model_id: str
51
+ name: str
52
+ optimum_order: int
53
+ source_model_id: str
54
+ forecast_start: int | None
55
+ forecast_end: int | None
56
+
57
+
49
58
  @dataclass(frozen=True, slots=True)
50
59
  class Metadata:
51
60
  uuid: uuid.UUID
@@ -65,6 +74,12 @@ class RawMetadata(Metadata):
65
74
  tags: list[str] | None
66
75
 
67
76
 
77
+ @dataclass(frozen=True, slots=True)
78
+ class RawModelMetadata(Metadata):
79
+ status: STATUS
80
+ model: Model
81
+
82
+
68
83
  @dataclass(frozen=True, slots=True)
69
84
  class ProductMetadata(Metadata):
70
85
  product: Product
@@ -7,12 +7,16 @@ from tqdm import tqdm
7
7
  from tqdm.asyncio import tqdm_asyncio
8
8
 
9
9
  from cloudnet_api_client import utils
10
- from cloudnet_api_client.containers import ProductMetadata, RawMetadata
10
+ from cloudnet_api_client.containers import (
11
+ ProductMetadata,
12
+ RawMetadata,
13
+ RawModelMetadata,
14
+ )
11
15
 
12
16
 
13
17
  async def download_files(
14
18
  base_url: str,
15
- metadata: list[ProductMetadata] | list[RawMetadata],
19
+ metadata: list[ProductMetadata] | list[RawMetadata] | list[RawModelMetadata],
16
20
  output_path: Path,
17
21
  concurrency_limit: int,
18
22
  disable_progress: bool | None,
@@ -26,7 +30,7 @@ async def download_files(
26
30
  destination = output_path / meta.download_url.split("/")[-1]
27
31
  full_paths.append(destination)
28
32
  if destination.exists() and _file_checksum_matches(meta, destination):
29
- logging.info(f"Already downloaded: {destination}")
33
+ logging.debug(f"Already downloaded: {destination}")
30
34
  continue
31
35
  task = asyncio.create_task(
32
36
  _download_file_with_retries(
@@ -57,6 +61,7 @@ async def _download_file_with_retries(
57
61
  logging.warning(f"Attempt {attempt} failed for {url}: {e}")
58
62
  if attempt == max_retries:
59
63
  logging.error(f"Giving up on {url} after {max_retries} attempts.")
64
+ raise e
60
65
  else:
61
66
  # Exponential backoff before retrying
62
67
  await asyncio.sleep(2**attempt)
@@ -89,11 +94,11 @@ async def _download_file(
89
94
  break
90
95
  file_out.write(chunk)
91
96
  bar.update(len(chunk))
92
- logging.info(f"Downloaded: {destination}")
97
+ logging.debug(f"Downloaded: {destination}")
93
98
 
94
99
 
95
100
  def _file_checksum_matches(
96
- meta: ProductMetadata | RawMetadata, destination: Path
101
+ meta: ProductMetadata | RawMetadata | RawModelMetadata, destination: Path
97
102
  ) -> bool:
98
- fun = utils.md5sum if isinstance(meta, RawMetadata) else utils.sha256sum
103
+ fun = utils.sha256sum if isinstance(meta, ProductMetadata) else utils.md5sum
99
104
  return fun(destination) == meta.checksum
@@ -0,0 +1 @@
1
+ __version__ = "0.5.1"
@@ -1 +0,0 @@
1
- __version__ = "0.4.1"