cloudnet-api-client 0.10.0__tar.gz → 0.11.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.10.0 → cloudnet_api_client-0.11.0}/.github/workflows/test.yml +1 -1
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/CHANGELOG.md +5 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/PKG-INFO +25 -3
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/README.md +22 -2
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/cloudnet_api_client/client.py +41 -8
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/cloudnet_api_client/containers.py +21 -1
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/cloudnet_api_client/utils.py +8 -3
- cloudnet_api_client-0.11.0/cloudnet_api_client/version.py +1 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/pyproject.toml +4 -1
- cloudnet_api_client-0.11.0/tests/data/20140205_hyytiala_classification.nc +0 -0
- cloudnet_api_client-0.11.0/tests/data/20250808_hyytiala_iwc-Z-T-method.nc +0 -0
- cloudnet_api_client-0.11.0/tests/data/20250814_bucharest_classification.nc +0 -0
- cloudnet_api_client-0.11.0/tests/test_client.py +359 -0
- cloudnet_api_client-0.10.0/cloudnet_api_client/version.py +0 -1
- cloudnet_api_client-0.10.0/tests/test_client.py +0 -139
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/.github/dataportal.env +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/.github/db.env +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/.github/docker-compose.yml +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/.github/initdb.d/init-dbs.sh +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/.github/ss.env +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/.github/workflows/publish.yml +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/.gitignore +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/.pre-commit-config.yaml +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/LICENSE +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/cloudnet_api_client/__init__.py +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/cloudnet_api_client/dl.py +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/cloudnet_api_client/py.typed +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/tests/data/20250801_Magurele_CHM170137_000.nc +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/tests/data/20250803_JOYCE_WST_01m.dat +0 -0
- {cloudnet_api_client-0.10.0 → cloudnet_api_client-0.11.0}/tests/data/20250808_Granada_CHM170119_0045_000.nc +0 -0
|
@@ -5,6 +5,11 @@ 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.11.0 – 2025-08-16
|
|
9
|
+
|
|
10
|
+
- Adjust routes and responses
|
|
11
|
+
- Improve tests
|
|
12
|
+
|
|
8
13
|
## 0.10.0 – 2025-08-13
|
|
9
14
|
|
|
10
15
|
- Add `volatile` to metadata response
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudnet-api-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Cloudnet API client
|
|
5
5
|
Author-email: Simo Tukiainen <simo.tukiainen@fmi.fi>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -22,7 +22,9 @@ Requires-Dist: types-requests; extra == 'dev'
|
|
|
22
22
|
Requires-Dist: types-tqdm; extra == 'dev'
|
|
23
23
|
Provides-Extra: test
|
|
24
24
|
Requires-Dist: mypy; extra == 'test'
|
|
25
|
+
Requires-Dist: netcdf4; extra == 'test'
|
|
25
26
|
Requires-Dist: pytest; extra == 'test'
|
|
27
|
+
Requires-Dist: pytest-asyncio; extra == 'test'
|
|
26
28
|
Description-Content-Type: text/markdown
|
|
27
29
|
|
|
28
30
|
[](https://github.com/actris-cloudnet/cloudnet-api-client/actions/workflows/test.yml)
|
|
@@ -128,13 +130,33 @@ Parameters:
|
|
|
128
130
|
|
|
129
131
|
\* = only with `RawMetadata`
|
|
130
132
|
|
|
133
|
+
### `APIClient().file()` → `ProductMetadata`
|
|
134
|
+
|
|
135
|
+
Fetch metadata of a single file.
|
|
136
|
+
|
|
137
|
+
Parameters:
|
|
138
|
+
|
|
139
|
+
| name | type |
|
|
140
|
+
| ---- | -------------------- |
|
|
141
|
+
| uuid | `str` or `uuid.UUID` |
|
|
142
|
+
|
|
143
|
+
### `APIClient().versions()` → `list[VersionMetadata]`
|
|
144
|
+
|
|
145
|
+
Fetch information of all versions of a file.
|
|
146
|
+
|
|
147
|
+
Parameters:
|
|
148
|
+
|
|
149
|
+
| name | type |
|
|
150
|
+
| ---- | -------------------- |
|
|
151
|
+
| uuid | `str` or `uuid.UUID` |
|
|
152
|
+
|
|
131
153
|
### `APIClient().sites()` → `list[Site]`
|
|
132
154
|
|
|
133
155
|
Fetch cloudnet sites.
|
|
134
156
|
|
|
135
157
|
Parameters:
|
|
136
158
|
|
|
137
|
-
| name | type |
|
|
159
|
+
| name | type | choices | default |
|
|
138
160
|
| ------- | -------------------- | ----------------------------------------- | ------- |
|
|
139
161
|
| site_id | `str` | | `None` |
|
|
140
162
|
| type | `str` or `list[str]` | "cloudnet", "campaign", "model", "hidden" | `None` |
|
|
@@ -145,7 +167,7 @@ Fetch cloudnet products.
|
|
|
145
167
|
|
|
146
168
|
Parameters:
|
|
147
169
|
|
|
148
|
-
| name | type |
|
|
170
|
+
| name | type | choices | default |
|
|
149
171
|
| ---- | -------------------- | ----------------------------------------- | ------- |
|
|
150
172
|
| type | `str` or `list[str]` | "instrument", "geophysical", "evaluation" | `None` |
|
|
151
173
|
|
|
@@ -101,13 +101,33 @@ Parameters:
|
|
|
101
101
|
|
|
102
102
|
\* = only with `RawMetadata`
|
|
103
103
|
|
|
104
|
+
### `APIClient().file()` → `ProductMetadata`
|
|
105
|
+
|
|
106
|
+
Fetch metadata of a single file.
|
|
107
|
+
|
|
108
|
+
Parameters:
|
|
109
|
+
|
|
110
|
+
| name | type |
|
|
111
|
+
| ---- | -------------------- |
|
|
112
|
+
| uuid | `str` or `uuid.UUID` |
|
|
113
|
+
|
|
114
|
+
### `APIClient().versions()` → `list[VersionMetadata]`
|
|
115
|
+
|
|
116
|
+
Fetch information of all versions of a file.
|
|
117
|
+
|
|
118
|
+
Parameters:
|
|
119
|
+
|
|
120
|
+
| name | type |
|
|
121
|
+
| ---- | -------------------- |
|
|
122
|
+
| uuid | `str` or `uuid.UUID` |
|
|
123
|
+
|
|
104
124
|
### `APIClient().sites()` → `list[Site]`
|
|
105
125
|
|
|
106
126
|
Fetch cloudnet sites.
|
|
107
127
|
|
|
108
128
|
Parameters:
|
|
109
129
|
|
|
110
|
-
| name | type |
|
|
130
|
+
| name | type | choices | default |
|
|
111
131
|
| ------- | -------------------- | ----------------------------------------- | ------- |
|
|
112
132
|
| site_id | `str` | | `None` |
|
|
113
133
|
| type | `str` or `list[str]` | "cloudnet", "campaign", "model", "hidden" | `None` |
|
|
@@ -118,7 +138,7 @@ Fetch cloudnet products.
|
|
|
118
138
|
|
|
119
139
|
Parameters:
|
|
120
140
|
|
|
121
|
-
| name | type |
|
|
141
|
+
| name | type | choices | default |
|
|
122
142
|
| ---- | -------------------- | ----------------------------------------- | ------- |
|
|
123
143
|
| type | `str` or `list[str]` | "instrument", "geophysical", "evaluation" | `None` |
|
|
124
144
|
|
|
@@ -3,12 +3,12 @@ import calendar
|
|
|
3
3
|
import datetime
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
|
-
import uuid
|
|
7
6
|
from dataclasses import fields, is_dataclass
|
|
8
7
|
from os import PathLike
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
from typing import TypeVar, cast
|
|
11
10
|
from urllib.parse import urljoin
|
|
11
|
+
from uuid import UUID
|
|
12
12
|
|
|
13
13
|
import requests
|
|
14
14
|
from requests.adapters import HTTPAdapter
|
|
@@ -25,6 +25,7 @@ from cloudnet_api_client.containers import (
|
|
|
25
25
|
RawMetadata,
|
|
26
26
|
RawModelMetadata,
|
|
27
27
|
Site,
|
|
28
|
+
VersionMetadata,
|
|
28
29
|
)
|
|
29
30
|
from cloudnet_api_client.dl import download_files
|
|
30
31
|
|
|
@@ -42,6 +43,8 @@ class APIClient:
|
|
|
42
43
|
base_url: str = "https://cloudnet.fmi.fi/api/",
|
|
43
44
|
session: requests.Session | None = None,
|
|
44
45
|
) -> None:
|
|
46
|
+
if not base_url.endswith("/"):
|
|
47
|
+
base_url += "/"
|
|
45
48
|
self.base_url = base_url
|
|
46
49
|
self.session = session or _make_session()
|
|
47
50
|
|
|
@@ -74,7 +77,7 @@ class APIClient:
|
|
|
74
77
|
instrument_id=obj["instrument"]["id"],
|
|
75
78
|
model=obj["model"],
|
|
76
79
|
type=obj["type"],
|
|
77
|
-
uuid=
|
|
80
|
+
uuid=UUID(obj["uuid"]),
|
|
78
81
|
pid=obj["pid"],
|
|
79
82
|
owners=obj["owners"],
|
|
80
83
|
serial_number=obj["serialNumber"],
|
|
@@ -83,6 +86,31 @@ class APIClient:
|
|
|
83
86
|
for obj in res
|
|
84
87
|
]
|
|
85
88
|
|
|
89
|
+
def file(
|
|
90
|
+
self,
|
|
91
|
+
uuid: str | UUID,
|
|
92
|
+
) -> ProductMetadata:
|
|
93
|
+
res = self._get_response(f"files/{uuid}")
|
|
94
|
+
return _build_meta_objects(res)[0]
|
|
95
|
+
|
|
96
|
+
def versions(self, uuid: str | UUID) -> list[VersionMetadata]:
|
|
97
|
+
res = self._get_response(
|
|
98
|
+
f"files/{uuid}/versions",
|
|
99
|
+
{"properties": ["pid", "dvasId", "legacy", "size", "checksum"]},
|
|
100
|
+
)
|
|
101
|
+
return [
|
|
102
|
+
VersionMetadata(
|
|
103
|
+
uuid=UUID(obj["uuid"]),
|
|
104
|
+
created_at=_parse_datetime(obj["createdAt"]),
|
|
105
|
+
pid=obj["pid"],
|
|
106
|
+
dvas_id=obj["dvasId"],
|
|
107
|
+
legacy=obj["legacy"],
|
|
108
|
+
size=int(obj["size"]),
|
|
109
|
+
checksum=obj["checksum"],
|
|
110
|
+
)
|
|
111
|
+
for obj in res
|
|
112
|
+
]
|
|
113
|
+
|
|
86
114
|
def metadata(
|
|
87
115
|
self,
|
|
88
116
|
site_id: QueryParam = None,
|
|
@@ -105,6 +133,10 @@ class APIClient:
|
|
|
105
133
|
"product": product,
|
|
106
134
|
"showLegacy": show_legacy,
|
|
107
135
|
}
|
|
136
|
+
if show_legacy is not True:
|
|
137
|
+
# API shows legacy files with any value (even <False>)
|
|
138
|
+
del params["showLegacy"]
|
|
139
|
+
|
|
108
140
|
_add_date_params(
|
|
109
141
|
params, date, date_from, date_to, updated_at, updated_at_from, updated_at_to
|
|
110
142
|
)
|
|
@@ -125,7 +157,8 @@ class APIClient:
|
|
|
125
157
|
or (model_id is not None and (product is None or "model" in product))
|
|
126
158
|
):
|
|
127
159
|
for key in ("showLegacy", "product", "instrument", "instrumentPid"):
|
|
128
|
-
|
|
160
|
+
if key in params:
|
|
161
|
+
del params[key]
|
|
129
162
|
params["model"] = model_id
|
|
130
163
|
files_res += self._get_response("model-files", params)
|
|
131
164
|
|
|
@@ -418,7 +451,7 @@ def _build_meta_objects(res: list[dict]) -> list[ProductMetadata]:
|
|
|
418
451
|
created_at=_parse_datetime(obj["createdAt"]),
|
|
419
452
|
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
420
453
|
size=int(obj["size"]),
|
|
421
|
-
uuid=
|
|
454
|
+
uuid=UUID(obj["uuid"]),
|
|
422
455
|
site=_create_site_object(obj["site"]),
|
|
423
456
|
)
|
|
424
457
|
for obj in res
|
|
@@ -437,7 +470,7 @@ def _build_raw_meta_objects(res: list[dict]) -> list[RawMetadata]:
|
|
|
437
470
|
created_at=_parse_datetime(obj["createdAt"]),
|
|
438
471
|
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
439
472
|
size=int(obj["size"]),
|
|
440
|
-
uuid=
|
|
473
|
+
uuid=UUID(obj["uuid"]),
|
|
441
474
|
site=_create_site_object(obj["site"]),
|
|
442
475
|
)
|
|
443
476
|
for obj in res
|
|
@@ -456,7 +489,7 @@ def _build_raw_model_meta_objects(res: list[dict]) -> list[RawModelMetadata]:
|
|
|
456
489
|
created_at=_parse_datetime(obj["createdAt"]),
|
|
457
490
|
updated_at=_parse_datetime(obj["updatedAt"]),
|
|
458
491
|
size=int(obj["size"]),
|
|
459
|
-
uuid=
|
|
492
|
+
uuid=UUID(obj["uuid"]),
|
|
460
493
|
site=_create_site_object(obj["site"]),
|
|
461
494
|
)
|
|
462
495
|
for obj in res
|
|
@@ -498,10 +531,10 @@ def _create_site_object(metadata: dict) -> Site:
|
|
|
498
531
|
|
|
499
532
|
def _create_instrument_object(metadata: dict) -> Instrument:
|
|
500
533
|
return Instrument(
|
|
501
|
-
instrument_id=metadata
|
|
534
|
+
instrument_id=metadata.get("instrumentId"), # not in api/files/:uuid
|
|
502
535
|
model=metadata["model"],
|
|
503
536
|
type=metadata["type"],
|
|
504
|
-
uuid=
|
|
537
|
+
uuid=UUID(metadata["uuid"]),
|
|
505
538
|
pid=metadata["pid"],
|
|
506
539
|
owners=metadata["owners"],
|
|
507
540
|
serial_number=metadata["serialNumber"],
|
|
@@ -35,7 +35,7 @@ class Product:
|
|
|
35
35
|
|
|
36
36
|
@dataclass(frozen=True, slots=True)
|
|
37
37
|
class Instrument:
|
|
38
|
-
instrument_id: str # CLU internal identifier, e.g. "rpg-fmcw-94"
|
|
38
|
+
instrument_id: str | None # CLU internal identifier, e.g. "rpg-fmcw-94"
|
|
39
39
|
model: str # From ACTRIS Vocabulary, e.g. "RPG-FMCW-94 DP"
|
|
40
40
|
type: str # From ACTRIS Vocabulary, e.g. "Doppler non-scanning cloud radar"
|
|
41
41
|
name: str # e.g. "FMI RPG-FMCW-94 (Pallas)"
|
|
@@ -87,3 +87,23 @@ class ProductMetadata(Metadata):
|
|
|
87
87
|
instrument: Instrument | None
|
|
88
88
|
model: Model | None
|
|
89
89
|
volatile: bool
|
|
90
|
+
legacy: bool
|
|
91
|
+
pid: str
|
|
92
|
+
dvas_id: str | None
|
|
93
|
+
error_level: str | None
|
|
94
|
+
coverage: float
|
|
95
|
+
timeliness: str
|
|
96
|
+
format: str
|
|
97
|
+
start_time: datetime.datetime | None
|
|
98
|
+
stop_time: datetime.datetime | None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True, slots=True)
|
|
102
|
+
class VersionMetadata:
|
|
103
|
+
uuid: uuid.UUID
|
|
104
|
+
created_at: datetime.datetime
|
|
105
|
+
pid: str
|
|
106
|
+
checksum: str
|
|
107
|
+
legacy: bool
|
|
108
|
+
size: int
|
|
109
|
+
dvas_id: str | None
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import hashlib
|
|
2
3
|
from os import PathLike
|
|
3
4
|
from typing import Literal
|
|
@@ -7,13 +8,17 @@ def sha256sum(filename: str | PathLike) -> str:
|
|
|
7
8
|
return _calc_hash_sum(filename, "sha256")
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
def md5sum(filename: str | PathLike) -> str:
|
|
11
|
-
return _calc_hash_sum(filename, "md5")
|
|
11
|
+
def md5sum(filename: str | PathLike, is_base64: bool = False) -> str:
|
|
12
|
+
return _calc_hash_sum(filename, "md5", is_base64)
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
def _calc_hash_sum(
|
|
15
|
+
def _calc_hash_sum(
|
|
16
|
+
filename: str | PathLike, method: Literal["sha256", "md5"], is_base64: bool = False
|
|
17
|
+
) -> str:
|
|
15
18
|
hash_sum = getattr(hashlib, method)()
|
|
16
19
|
with open(filename, "rb") as f:
|
|
17
20
|
for byte_block in iter(lambda: f.read(4096), b""):
|
|
18
21
|
hash_sum.update(byte_block)
|
|
22
|
+
if is_base64:
|
|
23
|
+
return base64.b64encode(hash_sum.digest()).decode("utf-8")
|
|
19
24
|
return hash_sum.hexdigest()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.11.0"
|
|
@@ -22,12 +22,15 @@ dependencies = ["aiohttp", "numpy", "requests", "tqdm"]
|
|
|
22
22
|
dynamic = ["version"]
|
|
23
23
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
|
-
test = ["mypy", "pytest"]
|
|
25
|
+
test = ["mypy", "netCDF4", "pytest", "pytest-asyncio"]
|
|
26
26
|
dev = ["pre-commit", "release-version", "types-requests", "types-tqdm"]
|
|
27
27
|
|
|
28
28
|
[tool.hatch.version]
|
|
29
29
|
path = "cloudnet_api_client/version.py"
|
|
30
30
|
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
asyncio_mode = "auto"
|
|
33
|
+
|
|
31
34
|
[tool.release-version]
|
|
32
35
|
filename = "cloudnet_api_client/version.py"
|
|
33
36
|
pattern = ["__version__ = \"(?P<major>\\d+).(?P<minor>\\d+).(?P<patch>\\d+)\""]
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import NamedTuple
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
import netCDF4
|
|
8
|
+
import pytest
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from cloudnet_api_client import APIClient
|
|
12
|
+
from cloudnet_api_client.containers import (
|
|
13
|
+
Instrument,
|
|
14
|
+
Product,
|
|
15
|
+
ProductMetadata,
|
|
16
|
+
RawMetadata,
|
|
17
|
+
Site,
|
|
18
|
+
VersionMetadata,
|
|
19
|
+
)
|
|
20
|
+
from cloudnet_api_client.utils import md5sum, sha256sum
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RawFile(NamedTuple):
|
|
24
|
+
filename: str
|
|
25
|
+
site: str
|
|
26
|
+
instrument: str
|
|
27
|
+
date: str
|
|
28
|
+
pid: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class File(NamedTuple):
|
|
32
|
+
filename: str
|
|
33
|
+
legacy: bool
|
|
34
|
+
volatile: bool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture(scope="session")
|
|
38
|
+
def backend_url() -> str:
|
|
39
|
+
return os.getenv("BACKEND_URL", "http://localhost:3000")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture(scope="session")
|
|
43
|
+
def client(backend_url) -> APIClient:
|
|
44
|
+
return APIClient(base_url=f"{backend_url}/api/")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture(scope="session")
|
|
48
|
+
def data_path() -> Path:
|
|
49
|
+
return Path(__file__).parent / "data"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture(scope="session")
|
|
53
|
+
def files_raw() -> list[RawFile]:
|
|
54
|
+
return [
|
|
55
|
+
RawFile(
|
|
56
|
+
filename="20250801_Magurele_CHM170137_000.nc",
|
|
57
|
+
site="bucharest",
|
|
58
|
+
instrument="chm15k",
|
|
59
|
+
date="2025-08-01",
|
|
60
|
+
pid="https://hdl.handle.net/21.12132/3.c60c931fac9d43f0",
|
|
61
|
+
),
|
|
62
|
+
RawFile(
|
|
63
|
+
filename="20250808_Granada_CHM170119_0045_000.nc",
|
|
64
|
+
site="granada",
|
|
65
|
+
instrument="chm15k",
|
|
66
|
+
date="2025-08-08",
|
|
67
|
+
pid="https://hdl.handle.net/21.12132/3.77a75f3b32294855",
|
|
68
|
+
),
|
|
69
|
+
RawFile(
|
|
70
|
+
filename="20250803_JOYCE_WST_01m.dat",
|
|
71
|
+
site="juelich",
|
|
72
|
+
instrument="weather-station",
|
|
73
|
+
date="2025-08-01",
|
|
74
|
+
pid="https://hdl.handle.net/21.12132/3.726b3b29de1949cc",
|
|
75
|
+
),
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.fixture(scope="session")
|
|
80
|
+
def files_product() -> list[File]:
|
|
81
|
+
return [
|
|
82
|
+
File("20250814_bucharest_classification.nc", legacy=False, volatile=True),
|
|
83
|
+
File("20250808_hyytiala_iwc-Z-T-method.nc", legacy=False, volatile=False),
|
|
84
|
+
File("20140205_hyytiala_classification.nc", legacy=True, volatile=False),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
89
|
+
def submit_raw_files(backend_url, data_path, files_raw):
|
|
90
|
+
for file_meta in files_raw:
|
|
91
|
+
_submit_raw_file(backend_url, data_path, file_meta)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
95
|
+
def submit_product_files(backend_url: str, data_path: Path, files_product: list[File]):
|
|
96
|
+
for file_meta in files_product:
|
|
97
|
+
_submit_product_file(backend_url, data_path, file_meta)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestBasicMetadata:
|
|
101
|
+
def test_sites(self, client: APIClient):
|
|
102
|
+
sites = client.sites()
|
|
103
|
+
assert sites
|
|
104
|
+
assert isinstance(sites[0], Site)
|
|
105
|
+
|
|
106
|
+
def test_site_filter_cloudnet(self, client: APIClient):
|
|
107
|
+
sites = client.sites(type="cloudnet")
|
|
108
|
+
assert all("cloudnet" in site.type for site in sites)
|
|
109
|
+
|
|
110
|
+
def test_site_filter_hidden(self, client: APIClient):
|
|
111
|
+
sites = client.sites(type="hidden")
|
|
112
|
+
assert all("hidden" in site.type for site in sites)
|
|
113
|
+
assert all("cloudnet" not in site.type for site in sites)
|
|
114
|
+
|
|
115
|
+
def test_products(self, client: APIClient):
|
|
116
|
+
products = client.products()
|
|
117
|
+
assert products
|
|
118
|
+
assert isinstance(products[0], Product)
|
|
119
|
+
|
|
120
|
+
def test_product_type_filter(self, client: APIClient):
|
|
121
|
+
products = client.products("instrument")
|
|
122
|
+
assert all(product.type == ["instrument"] for product in products)
|
|
123
|
+
|
|
124
|
+
def test_product_type_filter_combo(self, client: APIClient):
|
|
125
|
+
products = client.products(["instrument", "geophysical"])
|
|
126
|
+
assert all(
|
|
127
|
+
product.type in [["instrument"], ["geophysical"]] for product in products
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def test_instruments(self, client: APIClient):
|
|
131
|
+
instruments = client.instruments()
|
|
132
|
+
assert instruments
|
|
133
|
+
assert isinstance(instruments[0], Instrument)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestDateParameterHandling:
|
|
137
|
+
"""Test various date parameter formats and edge cases."""
|
|
138
|
+
|
|
139
|
+
def test_date_string_formats(self, client: APIClient):
|
|
140
|
+
meta1 = client.raw_metadata(date="2025-08-01")
|
|
141
|
+
meta2 = client.raw_metadata(date="2025-8-1")
|
|
142
|
+
assert len(meta1) == len(meta2)
|
|
143
|
+
|
|
144
|
+
def test_date_object_parameter(self, client: APIClient):
|
|
145
|
+
date_obj = datetime.date(2025, 8, 1)
|
|
146
|
+
meta = client.raw_metadata(date=date_obj)
|
|
147
|
+
assert len(meta) >= 0
|
|
148
|
+
|
|
149
|
+
def test_datetime_parameter(self, client: APIClient):
|
|
150
|
+
datetime_obj = datetime.datetime(2025, 8, 1, 12, 30)
|
|
151
|
+
meta = client.raw_metadata(updated_at=datetime_obj)
|
|
152
|
+
assert len(meta) >= 0
|
|
153
|
+
|
|
154
|
+
def test_invalid_date_format(self, client: APIClient):
|
|
155
|
+
with pytest.raises(ValueError):
|
|
156
|
+
client.raw_metadata(date="invalid-date")
|
|
157
|
+
|
|
158
|
+
def test_date_range_validation(self, client: APIClient):
|
|
159
|
+
# date_from > date_to should return empty results
|
|
160
|
+
meta = client.raw_metadata(date_from="2025-08-10", date_to="2025-08-01")
|
|
161
|
+
assert len(meta) == 0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestRawMetadata:
|
|
165
|
+
def test_filter_by_site_and_date(self, client: APIClient):
|
|
166
|
+
meta = client.raw_metadata(site_id="bucharest", date="2025-08-01")
|
|
167
|
+
assert len(meta) == 1
|
|
168
|
+
assert isinstance(meta[0], RawMetadata)
|
|
169
|
+
|
|
170
|
+
def test_filter_by_date_only(self, client: APIClient):
|
|
171
|
+
meta = client.raw_metadata(date="2025-08-08")
|
|
172
|
+
assert len(meta) == 1
|
|
173
|
+
|
|
174
|
+
def test_filter_by_instrument_pid(self, client: APIClient):
|
|
175
|
+
pid = "https://hdl.handle.net/21.12132/3.77a75f3b32294855"
|
|
176
|
+
meta = client.raw_metadata(instrument_pid=pid)
|
|
177
|
+
assert len(meta) == 1
|
|
178
|
+
|
|
179
|
+
def test_filter_by_instrument_pid_no_match(self, client: APIClient):
|
|
180
|
+
pid = "https://hdl.handle.net/21.12132/3.77a75f3b32294855"
|
|
181
|
+
meta = client.raw_metadata(instrument_pid=pid, date="2022-01-01")
|
|
182
|
+
assert len(meta) == 0
|
|
183
|
+
|
|
184
|
+
def test_filter_by_date_range_from(self, client: APIClient):
|
|
185
|
+
meta = client.raw_metadata(date_from="2025-08-01")
|
|
186
|
+
assert len(meta) == 3
|
|
187
|
+
|
|
188
|
+
def test_filter_by_date_range_inclusive(self, client: APIClient):
|
|
189
|
+
meta = client.raw_metadata(date_from="2025-08-01", date_to="2025-08-08")
|
|
190
|
+
assert len(meta) == 3
|
|
191
|
+
|
|
192
|
+
def test_filter_by_date_range_exclusive(self, client: APIClient):
|
|
193
|
+
meta = client.raw_metadata(date_from="2025-08-01", date_to="2025-08-07")
|
|
194
|
+
assert len(meta) == 2
|
|
195
|
+
|
|
196
|
+
def test_filter_by_filename_prefix(self, client: APIClient):
|
|
197
|
+
meta = client.raw_metadata(filename_prefix="20250801")
|
|
198
|
+
assert len(meta) == 1
|
|
199
|
+
|
|
200
|
+
def test_filter_by_filename_suffix(self, client: APIClient):
|
|
201
|
+
meta = client.raw_metadata(filename_suffix="000.nc")
|
|
202
|
+
assert len(meta) == 2
|
|
203
|
+
|
|
204
|
+
def test_filter_by_instrument_id(self, client: APIClient):
|
|
205
|
+
meta = client.raw_metadata(instrument_id="weather-station")
|
|
206
|
+
assert len(meta) == 1
|
|
207
|
+
|
|
208
|
+
def test_instrument_id_vs_pid_exclusivity(self, client: APIClient):
|
|
209
|
+
meta1 = client.raw_metadata(instrument_id="chm15k")
|
|
210
|
+
pid = "https://hdl.handle.net/21.12132/3.c60c931fac9d43f0"
|
|
211
|
+
meta2 = client.raw_metadata(instrument_pid=pid)
|
|
212
|
+
assert len(meta1) > 1 # Multiple chm15k files
|
|
213
|
+
assert len(meta2) == 1 # Specific PID
|
|
214
|
+
|
|
215
|
+
def test_malformed_pid(self, client: APIClient):
|
|
216
|
+
meta = client.raw_metadata(instrument_pid="not-a-valid-pid")
|
|
217
|
+
assert len(meta) == 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestDownloadingFunctionality:
|
|
221
|
+
def test_downloading(self, client: APIClient, tmp_path: Path):
|
|
222
|
+
meta = client.raw_metadata(date_from="2025-08-01")
|
|
223
|
+
assert len(meta) == 3
|
|
224
|
+
paths = client.download(meta, output_directory=tmp_path, progress=False)
|
|
225
|
+
assert len(paths) == 3
|
|
226
|
+
for path in paths:
|
|
227
|
+
assert path.exists()
|
|
228
|
+
|
|
229
|
+
def test_download_with_custom_directory(self, client: APIClient, tmp_path: Path):
|
|
230
|
+
meta = client.raw_metadata(date="2025-08-01", site_id="bucharest")
|
|
231
|
+
custom_dir = tmp_path / "custom" / "nested"
|
|
232
|
+
paths = client.download(meta, output_directory=custom_dir, progress=False)
|
|
233
|
+
assert len(paths) == 1
|
|
234
|
+
assert paths[0].parent == custom_dir
|
|
235
|
+
assert paths[0].exists()
|
|
236
|
+
|
|
237
|
+
def test_download_existing_file_skip(self, client: APIClient, tmp_path: Path):
|
|
238
|
+
meta = client.raw_metadata(date="2025-08-01", site_id="bucharest")
|
|
239
|
+
paths1 = client.download(meta, output_directory=tmp_path, progress=False)
|
|
240
|
+
original_size = paths1[0].stat().st_size
|
|
241
|
+
paths2 = client.download(meta, output_directory=tmp_path, progress=False)
|
|
242
|
+
assert paths1 == paths2
|
|
243
|
+
assert paths2[0].stat().st_size == original_size
|
|
244
|
+
|
|
245
|
+
async def test_async_download(self, client: APIClient, tmp_path: Path):
|
|
246
|
+
meta = client.raw_metadata(date_from="2025-08-01")
|
|
247
|
+
assert len(meta) == 3
|
|
248
|
+
paths = await client.adownload(meta, output_directory=tmp_path, progress=False)
|
|
249
|
+
assert len(paths) == 3
|
|
250
|
+
for path in paths:
|
|
251
|
+
assert path.exists()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class TestProductMeta:
|
|
255
|
+
def test_file_route(self, client: APIClient):
|
|
256
|
+
uuid = "8dcc865c-6920-49ce-a627-de045ec896e8"
|
|
257
|
+
meta = client.file(uuid)
|
|
258
|
+
assert isinstance(meta, ProductMetadata)
|
|
259
|
+
assert str(meta.uuid) == uuid
|
|
260
|
+
|
|
261
|
+
def test_versions_route(self, client: APIClient):
|
|
262
|
+
uuid = "8dcc865c-6920-49ce-a627-de045ec896e8"
|
|
263
|
+
meta = client.versions(uuid)
|
|
264
|
+
assert len(meta) == 1
|
|
265
|
+
assert isinstance(meta[0], VersionMetadata)
|
|
266
|
+
assert str(meta[0].uuid) == uuid
|
|
267
|
+
|
|
268
|
+
def test_product_option(self, client: APIClient):
|
|
269
|
+
meta = client.metadata(site_id="hyytiala", product="iwc")
|
|
270
|
+
assert len(meta) == 1
|
|
271
|
+
|
|
272
|
+
def test_show_legacy_option(self, client: APIClient):
|
|
273
|
+
meta = client.metadata(site_id="hyytiala", date="2014-02-05")
|
|
274
|
+
assert len(meta) == 0
|
|
275
|
+
meta = client.metadata(site_id="hyytiala", date="2014-02-05", show_legacy=True)
|
|
276
|
+
assert len(meta) == 1
|
|
277
|
+
|
|
278
|
+
def test_downloading(self, client: APIClient, tmp_path: Path):
|
|
279
|
+
meta = client.metadata(date_from="2025-08-01")
|
|
280
|
+
assert len(meta) == 2
|
|
281
|
+
paths = client.download(meta, output_directory=tmp_path, progress=False)
|
|
282
|
+
assert len(paths) == 2
|
|
283
|
+
for path in paths:
|
|
284
|
+
assert path.exists()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TestFilterCombinations:
|
|
288
|
+
def test_multiple_filters_combined(self, client: APIClient):
|
|
289
|
+
meta = client.raw_metadata(
|
|
290
|
+
site_id="bucharest",
|
|
291
|
+
instrument_id="chm15k",
|
|
292
|
+
date_from="2025-08-01",
|
|
293
|
+
date_to="2025-08-01",
|
|
294
|
+
)
|
|
295
|
+
assert len(meta) == 1
|
|
296
|
+
assert meta[0].site.id == "bucharest"
|
|
297
|
+
|
|
298
|
+
def test_contradictory_filters(self, client: APIClient):
|
|
299
|
+
meta = client.raw_metadata(site_id="bucharest", instrument_id="weather-station")
|
|
300
|
+
assert len(meta) == 0
|
|
301
|
+
|
|
302
|
+
def test_partial_filename_matches(self, client: APIClient):
|
|
303
|
+
meta = client.raw_metadata(filename_prefix="2025", filename_suffix=".nc")
|
|
304
|
+
assert len(meta) == 2
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _submit_product_file(backend_url: str, data_path: Path, meta: File):
|
|
308
|
+
_date, site_id, product = meta.filename.removesuffix(".nc").split("_")
|
|
309
|
+
full_path = data_path / meta.filename
|
|
310
|
+
headers = {"content-md5": md5sum(full_path, is_base64=True)}
|
|
311
|
+
bucket = f"cloudnet-product{'-volatile' if meta.volatile else ''}"
|
|
312
|
+
url = f"http://localhost:5900/{bucket}/{meta.filename}"
|
|
313
|
+
with open(full_path, "rb") as f:
|
|
314
|
+
res = requests.put(url, data=f, auth=("test", "test"), headers=headers)
|
|
315
|
+
res.raise_for_status()
|
|
316
|
+
file_info = {
|
|
317
|
+
"version": res.json().get("version", ""),
|
|
318
|
+
"size": int(res.json()["size"]),
|
|
319
|
+
}
|
|
320
|
+
with netCDF4.Dataset(full_path, "r") as nc:
|
|
321
|
+
year, month, day = str(nc.year), str(nc.month).zfill(2), str(nc.day).zfill(2)
|
|
322
|
+
payload = {
|
|
323
|
+
"product": getattr(nc, "cloudnet_file_type", product),
|
|
324
|
+
"site": site_id,
|
|
325
|
+
"measurementDate": f"{year}-{month}-{day}",
|
|
326
|
+
"format": "HDF5 (NetCDF4)",
|
|
327
|
+
"checksum": sha256sum(full_path),
|
|
328
|
+
"volatile": meta.volatile,
|
|
329
|
+
"legacy": meta.legacy,
|
|
330
|
+
"uuid": str(UUID(nc.file_uuid)),
|
|
331
|
+
"pid": nc.pid,
|
|
332
|
+
**file_info,
|
|
333
|
+
}
|
|
334
|
+
url = f"{backend_url}/files/{meta.filename}"
|
|
335
|
+
res = requests.put(url, json=payload)
|
|
336
|
+
if res.status_code == 403:
|
|
337
|
+
return
|
|
338
|
+
res.raise_for_status()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _submit_raw_file(backend_url: str, data_path: Path, meta: RawFile):
|
|
342
|
+
auth = ("admin", "admin")
|
|
343
|
+
file_path = data_path / meta.filename
|
|
344
|
+
checksum = md5sum(file_path)
|
|
345
|
+
metadata = {
|
|
346
|
+
"filename": meta.filename,
|
|
347
|
+
"checksum": checksum,
|
|
348
|
+
"site": meta.site,
|
|
349
|
+
"instrument": meta.instrument,
|
|
350
|
+
"measurementDate": meta.date,
|
|
351
|
+
"instrumentPid": meta.pid,
|
|
352
|
+
}
|
|
353
|
+
res = requests.post(f"{backend_url}/upload/metadata/", json=metadata, auth=auth)
|
|
354
|
+
if res.status_code == 409:
|
|
355
|
+
return
|
|
356
|
+
res.raise_for_status()
|
|
357
|
+
with open(file_path, "rb") as f:
|
|
358
|
+
res = requests.put(f"{backend_url}/upload/data/{checksum}", data=f, auth=auth)
|
|
359
|
+
res.raise_for_status()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.10.0"
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import hashlib
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import requests
|
|
5
|
-
|
|
6
|
-
from cloudnet_api_client import APIClient
|
|
7
|
-
from cloudnet_api_client.containers import Instrument, Product, RawMetadata, Site
|
|
8
|
-
|
|
9
|
-
BACKEND_URL = "http://localhost:3000"
|
|
10
|
-
DATA_PATH = Path(__file__).parent / "data"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class TestFixtures:
|
|
14
|
-
def setup_method(self):
|
|
15
|
-
self.client = APIClient(base_url=f"{BACKEND_URL}/api/")
|
|
16
|
-
|
|
17
|
-
def test_sites(self):
|
|
18
|
-
sites = self.client.sites()
|
|
19
|
-
assert isinstance(sites[0], Site)
|
|
20
|
-
|
|
21
|
-
def test_site_filter_cloudnet(self):
|
|
22
|
-
sites = self.client.sites(type="cloudnet")
|
|
23
|
-
assert all("cloudnet" in site.type for site in sites)
|
|
24
|
-
|
|
25
|
-
def test_site_filter_hidden(self):
|
|
26
|
-
sites = self.client.sites(type="hidden")
|
|
27
|
-
assert all("hidden" in site.type for site in sites)
|
|
28
|
-
assert all("cloudnet" not in site.type for site in sites)
|
|
29
|
-
|
|
30
|
-
def test_products(self):
|
|
31
|
-
products = self.client.products()
|
|
32
|
-
assert isinstance(products[0], Product)
|
|
33
|
-
|
|
34
|
-
def test_instruments(self):
|
|
35
|
-
instruments = self.client.instruments()
|
|
36
|
-
assert isinstance(instruments[0], Instrument)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class TestWithRawFiles:
|
|
40
|
-
def setup_method(self):
|
|
41
|
-
self.client = APIClient(base_url=f"{BACKEND_URL}/api/")
|
|
42
|
-
metadata_list = [
|
|
43
|
-
(
|
|
44
|
-
"20250801_Magurele_CHM170137_000.nc",
|
|
45
|
-
"bucharest",
|
|
46
|
-
"chm15k",
|
|
47
|
-
"2025-08-01",
|
|
48
|
-
"https://hdl.handle.net/21.12132/3.c60c931fac9d43f0",
|
|
49
|
-
),
|
|
50
|
-
(
|
|
51
|
-
"20250808_Granada_CHM170119_0045_000.nc",
|
|
52
|
-
"granada",
|
|
53
|
-
"chm15k",
|
|
54
|
-
"2025-08-08",
|
|
55
|
-
"https://hdl.handle.net/21.12132/3.77a75f3b32294855",
|
|
56
|
-
),
|
|
57
|
-
(
|
|
58
|
-
"20250803_JOYCE_WST_01m.dat",
|
|
59
|
-
"juelich",
|
|
60
|
-
"weather-station",
|
|
61
|
-
"2025-08-01",
|
|
62
|
-
"https://hdl.handle.net/21.12132/3.726b3b29de1949cc",
|
|
63
|
-
),
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
for item in metadata_list:
|
|
67
|
-
_submit_file(*item)
|
|
68
|
-
|
|
69
|
-
def test_raw_metadata_1(self):
|
|
70
|
-
meta = self.client.raw_metadata(site_id="bucharest", date="2025-08-01")
|
|
71
|
-
assert isinstance(meta, list)
|
|
72
|
-
assert len(meta) == 1
|
|
73
|
-
assert isinstance(meta[0], RawMetadata)
|
|
74
|
-
|
|
75
|
-
def test_raw_metadata_2(self):
|
|
76
|
-
meta = self.client.raw_metadata(date="2025-08-08")
|
|
77
|
-
assert len(meta) == 1
|
|
78
|
-
|
|
79
|
-
def test_raw_metadata_3(self):
|
|
80
|
-
pid = "https://hdl.handle.net/21.12132/3.77a75f3b32294855"
|
|
81
|
-
meta = self.client.raw_metadata(instrument_pid=pid)
|
|
82
|
-
assert len(meta) == 1
|
|
83
|
-
|
|
84
|
-
def test_raw_metadata_4(self):
|
|
85
|
-
pid = "https://hdl.handle.net/21.12132/3.77a75f3b32294855"
|
|
86
|
-
meta = self.client.raw_metadata(instrument_pid=pid, date="2022-01-01")
|
|
87
|
-
assert len(meta) == 0
|
|
88
|
-
|
|
89
|
-
def test_raw_metadata_5(self):
|
|
90
|
-
meta = self.client.raw_metadata(date_from="2025-08-01")
|
|
91
|
-
assert len(meta) == 3
|
|
92
|
-
|
|
93
|
-
def test_raw_metadata_6(self):
|
|
94
|
-
meta = self.client.raw_metadata(date_from="2025-08-01", date_to="2025-08-08")
|
|
95
|
-
assert len(meta) == 3
|
|
96
|
-
|
|
97
|
-
def test_raw_metadata_7(self):
|
|
98
|
-
meta = self.client.raw_metadata(date_from="2025-08-01", date_to="2025-08-07")
|
|
99
|
-
assert len(meta) == 2
|
|
100
|
-
|
|
101
|
-
def test_raw_metadata_8(self):
|
|
102
|
-
meta = self.client.raw_metadata(filename_prefix="20250801")
|
|
103
|
-
assert len(meta) == 1
|
|
104
|
-
|
|
105
|
-
def test_raw_metadata_9(self):
|
|
106
|
-
meta = self.client.raw_metadata(filename_suffix="000.nc")
|
|
107
|
-
assert len(meta) == 2
|
|
108
|
-
|
|
109
|
-
def test_raw_metadata_10(self):
|
|
110
|
-
meta = self.client.raw_metadata(instrument_id="weather-station")
|
|
111
|
-
assert len(meta) == 1
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _submit_file(filename: str, site: str, instrument: str, date: str, pid: str):
|
|
115
|
-
auth = ("admin", "admin")
|
|
116
|
-
file_path = DATA_PATH / filename
|
|
117
|
-
|
|
118
|
-
with open(file_path, "rb") as f:
|
|
119
|
-
checksum = hashlib.md5(f.read()).hexdigest()
|
|
120
|
-
|
|
121
|
-
metadata = {
|
|
122
|
-
"filename": filename,
|
|
123
|
-
"checksum": checksum,
|
|
124
|
-
"site": site,
|
|
125
|
-
"instrument": instrument,
|
|
126
|
-
"measurementDate": date,
|
|
127
|
-
"instrumentPid": pid,
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
res = requests.post(f"{BACKEND_URL}/upload/metadata/", json=metadata, auth=auth)
|
|
131
|
-
if res.status_code not in (200, 409):
|
|
132
|
-
res.raise_for_status()
|
|
133
|
-
|
|
134
|
-
if res.status_code == 200:
|
|
135
|
-
with open(file_path, "rb") as f:
|
|
136
|
-
res = requests.put(
|
|
137
|
-
f"{BACKEND_URL}/upload/data/{checksum}", data=f, auth=auth
|
|
138
|
-
)
|
|
139
|
-
res.raise_for_status()
|
|
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.10.0 → cloudnet_api_client-0.11.0}/tests/data/20250803_JOYCE_WST_01m.dat
RENAMED
|
File without changes
|
|
File without changes
|