clarity-api-sdk-python 0.3.38__tar.gz → 0.4.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.
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/PKG-INFO +1 -1
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/pyproject.toml +1 -1
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/clarity_api_sdk_python.egg-info/PKG-INFO +1 -1
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/clarity_api_sdk_python.egg-info/SOURCES.txt +2 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/api/sonar_wiz_api.py +132 -8
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/api/sonar_wiz_async_api.py +138 -8
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/cli/main.py +31 -6
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/raw_file_device_mapping.py +43 -2
- clarity_api_sdk_python-0.4.1/src/cti/model/user_layer.py +125 -0
- clarity_api_sdk_python-0.4.1/tests/test_raw_file_device_mapping_model.py +110 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/tests/test_sdk_async_methods.py +150 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/tests/test_sdk_methods.py +123 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/README.md +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/setup.cfg +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/clarity_api_sdk_python.egg-info/dependency_links.txt +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/clarity_api_sdk_python.egg-info/entry_points.txt +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/clarity_api_sdk_python.egg-info/requires.txt +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/clarity_api_sdk_python.egg-info/top_level.txt +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/__init__.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/api/__init__.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/api/async_client.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/api/client.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/api/session.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/cli/__init__.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/cli/__main__.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/cli/client.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/logger/__init__.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/logger/logger.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/main.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/main_api.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/__init__.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/altitude_source.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/attitude_source.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/deferred_object_deletion.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/depth_source.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/device.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/device_type.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/final_product.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/hierarchy.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/layback_algorithm.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/layback_source.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/layback_type.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/organization.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/platform.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/platform_type.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/position_source.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/processing_job.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/processing_log.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/project.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/projection_option.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/raw_file.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/raw_file_configuration.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/raw_file_state.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/s3.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/sidescan_ping_source.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/source.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/survey.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/target.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/tow_system.py +0 -0
- {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clarity-api-sdk-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A Python SDK to connect to the CTI Clarity API server.
|
|
5
5
|
Author-email: "Chesapeake Technology Inc." <support@chesapeaketech.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/chesapeake-tech/clarity-api-sdk-python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clarity-api-sdk-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A Python SDK to connect to the CTI Clarity API server.
|
|
5
5
|
Author-email: "Chesapeake Technology Inc." <support@chesapeaketech.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/chesapeake-tech/clarity-api-sdk-python
|
|
@@ -51,6 +51,8 @@ src/cti/model/source.py
|
|
|
51
51
|
src/cti/model/survey.py
|
|
52
52
|
src/cti/model/target.py
|
|
53
53
|
src/cti/model/tow_system.py
|
|
54
|
+
src/cti/model/user_layer.py
|
|
54
55
|
tests/test_cli.py
|
|
56
|
+
tests/test_raw_file_device_mapping_model.py
|
|
55
57
|
tests/test_sdk_async_methods.py
|
|
56
58
|
tests/test_sdk_methods.py
|
|
@@ -99,6 +99,10 @@ from cti.model.target import TargetUpdate
|
|
|
99
99
|
from cti.model.tow_system import TowSystem
|
|
100
100
|
from cti.model.tow_system import TowSystemCreate
|
|
101
101
|
from cti.model.tow_system import TowSystemUpdate
|
|
102
|
+
from cti.model.user_layer import UserLayer
|
|
103
|
+
from cti.model.user_layer import UserLayerCreate
|
|
104
|
+
from cti.model.user_layer import UserLayerUpdate
|
|
105
|
+
from cti.model.user_layer import UserLayerWithUpload
|
|
102
106
|
|
|
103
107
|
from .client import ClarityApiClient
|
|
104
108
|
|
|
@@ -353,13 +357,37 @@ class SonarWizApi:
|
|
|
353
357
|
response.raise_for_status()
|
|
354
358
|
return RawFileDeviceMapping.model_validate(response.json())
|
|
355
359
|
|
|
356
|
-
def
|
|
357
|
-
|
|
360
|
+
def get_raw_file_device_mappings(
|
|
361
|
+
self,
|
|
362
|
+
*,
|
|
363
|
+
raw_file_id: UUID | str | None = None,
|
|
364
|
+
device_id: UUID | str | None = None,
|
|
365
|
+
) -> list[RawFileDeviceMapping]:
|
|
366
|
+
"""Get raw file device mappings filtered by raw file or device.
|
|
367
|
+
|
|
368
|
+
At least one of ``raw_file_id`` / ``device_id`` must be provided —
|
|
369
|
+
the server requires a filter and returns 400 otherwise.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
raw_file_id: Match only mappings for this raw file.
|
|
373
|
+
device_id: Match only mappings for this device.
|
|
358
374
|
|
|
359
375
|
Returns:
|
|
360
|
-
List of raw file device
|
|
376
|
+
List of matching raw file device mappings (empty if none match).
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
ValueError: If neither ``raw_file_id`` nor ``device_id`` is given.
|
|
361
380
|
"""
|
|
362
|
-
|
|
381
|
+
if raw_file_id is None and device_id is None:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
"At least one of raw_file_id or device_id must be provided."
|
|
384
|
+
)
|
|
385
|
+
params: dict[str, str] = {}
|
|
386
|
+
if raw_file_id is not None:
|
|
387
|
+
params["raw_file_id"] = str(raw_file_id)
|
|
388
|
+
if device_id is not None:
|
|
389
|
+
params["device_id"] = str(device_id)
|
|
390
|
+
response = self._client.get("/api/v1/raw-file-device-mappings", params=params)
|
|
363
391
|
response.raise_for_status()
|
|
364
392
|
return [RawFileDeviceMapping.model_validate(item) for item in response.json()]
|
|
365
393
|
|
|
@@ -1975,13 +2003,34 @@ class SonarWizApi:
|
|
|
1975
2003
|
response.raise_for_status()
|
|
1976
2004
|
return ProcessingLog.model_validate(response.json())
|
|
1977
2005
|
|
|
1978
|
-
def list_processing_logs(
|
|
1979
|
-
|
|
2006
|
+
def list_processing_logs(
|
|
2007
|
+
self,
|
|
2008
|
+
*,
|
|
2009
|
+
raw_file_id: UUID | str | None = None,
|
|
2010
|
+
survey_id: UUID | str | None = None,
|
|
2011
|
+
processing_step: str | None = None,
|
|
2012
|
+
) -> list[ProcessingLog]:
|
|
2013
|
+
"""List processing logs, optionally filtered.
|
|
2014
|
+
|
|
2015
|
+
Filters compose: each provided filter narrows the result set.
|
|
2016
|
+
|
|
2017
|
+
Args:
|
|
2018
|
+
raw_file_id: Match only logs for this raw file.
|
|
2019
|
+
survey_id: Match only logs for this survey.
|
|
2020
|
+
processing_step: Match only logs with this processing step
|
|
2021
|
+
(e.g. ``"scan"``, ``"ingest"``, ``"products"``).
|
|
1980
2022
|
|
|
1981
2023
|
Returns:
|
|
1982
|
-
List of processing
|
|
2024
|
+
List of matching processing logs (empty list if none match).
|
|
1983
2025
|
"""
|
|
1984
|
-
|
|
2026
|
+
params: dict[str, str] = {}
|
|
2027
|
+
if raw_file_id is not None:
|
|
2028
|
+
params["raw_file_id"] = str(raw_file_id)
|
|
2029
|
+
if survey_id is not None:
|
|
2030
|
+
params["survey_id"] = str(survey_id)
|
|
2031
|
+
if processing_step is not None:
|
|
2032
|
+
params["processing_step"] = processing_step
|
|
2033
|
+
response = self._client.get("/api/v1/processing-logs", params=params or None)
|
|
1985
2034
|
response.raise_for_status()
|
|
1986
2035
|
return [ProcessingLog.model_validate(item) for item in response.json()]
|
|
1987
2036
|
|
|
@@ -2004,6 +2053,81 @@ class SonarWizApi:
|
|
|
2004
2053
|
response.raise_for_status()
|
|
2005
2054
|
return ProcessingLog.model_validate(response.json())
|
|
2006
2055
|
|
|
2056
|
+
# ── User Layers ──────────────────────────────────────────────────────
|
|
2057
|
+
|
|
2058
|
+
def create_user_layer(self, user_layer: UserLayerCreate) -> UserLayerWithUpload:
|
|
2059
|
+
"""Create a new user layer and initiate upload.
|
|
2060
|
+
|
|
2061
|
+
Args:
|
|
2062
|
+
user_layer: User layer creation data.
|
|
2063
|
+
|
|
2064
|
+
Returns:
|
|
2065
|
+
Created user layer instance with upload information.
|
|
2066
|
+
"""
|
|
2067
|
+
response = self._client.post(
|
|
2068
|
+
"/api/v1/user-layers/upload",
|
|
2069
|
+
json=user_layer.model_dump(mode="json"),
|
|
2070
|
+
)
|
|
2071
|
+
response.raise_for_status()
|
|
2072
|
+
return UserLayerWithUpload.model_validate(response.json())
|
|
2073
|
+
|
|
2074
|
+
def get_user_layer(self, user_layer_id: UUID) -> UserLayer:
|
|
2075
|
+
"""Fetch a user layer by ID.
|
|
2076
|
+
|
|
2077
|
+
Args:
|
|
2078
|
+
user_layer_id: User layer UUID to fetch.
|
|
2079
|
+
|
|
2080
|
+
Returns:
|
|
2081
|
+
UserLayer instance.
|
|
2082
|
+
"""
|
|
2083
|
+
response = self._client.get(f"/api/v1/user-layers/{user_layer_id}")
|
|
2084
|
+
response.raise_for_status()
|
|
2085
|
+
return UserLayer.model_validate(response.json())
|
|
2086
|
+
|
|
2087
|
+
def list_user_layers(self, project_id: UUID) -> list[UserLayer]:
|
|
2088
|
+
"""List all user layers for a project.
|
|
2089
|
+
|
|
2090
|
+
Args:
|
|
2091
|
+
project_id: Project UUID to filter by.
|
|
2092
|
+
|
|
2093
|
+
Returns:
|
|
2094
|
+
List of user layer instances.
|
|
2095
|
+
"""
|
|
2096
|
+
response = self._client.get(
|
|
2097
|
+
"/api/v1/user-layers",
|
|
2098
|
+
params={"project_id": str(project_id)},
|
|
2099
|
+
)
|
|
2100
|
+
response.raise_for_status()
|
|
2101
|
+
return [UserLayer.model_validate(item) for item in response.json()]
|
|
2102
|
+
|
|
2103
|
+
def update_user_layer(
|
|
2104
|
+
self, user_layer_id: UUID, user_layer: UserLayerUpdate
|
|
2105
|
+
) -> UserLayer:
|
|
2106
|
+
"""Update a user layer.
|
|
2107
|
+
|
|
2108
|
+
Args:
|
|
2109
|
+
user_layer_id: User layer UUID to update.
|
|
2110
|
+
user_layer: User layer update data.
|
|
2111
|
+
|
|
2112
|
+
Returns:
|
|
2113
|
+
Updated user layer instance.
|
|
2114
|
+
"""
|
|
2115
|
+
response = self._client.patch(
|
|
2116
|
+
f"/api/v1/user-layers/{user_layer_id}",
|
|
2117
|
+
json=user_layer.model_dump(mode="json", exclude_none=True),
|
|
2118
|
+
)
|
|
2119
|
+
response.raise_for_status()
|
|
2120
|
+
return UserLayer.model_validate(response.json())
|
|
2121
|
+
|
|
2122
|
+
def delete_user_layer(self, user_layer_id: UUID) -> None:
|
|
2123
|
+
"""Delete a user layer.
|
|
2124
|
+
|
|
2125
|
+
Args:
|
|
2126
|
+
user_layer_id: User layer UUID to delete.
|
|
2127
|
+
"""
|
|
2128
|
+
response = self._client.delete(f"/api/v1/user-layers/{user_layer_id}")
|
|
2129
|
+
response.raise_for_status()
|
|
2130
|
+
|
|
2007
2131
|
# ── Processing Jobs ──────────────────────────────────────────────────
|
|
2008
2132
|
|
|
2009
2133
|
def create_job(self, job_data: ProcessingJobCreate) -> ProcessingJob:
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/api/sonar_wiz_async_api.py
RENAMED
|
@@ -97,6 +97,10 @@ from cti.model.target import TargetUpdate
|
|
|
97
97
|
from cti.model.tow_system import TowSystem
|
|
98
98
|
from cti.model.tow_system import TowSystemCreate
|
|
99
99
|
from cti.model.tow_system import TowSystemUpdate
|
|
100
|
+
from cti.model.user_layer import UserLayer
|
|
101
|
+
from cti.model.user_layer import UserLayerCreate
|
|
102
|
+
from cti.model.user_layer import UserLayerUpdate
|
|
103
|
+
from cti.model.user_layer import UserLayerWithUpload
|
|
100
104
|
|
|
101
105
|
from .async_client import ClarityApiAsyncClient
|
|
102
106
|
|
|
@@ -353,13 +357,39 @@ class SonarWizAsyncApi:
|
|
|
353
357
|
response.raise_for_status()
|
|
354
358
|
return RawFileDeviceMapping.model_validate(response.json())
|
|
355
359
|
|
|
356
|
-
async def
|
|
357
|
-
|
|
360
|
+
async def get_raw_file_device_mappings(
|
|
361
|
+
self,
|
|
362
|
+
*,
|
|
363
|
+
raw_file_id: UUID | str | None = None,
|
|
364
|
+
device_id: UUID | str | None = None,
|
|
365
|
+
) -> list[RawFileDeviceMapping]:
|
|
366
|
+
"""Get raw file device mappings filtered by raw file or device.
|
|
367
|
+
|
|
368
|
+
At least one of ``raw_file_id`` / ``device_id`` must be provided —
|
|
369
|
+
the server requires a filter and returns 400 otherwise.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
raw_file_id: Match only mappings for this raw file.
|
|
373
|
+
device_id: Match only mappings for this device.
|
|
358
374
|
|
|
359
375
|
Returns:
|
|
360
|
-
List of raw file device
|
|
376
|
+
List of matching raw file device mappings (empty if none match).
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
ValueError: If neither ``raw_file_id`` nor ``device_id`` is given.
|
|
361
380
|
"""
|
|
362
|
-
|
|
381
|
+
if raw_file_id is None and device_id is None:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
"At least one of raw_file_id or device_id must be provided."
|
|
384
|
+
)
|
|
385
|
+
params: dict[str, str] = {}
|
|
386
|
+
if raw_file_id is not None:
|
|
387
|
+
params["raw_file_id"] = str(raw_file_id)
|
|
388
|
+
if device_id is not None:
|
|
389
|
+
params["device_id"] = str(device_id)
|
|
390
|
+
response = await self._client.get(
|
|
391
|
+
"/api/v1/raw-file-device-mappings", params=params
|
|
392
|
+
)
|
|
363
393
|
response.raise_for_status()
|
|
364
394
|
return [RawFileDeviceMapping.model_validate(item) for item in response.json()]
|
|
365
395
|
|
|
@@ -2023,13 +2053,36 @@ class SonarWizAsyncApi:
|
|
|
2023
2053
|
response.raise_for_status()
|
|
2024
2054
|
return ProcessingLog.model_validate(response.json())
|
|
2025
2055
|
|
|
2026
|
-
async def list_processing_logs(
|
|
2027
|
-
|
|
2056
|
+
async def list_processing_logs(
|
|
2057
|
+
self,
|
|
2058
|
+
*,
|
|
2059
|
+
raw_file_id: UUID | str | None = None,
|
|
2060
|
+
survey_id: UUID | str | None = None,
|
|
2061
|
+
processing_step: str | None = None,
|
|
2062
|
+
) -> list[ProcessingLog]:
|
|
2063
|
+
"""List processing logs, optionally filtered.
|
|
2064
|
+
|
|
2065
|
+
Filters compose: each provided filter narrows the result set.
|
|
2066
|
+
|
|
2067
|
+
Args:
|
|
2068
|
+
raw_file_id: Match only logs for this raw file.
|
|
2069
|
+
survey_id: Match only logs for this survey.
|
|
2070
|
+
processing_step: Match only logs with this processing step
|
|
2071
|
+
(e.g. ``"scan"``, ``"ingest"``, ``"products"``).
|
|
2028
2072
|
|
|
2029
2073
|
Returns:
|
|
2030
|
-
List of processing
|
|
2074
|
+
List of matching processing logs (empty list if none match).
|
|
2031
2075
|
"""
|
|
2032
|
-
|
|
2076
|
+
params: dict[str, str] = {}
|
|
2077
|
+
if raw_file_id is not None:
|
|
2078
|
+
params["raw_file_id"] = str(raw_file_id)
|
|
2079
|
+
if survey_id is not None:
|
|
2080
|
+
params["survey_id"] = str(survey_id)
|
|
2081
|
+
if processing_step is not None:
|
|
2082
|
+
params["processing_step"] = processing_step
|
|
2083
|
+
response = await self._client.get(
|
|
2084
|
+
"/api/v1/processing-logs", params=params or None
|
|
2085
|
+
)
|
|
2033
2086
|
response.raise_for_status()
|
|
2034
2087
|
return [ProcessingLog.model_validate(item) for item in response.json()]
|
|
2035
2088
|
|
|
@@ -2052,6 +2105,83 @@ class SonarWizAsyncApi:
|
|
|
2052
2105
|
response.raise_for_status()
|
|
2053
2106
|
return ProcessingLog.model_validate(response.json())
|
|
2054
2107
|
|
|
2108
|
+
# ── User Layers ──────────────────────────────────────────────────────
|
|
2109
|
+
|
|
2110
|
+
async def create_user_layer(
|
|
2111
|
+
self, user_layer: UserLayerCreate
|
|
2112
|
+
) -> UserLayerWithUpload:
|
|
2113
|
+
"""Create a new user layer and initiate upload.
|
|
2114
|
+
|
|
2115
|
+
Args:
|
|
2116
|
+
user_layer: User layer creation data.
|
|
2117
|
+
|
|
2118
|
+
Returns:
|
|
2119
|
+
Created user layer instance with upload information.
|
|
2120
|
+
"""
|
|
2121
|
+
response = await self._client.post(
|
|
2122
|
+
"/api/v1/user-layers/upload",
|
|
2123
|
+
json=user_layer.model_dump(mode="json"),
|
|
2124
|
+
)
|
|
2125
|
+
response.raise_for_status()
|
|
2126
|
+
return UserLayerWithUpload.model_validate(response.json())
|
|
2127
|
+
|
|
2128
|
+
async def get_user_layer(self, user_layer_id: UUID) -> UserLayer:
|
|
2129
|
+
"""Fetch a user layer by ID.
|
|
2130
|
+
|
|
2131
|
+
Args:
|
|
2132
|
+
user_layer_id: User layer UUID to fetch.
|
|
2133
|
+
|
|
2134
|
+
Returns:
|
|
2135
|
+
UserLayer instance.
|
|
2136
|
+
"""
|
|
2137
|
+
response = await self._client.get(f"/api/v1/user-layers/{user_layer_id}")
|
|
2138
|
+
response.raise_for_status()
|
|
2139
|
+
return UserLayer.model_validate(response.json())
|
|
2140
|
+
|
|
2141
|
+
async def list_user_layers(self, project_id: UUID) -> list[UserLayer]:
|
|
2142
|
+
"""List all user layers for a project.
|
|
2143
|
+
|
|
2144
|
+
Args:
|
|
2145
|
+
project_id: Project UUID to filter by.
|
|
2146
|
+
|
|
2147
|
+
Returns:
|
|
2148
|
+
List of user layer instances.
|
|
2149
|
+
"""
|
|
2150
|
+
response = await self._client.get(
|
|
2151
|
+
"/api/v1/user-layers",
|
|
2152
|
+
params={"project_id": str(project_id)},
|
|
2153
|
+
)
|
|
2154
|
+
response.raise_for_status()
|
|
2155
|
+
return [UserLayer.model_validate(item) for item in response.json()]
|
|
2156
|
+
|
|
2157
|
+
async def update_user_layer(
|
|
2158
|
+
self, user_layer_id: UUID, user_layer: UserLayerUpdate
|
|
2159
|
+
) -> UserLayer:
|
|
2160
|
+
"""Update a user layer.
|
|
2161
|
+
|
|
2162
|
+
Args:
|
|
2163
|
+
user_layer_id: User layer UUID to update.
|
|
2164
|
+
user_layer: User layer update data.
|
|
2165
|
+
|
|
2166
|
+
Returns:
|
|
2167
|
+
Updated user layer instance.
|
|
2168
|
+
"""
|
|
2169
|
+
response = await self._client.patch(
|
|
2170
|
+
f"/api/v1/user-layers/{user_layer_id}",
|
|
2171
|
+
json=user_layer.model_dump(mode="json", exclude_none=True),
|
|
2172
|
+
)
|
|
2173
|
+
response.raise_for_status()
|
|
2174
|
+
return UserLayer.model_validate(response.json())
|
|
2175
|
+
|
|
2176
|
+
async def delete_user_layer(self, user_layer_id: UUID) -> None:
|
|
2177
|
+
"""Delete a user layer.
|
|
2178
|
+
|
|
2179
|
+
Args:
|
|
2180
|
+
user_layer_id: User layer UUID to delete.
|
|
2181
|
+
"""
|
|
2182
|
+
response = await self._client.delete(f"/api/v1/user-layers/{user_layer_id}")
|
|
2183
|
+
response.raise_for_status()
|
|
2184
|
+
|
|
2055
2185
|
# ── Processing Jobs ──────────────────────────────────────────────────
|
|
2056
2186
|
|
|
2057
2187
|
async def create_job(self, job_data: ProcessingJobCreate) -> ProcessingJob:
|
|
@@ -59,10 +59,14 @@ def upload(
|
|
|
59
59
|
False, "--no-mappings", help="Skip automatic device mapping creation"
|
|
60
60
|
),
|
|
61
61
|
sidescan_stream: str = typer.Option(
|
|
62
|
-
"
|
|
62
|
+
"xtf://sidescan(source=ping_header,pair=0)",
|
|
63
|
+
"--sidescan-stream",
|
|
64
|
+
help="Source URI for the sidescan stream identifier",
|
|
63
65
|
),
|
|
64
66
|
gnss_stream: str = typer.Option(
|
|
65
|
-
"
|
|
67
|
+
"xtf://position(source=ping_header,description=ship_position)",
|
|
68
|
+
"--gnss-stream",
|
|
69
|
+
help="Source URI for the GNSS stream identifier",
|
|
66
70
|
),
|
|
67
71
|
output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
68
72
|
) -> None:
|
|
@@ -71,7 +75,22 @@ def upload(
|
|
|
71
75
|
After uploading, automatically creates device mappings linking the file
|
|
72
76
|
to the survey's sidescan and GNSS devices. Use --no-mappings to skip.
|
|
73
77
|
"""
|
|
74
|
-
from cti.model.raw_file_device_mapping import
|
|
78
|
+
from cti.model.raw_file_device_mapping import (
|
|
79
|
+
RawFileDeviceMappingCreate,
|
|
80
|
+
SourceSelection,
|
|
81
|
+
StreamIdentifier,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _identifier_from_uri(uri: str, roles: tuple[str, ...]) -> StreamIdentifier:
|
|
85
|
+
return StreamIdentifier(
|
|
86
|
+
source=SourceSelection(available=[uri], selected=uri),
|
|
87
|
+
fields_by_source={
|
|
88
|
+
uri: {
|
|
89
|
+
role: SourceSelection(available=[role], selected=role)
|
|
90
|
+
for role in roles
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
)
|
|
75
94
|
|
|
76
95
|
if not file.exists():
|
|
77
96
|
typer.echo(f"Error: file not found: {file}", err=True)
|
|
@@ -115,9 +134,13 @@ def upload(
|
|
|
115
134
|
if not dt:
|
|
116
135
|
continue
|
|
117
136
|
if dt.enum_name == "sidescan_sonar":
|
|
118
|
-
stream_id =
|
|
137
|
+
stream_id = _identifier_from_uri(
|
|
138
|
+
sidescan_stream, ("samples", "timestamp")
|
|
139
|
+
)
|
|
119
140
|
elif dt.enum_name == "gnss_gps_position_sensor":
|
|
120
|
-
stream_id =
|
|
141
|
+
stream_id = _identifier_from_uri(
|
|
142
|
+
gnss_stream, ("position", "timestamp")
|
|
143
|
+
)
|
|
121
144
|
else:
|
|
122
145
|
continue
|
|
123
146
|
|
|
@@ -130,7 +153,9 @@ def upload(
|
|
|
130
153
|
input_timezone=tz,
|
|
131
154
|
)
|
|
132
155
|
)
|
|
133
|
-
typer.echo(
|
|
156
|
+
typer.echo(
|
|
157
|
+
f"Mapped: {device.name} → stream {stream_id.source.selected}"
|
|
158
|
+
)
|
|
134
159
|
mappings_created += 1
|
|
135
160
|
|
|
136
161
|
if mappings_created == 0:
|
|
@@ -7,20 +7,61 @@ from uuid import UUID
|
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class SourceSelection(BaseModel):
|
|
11
|
+
"""An ``{available, selected}`` pair.
|
|
12
|
+
|
|
13
|
+
Used at both levels of ``StreamIdentifier``: outer source URI and
|
|
14
|
+
inner field-role selection.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
available: All candidate values produced by the scan.
|
|
18
|
+
selected: The currently chosen value; must be one of ``available``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
available: list[str]
|
|
22
|
+
selected: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StreamIdentifier(BaseModel):
|
|
26
|
+
"""Two-level source / field selection persisted on RawFileDeviceMapping.
|
|
27
|
+
|
|
28
|
+
Mirrors the shape produced by the engine's manifest serializer for
|
|
29
|
+
one stream descriptor (see clarity-engine
|
|
30
|
+
``pipeline/ingest/manifest_serializer.py``):
|
|
31
|
+
|
|
32
|
+
- ``source`` — which packet-level source within the file feeds this
|
|
33
|
+
device (e.g., ``xtf://sidescan(source=ping_header,pair=0)``).
|
|
34
|
+
- ``fields_by_source`` — for each available source URI, which raw
|
|
35
|
+
field provides each role (``samples``, ``timestamp``, ``position``,
|
|
36
|
+
…).
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
source: Outer source-URI selection.
|
|
40
|
+
fields_by_source: Per-source role → field selection. Outer key is
|
|
41
|
+
the source URI; inner key is the role name.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
source: SourceSelection
|
|
45
|
+
fields_by_source: dict[str, dict[str, SourceSelection]]
|
|
46
|
+
|
|
47
|
+
|
|
10
48
|
class RawFileDeviceMappingBase(BaseModel):
|
|
11
49
|
"""Base model for raw file device mapping.
|
|
12
50
|
|
|
13
51
|
Attributes:
|
|
14
52
|
raw_file_id: Foreign key reference to RawFile.
|
|
15
53
|
device_id: Foreign key reference to Device.
|
|
16
|
-
source_stream_identifier:
|
|
54
|
+
source_stream_identifier: Two-level source / field-role selection.
|
|
55
|
+
auto_mapped: True for system-generated mappings, False for user
|
|
56
|
+
overrides. Defaults to True.
|
|
17
57
|
input_srid: Spatial reference ID for the input data.
|
|
18
58
|
input_timezone: IANA timezone name for the input data (e.g. Etc/UTC).
|
|
19
59
|
"""
|
|
20
60
|
|
|
21
61
|
raw_file_id: UUID
|
|
22
62
|
device_id: UUID
|
|
23
|
-
source_stream_identifier:
|
|
63
|
+
source_stream_identifier: StreamIdentifier
|
|
64
|
+
auto_mapped: bool = True
|
|
24
65
|
input_srid: int = Field(ge=2000, le=900913)
|
|
25
66
|
input_timezone: str
|
|
26
67
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Pydantic models for user layer. User-uploaded geospatial layers associated with a project."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UserLayerState(str, Enum):
|
|
11
|
+
"""Status of a user layer."""
|
|
12
|
+
|
|
13
|
+
UPLOADING = "uploading"
|
|
14
|
+
READY = "ready"
|
|
15
|
+
ERROR = "error"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UserLayerType(str, Enum):
|
|
19
|
+
"""Supported user layer file types."""
|
|
20
|
+
|
|
21
|
+
GEOJSON = "geojson"
|
|
22
|
+
KML = "kml"
|
|
23
|
+
KMZ = "kmz"
|
|
24
|
+
SHAPEFILE = "shapefile"
|
|
25
|
+
GPX = "gpx"
|
|
26
|
+
CSV = "csv"
|
|
27
|
+
GEOTIFF = "geotiff"
|
|
28
|
+
BAG = "bag"
|
|
29
|
+
LAS = "las"
|
|
30
|
+
LAZ = "laz"
|
|
31
|
+
PLY = "ply"
|
|
32
|
+
PCD = "pcd"
|
|
33
|
+
MAGNETOMETRY = "magnetometry"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UserLayerBase(BaseModel):
|
|
37
|
+
"""Base model for user layer data.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
project_id: Foreign key reference to Project.
|
|
41
|
+
layer_name: Display name for the layer.
|
|
42
|
+
layer_type: Type of the user layer file.
|
|
43
|
+
file_name: Name of the uploaded file.
|
|
44
|
+
file_size: Size of the uploaded file in bytes.
|
|
45
|
+
style: Optional styling configuration for the layer.
|
|
46
|
+
metadata: Optional metadata for the layer.
|
|
47
|
+
sort_order: Display sort order.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
project_id: UUID
|
|
51
|
+
layer_name: str
|
|
52
|
+
layer_type: UserLayerType
|
|
53
|
+
file_name: str | None = None
|
|
54
|
+
file_size: int | None = None
|
|
55
|
+
style: dict | None = None
|
|
56
|
+
metadata: dict | None = None
|
|
57
|
+
sort_order: int = 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UserLayerCreate(UserLayerBase):
|
|
61
|
+
"""Model for creating a user layer."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class UserLayerUpdate(BaseModel):
|
|
65
|
+
"""Model for updating a user layer.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
layer_name: Optional updated display name.
|
|
69
|
+
style: Optional updated styling configuration.
|
|
70
|
+
metadata: Optional updated metadata.
|
|
71
|
+
sort_order: Optional updated sort order.
|
|
72
|
+
state: Optional updated state.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
layer_name: str | None = None
|
|
76
|
+
style: dict | None = None
|
|
77
|
+
metadata: dict | None = None
|
|
78
|
+
sort_order: int | None = None
|
|
79
|
+
state: UserLayerState | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class UserLayer(UserLayerBase):
|
|
83
|
+
"""Model for user layer data with database fields.
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
user_layer_id: Unique identifier for the user layer.
|
|
87
|
+
user_id: ID of the user who created the layer.
|
|
88
|
+
state: Upload state (uploading, ready, error).
|
|
89
|
+
uri: URI for the layer file storage location.
|
|
90
|
+
upload_id: S3 multipart upload ID (present while uploading, cleared when complete).
|
|
91
|
+
created_date: Timestamp when the layer was created.
|
|
92
|
+
updated_date: Timestamp when the layer was last updated.
|
|
93
|
+
updated_by: ID of the user who last updated the layer.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
user_layer_id: UUID
|
|
97
|
+
user_id: str
|
|
98
|
+
state: UserLayerState = UserLayerState.UPLOADING
|
|
99
|
+
uri: str | None = None
|
|
100
|
+
upload_id: str | None = None
|
|
101
|
+
created_date: datetime
|
|
102
|
+
updated_date: datetime
|
|
103
|
+
updated_by: str
|
|
104
|
+
|
|
105
|
+
model_config = ConfigDict(from_attributes=True)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class UserLayerWithUpload(UserLayer):
|
|
109
|
+
"""UserLayer response for the create-with-upload endpoint.
|
|
110
|
+
|
|
111
|
+
The endpoint initiates an S3 multipart upload before returning, so `uri`
|
|
112
|
+
and `upload_id` are guaranteed to be present. Overrides those fields to
|
|
113
|
+
be non-nullable so callers don't have to defensively check.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
uri: S3 URI for the layer file storage location. Always populated.
|
|
117
|
+
upload_id: S3 multipart upload ID. Always populated; clients pass it
|
|
118
|
+
to subsequent part-upload and complete-upload calls.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
# Tightening parent's `str | None` to required `str`. Pyright flags the
|
|
122
|
+
# type narrowing as an override issue; Pydantic treats the bare field
|
|
123
|
+
# annotation as required at runtime, which is what we want.
|
|
124
|
+
uri: str # type: ignore[assignment]
|
|
125
|
+
upload_id: str # type: ignore[assignment]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Round-trip tests for the RawFileDeviceMapping SDK Pydantic model.
|
|
2
|
+
|
|
3
|
+
Phase II Step 10 reshaped ``source_stream_identifier`` from a free-form
|
|
4
|
+
string to a nested ``StreamIdentifier``. These tests pin the wire shape
|
|
5
|
+
the engine and server already agree on so future drift fails loudly.
|
|
6
|
+
|
|
7
|
+
See clarity-server#104 / #105 and
|
|
8
|
+
designs/spec-file-prescan-stream-mapping.md §lines 489–495, 939–953.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
|
|
18
|
+
from cti.model.raw_file_device_mapping import (
|
|
19
|
+
RawFileDeviceMappingCreate,
|
|
20
|
+
SourceSelection,
|
|
21
|
+
StreamIdentifier,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_SOURCE_URI = "xtf://sidescan(source=ping_header,pair=0)"
|
|
26
|
+
_SAMPLE_IDENTIFIER_DICT = {
|
|
27
|
+
"source": {"available": [_SOURCE_URI], "selected": _SOURCE_URI},
|
|
28
|
+
"fields_by_source": {
|
|
29
|
+
_SOURCE_URI: {
|
|
30
|
+
"samples": {"available": ["samples"], "selected": "samples"},
|
|
31
|
+
"timestamp": {"available": ["timestamp"], "selected": "timestamp"},
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_stream_identifier_round_trips_from_dict() -> None:
|
|
38
|
+
"""A dict in the manifest-serializer shape parses and serializes back unchanged."""
|
|
39
|
+
ident = StreamIdentifier.model_validate(_SAMPLE_IDENTIFIER_DICT)
|
|
40
|
+
|
|
41
|
+
assert isinstance(ident.source, SourceSelection)
|
|
42
|
+
assert ident.source.selected == _SOURCE_URI
|
|
43
|
+
assert ident.fields_by_source[_SOURCE_URI]["samples"].selected == "samples"
|
|
44
|
+
|
|
45
|
+
assert ident.model_dump() == _SAMPLE_IDENTIFIER_DICT
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_create_payload_round_trips_through_json() -> None:
|
|
49
|
+
"""End-to-end: build a Create model, serialize to JSON, parse it back."""
|
|
50
|
+
payload = RawFileDeviceMappingCreate(
|
|
51
|
+
raw_file_id=uuid4(),
|
|
52
|
+
device_id=uuid4(),
|
|
53
|
+
source_stream_identifier=StreamIdentifier.model_validate(
|
|
54
|
+
_SAMPLE_IDENTIFIER_DICT
|
|
55
|
+
),
|
|
56
|
+
input_srid=4326,
|
|
57
|
+
input_timezone="Etc/UTC",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parsed = RawFileDeviceMappingCreate.model_validate_json(payload.model_dump_json())
|
|
61
|
+
assert parsed == payload
|
|
62
|
+
assert parsed.auto_mapped is True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_auto_mapped_defaults_to_true_when_omitted() -> None:
|
|
66
|
+
"""``auto_mapped`` is a system-generated default, not required from clients."""
|
|
67
|
+
payload = RawFileDeviceMappingCreate.model_validate(
|
|
68
|
+
{
|
|
69
|
+
"raw_file_id": str(uuid4()),
|
|
70
|
+
"device_id": str(uuid4()),
|
|
71
|
+
"source_stream_identifier": _SAMPLE_IDENTIFIER_DICT,
|
|
72
|
+
"input_srid": 4326,
|
|
73
|
+
"input_timezone": "Etc/UTC",
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
assert payload.auto_mapped is True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_auto_mapped_false_persists_for_user_overrides() -> None:
|
|
80
|
+
"""User-supplied overrides must round-trip ``auto_mapped: false``."""
|
|
81
|
+
payload = RawFileDeviceMappingCreate.model_validate(
|
|
82
|
+
{
|
|
83
|
+
"raw_file_id": str(uuid4()),
|
|
84
|
+
"device_id": str(uuid4()),
|
|
85
|
+
"source_stream_identifier": _SAMPLE_IDENTIFIER_DICT,
|
|
86
|
+
"auto_mapped": False,
|
|
87
|
+
"input_srid": 4326,
|
|
88
|
+
"input_timezone": "Etc/UTC",
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
assert payload.auto_mapped is False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_string_source_stream_identifier_is_rejected() -> None:
|
|
95
|
+
"""The pre-Phase-II free-form string form must no longer validate.
|
|
96
|
+
|
|
97
|
+
Pins the breaking change so a future regression that loosens the type
|
|
98
|
+
back to ``str`` fails this test instead of silently re-allowing
|
|
99
|
+
unstructured identifiers.
|
|
100
|
+
"""
|
|
101
|
+
with pytest.raises(ValidationError):
|
|
102
|
+
RawFileDeviceMappingCreate.model_validate(
|
|
103
|
+
{
|
|
104
|
+
"raw_file_id": str(uuid4()),
|
|
105
|
+
"device_id": str(uuid4()),
|
|
106
|
+
"source_stream_identifier": "channel-0",
|
|
107
|
+
"input_srid": 4326,
|
|
108
|
+
"input_timezone": "Etc/UTC",
|
|
109
|
+
}
|
|
110
|
+
)
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/tests/test_sdk_async_methods.py
RENAMED
|
@@ -18,11 +18,16 @@ from .conftest import (
|
|
|
18
18
|
RAW_FILE_ID,
|
|
19
19
|
SURVEY_ID,
|
|
20
20
|
make_job_json,
|
|
21
|
+
make_processing_log_json,
|
|
22
|
+
make_raw_file_device_mapping_json,
|
|
21
23
|
make_raw_file_json,
|
|
22
24
|
make_raw_file_with_upload_json,
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
DEVICE_ID = UUID("00000000-0000-0000-0000-000000000099")
|
|
29
|
+
|
|
30
|
+
|
|
26
31
|
def _mock_response(
|
|
27
32
|
json_data: dict, status_code: int = 200, headers: dict | None = None
|
|
28
33
|
) -> MagicMock:
|
|
@@ -219,3 +224,148 @@ async def test_wait_for_job_raises_on_timeout():
|
|
|
219
224
|
with patch.object(loop, "time", side_effect=mock_time):
|
|
220
225
|
with pytest.raises(TimeoutError, match="did not complete"):
|
|
221
226
|
await api.wait_for_job(job_id=JOB_ID, timeout=10)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ── list_processing_logs filter kwargs (WI-0106 A1) ─────────────────────
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@pytest.mark.asyncio
|
|
233
|
+
async def test_list_processing_logs_no_filters_omits_params():
|
|
234
|
+
"""No kwargs → request sent with params=None (preserves existing behavior)."""
|
|
235
|
+
client = AsyncMock()
|
|
236
|
+
client.get.return_value = _mock_response([make_processing_log_json()])
|
|
237
|
+
|
|
238
|
+
api = SonarWizAsyncApi(client)
|
|
239
|
+
logs = await api.list_processing_logs()
|
|
240
|
+
|
|
241
|
+
assert len(logs) == 1
|
|
242
|
+
client.get.assert_awaited_once_with("/api/v1/processing-logs", params=None)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@pytest.mark.asyncio
|
|
246
|
+
async def test_list_processing_logs_all_filters_passed_as_query_params():
|
|
247
|
+
"""All three filters compose into the query string; UUIDs stringified."""
|
|
248
|
+
client = AsyncMock()
|
|
249
|
+
client.get.return_value = _mock_response(
|
|
250
|
+
[make_processing_log_json(processing_step="scan", result='{"streams":[]}')]
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
api = SonarWizAsyncApi(client)
|
|
254
|
+
logs = await api.list_processing_logs(
|
|
255
|
+
raw_file_id=RAW_FILE_ID,
|
|
256
|
+
survey_id=SURVEY_ID,
|
|
257
|
+
processing_step="scan",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
client.get.assert_awaited_once_with(
|
|
261
|
+
"/api/v1/processing-logs",
|
|
262
|
+
params={
|
|
263
|
+
"raw_file_id": str(RAW_FILE_ID),
|
|
264
|
+
"survey_id": str(SURVEY_ID),
|
|
265
|
+
"processing_step": "scan",
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
assert logs[0].processing_step == "scan"
|
|
269
|
+
assert logs[0].result == '{"streams":[]}'
|
|
270
|
+
assert logs[0].processing_hash == "a" * 64
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@pytest.mark.asyncio
|
|
274
|
+
async def test_list_processing_logs_partial_filter_only_includes_provided():
|
|
275
|
+
"""Unset filters are omitted from the query string."""
|
|
276
|
+
client = AsyncMock()
|
|
277
|
+
client.get.return_value = _mock_response([])
|
|
278
|
+
|
|
279
|
+
api = SonarWizAsyncApi(client)
|
|
280
|
+
logs = await api.list_processing_logs(
|
|
281
|
+
raw_file_id=RAW_FILE_ID, processing_step="scan"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
client.get.assert_awaited_once_with(
|
|
285
|
+
"/api/v1/processing-logs",
|
|
286
|
+
params={"raw_file_id": str(RAW_FILE_ID), "processing_step": "scan"},
|
|
287
|
+
)
|
|
288
|
+
assert logs == []
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@pytest.mark.asyncio
|
|
292
|
+
async def test_list_processing_logs_accepts_string_uuid():
|
|
293
|
+
"""String UUIDs are accepted and stringified the same way."""
|
|
294
|
+
client = AsyncMock()
|
|
295
|
+
client.get.return_value = _mock_response([])
|
|
296
|
+
|
|
297
|
+
api = SonarWizAsyncApi(client)
|
|
298
|
+
await api.list_processing_logs(raw_file_id=str(RAW_FILE_ID))
|
|
299
|
+
|
|
300
|
+
client.get.assert_awaited_once_with(
|
|
301
|
+
"/api/v1/processing-logs",
|
|
302
|
+
params={"raw_file_id": str(RAW_FILE_ID)},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ── get_raw_file_device_mappings filter kwargs (#23) ────────────────────
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@pytest.mark.asyncio
|
|
310
|
+
async def test_get_raw_file_device_mappings_requires_a_filter():
|
|
311
|
+
"""Calling with neither filter raises ValueError before touching the wire."""
|
|
312
|
+
client = AsyncMock()
|
|
313
|
+
api = SonarWizAsyncApi(client)
|
|
314
|
+
|
|
315
|
+
with pytest.raises(ValueError, match="raw_file_id or device_id"):
|
|
316
|
+
await api.get_raw_file_device_mappings()
|
|
317
|
+
|
|
318
|
+
client.get.assert_not_called()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@pytest.mark.asyncio
|
|
322
|
+
async def test_get_raw_file_device_mappings_filters_by_raw_file_id():
|
|
323
|
+
"""raw_file_id filter is sent as a query param; UUID stringified."""
|
|
324
|
+
client = AsyncMock()
|
|
325
|
+
client.get.return_value = _mock_response(
|
|
326
|
+
[make_raw_file_device_mapping_json(device_id=DEVICE_ID)]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
api = SonarWizAsyncApi(client)
|
|
330
|
+
mappings = await api.get_raw_file_device_mappings(raw_file_id=RAW_FILE_ID)
|
|
331
|
+
|
|
332
|
+
client.get.assert_awaited_once_with(
|
|
333
|
+
"/api/v1/raw-file-device-mappings",
|
|
334
|
+
params={"raw_file_id": str(RAW_FILE_ID)},
|
|
335
|
+
)
|
|
336
|
+
assert len(mappings) == 1
|
|
337
|
+
assert mappings[0].raw_file_id == RAW_FILE_ID
|
|
338
|
+
assert mappings[0].device_id == DEVICE_ID
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@pytest.mark.asyncio
|
|
342
|
+
async def test_get_raw_file_device_mappings_filters_by_device_id():
|
|
343
|
+
"""device_id filter is sent alone when raw_file_id is omitted."""
|
|
344
|
+
client = AsyncMock()
|
|
345
|
+
client.get.return_value = _mock_response([])
|
|
346
|
+
|
|
347
|
+
api = SonarWizAsyncApi(client)
|
|
348
|
+
mappings = await api.get_raw_file_device_mappings(device_id=DEVICE_ID)
|
|
349
|
+
|
|
350
|
+
client.get.assert_awaited_once_with(
|
|
351
|
+
"/api/v1/raw-file-device-mappings",
|
|
352
|
+
params={"device_id": str(DEVICE_ID)},
|
|
353
|
+
)
|
|
354
|
+
assert mappings == []
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@pytest.mark.asyncio
|
|
358
|
+
async def test_get_raw_file_device_mappings_accepts_string_uuids():
|
|
359
|
+
"""String UUIDs are accepted and stringified the same way."""
|
|
360
|
+
client = AsyncMock()
|
|
361
|
+
client.get.return_value = _mock_response([])
|
|
362
|
+
|
|
363
|
+
api = SonarWizAsyncApi(client)
|
|
364
|
+
await api.get_raw_file_device_mappings(
|
|
365
|
+
raw_file_id=str(RAW_FILE_ID), device_id=str(DEVICE_ID)
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
client.get.assert_awaited_once_with(
|
|
369
|
+
"/api/v1/raw-file-device-mappings",
|
|
370
|
+
params={"raw_file_id": str(RAW_FILE_ID), "device_id": str(DEVICE_ID)},
|
|
371
|
+
)
|
|
@@ -19,11 +19,16 @@ from .conftest import (
|
|
|
19
19
|
RAW_FILE_ID,
|
|
20
20
|
SURVEY_ID,
|
|
21
21
|
make_job_json,
|
|
22
|
+
make_processing_log_json,
|
|
23
|
+
make_raw_file_device_mapping_json,
|
|
22
24
|
make_raw_file_json,
|
|
23
25
|
make_raw_file_with_upload_json,
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
DEVICE_ID = UUID("00000000-0000-0000-0000-000000000099")
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
def _mock_response(
|
|
28
33
|
json_data: dict, status_code: int = 200, headers: dict | None = None
|
|
29
34
|
) -> MagicMock:
|
|
@@ -262,3 +267,121 @@ class TestWaitForJob:
|
|
|
262
267
|
mock_mono.side_effect = [0.0, 0.0, 0.0, 100.0]
|
|
263
268
|
with pytest.raises(TimeoutError, match="did not complete"):
|
|
264
269
|
api.wait_for_job(job_id=JOB_ID, timeout=10)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ── list_processing_logs filter kwargs (WI-0106 A1) ─────────────────────
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_list_processing_logs_no_filters_omits_params():
|
|
276
|
+
"""No kwargs → request sent with params=None."""
|
|
277
|
+
client = MagicMock()
|
|
278
|
+
client.get.return_value = _mock_response([make_processing_log_json()])
|
|
279
|
+
|
|
280
|
+
api = SonarWizApi(client)
|
|
281
|
+
logs = api.list_processing_logs()
|
|
282
|
+
|
|
283
|
+
assert len(logs) == 1
|
|
284
|
+
client.get.assert_called_once_with("/api/v1/processing-logs", params=None)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_list_processing_logs_all_filters_passed_as_query_params():
|
|
288
|
+
"""All three filters compose into the query string."""
|
|
289
|
+
client = MagicMock()
|
|
290
|
+
client.get.return_value = _mock_response(
|
|
291
|
+
[make_processing_log_json(processing_step="scan")]
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
api = SonarWizApi(client)
|
|
295
|
+
api.list_processing_logs(
|
|
296
|
+
raw_file_id=RAW_FILE_ID,
|
|
297
|
+
survey_id=SURVEY_ID,
|
|
298
|
+
processing_step="scan",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
client.get.assert_called_once_with(
|
|
302
|
+
"/api/v1/processing-logs",
|
|
303
|
+
params={
|
|
304
|
+
"raw_file_id": str(RAW_FILE_ID),
|
|
305
|
+
"survey_id": str(SURVEY_ID),
|
|
306
|
+
"processing_step": "scan",
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_list_processing_logs_partial_filter_only_includes_provided():
|
|
312
|
+
"""Unset filters are omitted from the query string."""
|
|
313
|
+
client = MagicMock()
|
|
314
|
+
client.get.return_value = _mock_response([])
|
|
315
|
+
|
|
316
|
+
api = SonarWizApi(client)
|
|
317
|
+
logs = api.list_processing_logs(survey_id=SURVEY_ID)
|
|
318
|
+
|
|
319
|
+
client.get.assert_called_once_with(
|
|
320
|
+
"/api/v1/processing-logs",
|
|
321
|
+
params={"survey_id": str(SURVEY_ID)},
|
|
322
|
+
)
|
|
323
|
+
assert logs == []
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ── get_raw_file_device_mappings filter kwargs (#23) ────────────────────
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_get_raw_file_device_mappings_requires_a_filter():
|
|
330
|
+
"""Calling with neither filter raises ValueError before touching the wire."""
|
|
331
|
+
client = MagicMock()
|
|
332
|
+
api = SonarWizApi(client)
|
|
333
|
+
|
|
334
|
+
with pytest.raises(ValueError, match="raw_file_id or device_id"):
|
|
335
|
+
api.get_raw_file_device_mappings()
|
|
336
|
+
|
|
337
|
+
client.get.assert_not_called()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_get_raw_file_device_mappings_filters_by_raw_file_id():
|
|
341
|
+
"""raw_file_id filter is sent as a query param; UUID stringified."""
|
|
342
|
+
client = MagicMock()
|
|
343
|
+
client.get.return_value = _mock_response(
|
|
344
|
+
[make_raw_file_device_mapping_json(device_id=DEVICE_ID)]
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
api = SonarWizApi(client)
|
|
348
|
+
mappings = api.get_raw_file_device_mappings(raw_file_id=RAW_FILE_ID)
|
|
349
|
+
|
|
350
|
+
client.get.assert_called_once_with(
|
|
351
|
+
"/api/v1/raw-file-device-mappings",
|
|
352
|
+
params={"raw_file_id": str(RAW_FILE_ID)},
|
|
353
|
+
)
|
|
354
|
+
assert len(mappings) == 1
|
|
355
|
+
assert mappings[0].raw_file_id == RAW_FILE_ID
|
|
356
|
+
assert mappings[0].device_id == DEVICE_ID
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_get_raw_file_device_mappings_filters_by_device_id():
|
|
360
|
+
"""device_id filter is sent alone when raw_file_id is omitted."""
|
|
361
|
+
client = MagicMock()
|
|
362
|
+
client.get.return_value = _mock_response([])
|
|
363
|
+
|
|
364
|
+
api = SonarWizApi(client)
|
|
365
|
+
mappings = api.get_raw_file_device_mappings(device_id=DEVICE_ID)
|
|
366
|
+
|
|
367
|
+
client.get.assert_called_once_with(
|
|
368
|
+
"/api/v1/raw-file-device-mappings",
|
|
369
|
+
params={"device_id": str(DEVICE_ID)},
|
|
370
|
+
)
|
|
371
|
+
assert mappings == []
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_get_raw_file_device_mappings_accepts_string_uuids():
|
|
375
|
+
"""String UUIDs are accepted and stringified the same way."""
|
|
376
|
+
client = MagicMock()
|
|
377
|
+
client.get.return_value = _mock_response([])
|
|
378
|
+
|
|
379
|
+
api = SonarWizApi(client)
|
|
380
|
+
api.get_raw_file_device_mappings(
|
|
381
|
+
raw_file_id=str(RAW_FILE_ID), device_id=str(DEVICE_ID)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
client.get.assert_called_once_with(
|
|
385
|
+
"/api/v1/raw-file-device-mappings",
|
|
386
|
+
params={"raw_file_id": str(RAW_FILE_ID), "device_id": str(DEVICE_ID)},
|
|
387
|
+
)
|
|
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
|
|
File without changes
|
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/altitude_source.py
RENAMED
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/attitude_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/depth_source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/final_product.py
RENAMED
|
File without changes
|
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/layback_algorithm.py
RENAMED
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/layback_source.py
RENAMED
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/layback_type.py
RENAMED
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/organization.py
RENAMED
|
File without changes
|
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/platform_type.py
RENAMED
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/position_source.py
RENAMED
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/processing_job.py
RENAMED
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/processing_log.py
RENAMED
|
File without changes
|
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/projection_option.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/raw_file_state.py
RENAMED
|
File without changes
|
|
File without changes
|
{clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.1}/src/cti/model/sidescan_ping_source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|