clarity-api-sdk-python 0.4.1__tar.gz → 0.4.3__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.4.1 → clarity_api_sdk_python-0.4.3}/PKG-INFO +2 -1
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/pyproject.toml +2 -1
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/clarity_api_sdk_python.egg-info/PKG-INFO +2 -1
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/clarity_api_sdk_python.egg-info/SOURCES.txt +4 -1
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/clarity_api_sdk_python.egg-info/requires.txt +1 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/api/sonar_wiz_api.py +30 -7
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/api/sonar_wiz_async_api.py +30 -7
- clarity_api_sdk_python-0.4.3/src/cti/model/target.py +132 -0
- clarity_api_sdk_python-0.4.3/src/cti/positioning/__init__.py +8 -0
- clarity_api_sdk_python-0.4.3/src/cti/positioning/target_geometry.py +159 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/tests/test_sdk_async_methods.py +160 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/tests/test_sdk_methods.py +150 -0
- clarity_api_sdk_python-0.4.3/tests/test_target_geometry.py +253 -0
- clarity_api_sdk_python-0.4.1/src/cti/model/target.py +0 -63
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/README.md +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/setup.cfg +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/clarity_api_sdk_python.egg-info/dependency_links.txt +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/clarity_api_sdk_python.egg-info/entry_points.txt +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/clarity_api_sdk_python.egg-info/top_level.txt +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/__init__.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/api/__init__.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/api/async_client.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/api/client.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/api/session.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/cli/__init__.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/cli/__main__.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/cli/client.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/cli/main.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/logger/__init__.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/logger/logger.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/main.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/main_api.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/__init__.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/altitude_source.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/attitude_source.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/deferred_object_deletion.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/depth_source.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/device.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/device_type.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/final_product.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/hierarchy.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/layback_algorithm.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/layback_source.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/layback_type.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/organization.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/platform.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/platform_type.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/position_source.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/processing_job.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/processing_log.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/project.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/projection_option.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/raw_file.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/raw_file_configuration.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/raw_file_device_mapping.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/raw_file_state.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/s3.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/sidescan_ping_source.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/source.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/survey.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/tow_system.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/model/user_layer.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/tests/test_cli.py +0 -0
- {clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/tests/test_raw_file_device_mapping_model.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clarity-api-sdk-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
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
|
|
@@ -15,6 +15,7 @@ Requires-Dist: h2
|
|
|
15
15
|
Requires-Dist: httpx_auth>=0.23.1
|
|
16
16
|
Requires-Dist: httpx-retries>=0.4.5
|
|
17
17
|
Requires-Dist: pydantic==2.12.3
|
|
18
|
+
Requires-Dist: pyproj==3.7.2
|
|
18
19
|
Requires-Dist: structlog==25.4.0
|
|
19
20
|
Requires-Dist: typer>=0.15
|
|
20
21
|
Provides-Extra: dev
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "clarity-api-sdk-python"
|
|
8
|
-
version = "0.4.
|
|
8
|
+
version = "0.4.3"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="Chesapeake Technology Inc.", email="support@chesapeaketech.com" },
|
|
11
11
|
]
|
|
@@ -24,6 +24,7 @@ dependencies = [
|
|
|
24
24
|
"httpx_auth>=0.23.1",
|
|
25
25
|
"httpx-retries>=0.4.5",
|
|
26
26
|
"pydantic==2.12.3",
|
|
27
|
+
"pyproj==3.7.2",
|
|
27
28
|
"structlog==25.4.0",
|
|
28
29
|
"typer>=0.15",
|
|
29
30
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clarity-api-sdk-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
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
|
|
@@ -15,6 +15,7 @@ Requires-Dist: h2
|
|
|
15
15
|
Requires-Dist: httpx_auth>=0.23.1
|
|
16
16
|
Requires-Dist: httpx-retries>=0.4.5
|
|
17
17
|
Requires-Dist: pydantic==2.12.3
|
|
18
|
+
Requires-Dist: pyproj==3.7.2
|
|
18
19
|
Requires-Dist: structlog==25.4.0
|
|
19
20
|
Requires-Dist: typer>=0.15
|
|
20
21
|
Provides-Extra: dev
|
|
@@ -52,7 +52,10 @@ src/cti/model/survey.py
|
|
|
52
52
|
src/cti/model/target.py
|
|
53
53
|
src/cti/model/tow_system.py
|
|
54
54
|
src/cti/model/user_layer.py
|
|
55
|
+
src/cti/positioning/__init__.py
|
|
56
|
+
src/cti/positioning/target_geometry.py
|
|
55
57
|
tests/test_cli.py
|
|
56
58
|
tests/test_raw_file_device_mapping_model.py
|
|
57
59
|
tests/test_sdk_async_methods.py
|
|
58
|
-
tests/test_sdk_methods.py
|
|
60
|
+
tests/test_sdk_methods.py
|
|
61
|
+
tests/test_target_geometry.py
|
|
@@ -1709,33 +1709,56 @@ class SonarWizApi:
|
|
|
1709
1709
|
response.raise_for_status()
|
|
1710
1710
|
return Target.model_validate(response.json())
|
|
1711
1711
|
|
|
1712
|
-
def list_targets(self) -> list[Target]:
|
|
1713
|
-
"""List all targets.
|
|
1712
|
+
def list_targets(self, sidescan_ping_source_id: UUID | str) -> list[Target]:
|
|
1713
|
+
"""List all targets for one sidescan ping source.
|
|
1714
|
+
|
|
1715
|
+
The cross-source ``GET /api/v1/targets`` endpoint is deprecated;
|
|
1716
|
+
the spec scopes target reads to a single SidescanPingSource.
|
|
1717
|
+
|
|
1718
|
+
Args:
|
|
1719
|
+
sidescan_ping_source_id: SidescanPingSource UUID or string identifier.
|
|
1714
1720
|
|
|
1715
1721
|
Returns:
|
|
1716
|
-
List of target instances
|
|
1722
|
+
List of target instances, each with the ``derived`` block
|
|
1723
|
+
populated when the parent line's nav state is available.
|
|
1717
1724
|
"""
|
|
1718
|
-
response = self._client.get(
|
|
1725
|
+
response = self._client.get(
|
|
1726
|
+
f"/api/v1/sidescan-ping-sources/{sidescan_ping_source_id}/targets"
|
|
1727
|
+
)
|
|
1719
1728
|
response.raise_for_status()
|
|
1720
1729
|
return [Target.model_validate(item) for item in response.json()]
|
|
1721
1730
|
|
|
1722
1731
|
def update_target(self, target_id: UUID | str, target: TargetUpdate) -> Target:
|
|
1723
|
-
"""
|
|
1732
|
+
"""Partially update a target.
|
|
1733
|
+
|
|
1734
|
+
Targets MVP is the first endpoint in the API to use PATCH (older
|
|
1735
|
+
update endpoints use PUT for legacy reasons but already behave as
|
|
1736
|
+
PATCH server-side via ``exclude_unset=True``). Body fields are
|
|
1737
|
+
optional; only the supplied fields are mutated.
|
|
1724
1738
|
|
|
1725
1739
|
Args:
|
|
1726
1740
|
target_id: Target UUID or string identifier.
|
|
1727
|
-
target:
|
|
1741
|
+
target: Partial target update data.
|
|
1728
1742
|
|
|
1729
1743
|
Returns:
|
|
1730
1744
|
Updated target instance.
|
|
1731
1745
|
"""
|
|
1732
|
-
response = self._client.
|
|
1746
|
+
response = self._client.patch(
|
|
1733
1747
|
f"/api/v1/targets/{target_id}",
|
|
1734
1748
|
json=target.model_dump(mode="json", exclude_unset=True),
|
|
1735
1749
|
)
|
|
1736
1750
|
response.raise_for_status()
|
|
1737
1751
|
return Target.model_validate(response.json())
|
|
1738
1752
|
|
|
1753
|
+
def delete_target(self, target_id: UUID | str) -> None:
|
|
1754
|
+
"""Hard-delete a target.
|
|
1755
|
+
|
|
1756
|
+
Args:
|
|
1757
|
+
target_id: Target UUID or string identifier.
|
|
1758
|
+
"""
|
|
1759
|
+
response = self._client.delete(f"/api/v1/targets/{target_id}")
|
|
1760
|
+
response.raise_for_status()
|
|
1761
|
+
|
|
1739
1762
|
def create_tow_system(self, tow_system: TowSystemCreate) -> TowSystem:
|
|
1740
1763
|
"""Create a new tow system.
|
|
1741
1764
|
|
{clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/src/cti/api/sonar_wiz_async_api.py
RENAMED
|
@@ -1753,35 +1753,58 @@ class SonarWizAsyncApi:
|
|
|
1753
1753
|
response.raise_for_status()
|
|
1754
1754
|
return Target.model_validate(response.json())
|
|
1755
1755
|
|
|
1756
|
-
async def list_targets(self) -> list[Target]:
|
|
1757
|
-
"""List all targets.
|
|
1756
|
+
async def list_targets(self, sidescan_ping_source_id: UUID | str) -> list[Target]:
|
|
1757
|
+
"""List all targets for one sidescan ping source.
|
|
1758
|
+
|
|
1759
|
+
The cross-source ``GET /api/v1/targets`` endpoint is deprecated;
|
|
1760
|
+
the spec scopes target reads to a single SidescanPingSource.
|
|
1761
|
+
|
|
1762
|
+
Args:
|
|
1763
|
+
sidescan_ping_source_id: SidescanPingSource UUID or string identifier.
|
|
1758
1764
|
|
|
1759
1765
|
Returns:
|
|
1760
|
-
List of target instances
|
|
1766
|
+
List of target instances, each with the ``derived`` block
|
|
1767
|
+
populated when the parent line's nav state is available.
|
|
1761
1768
|
"""
|
|
1762
|
-
response = await self._client.get(
|
|
1769
|
+
response = await self._client.get(
|
|
1770
|
+
f"/api/v1/sidescan-ping-sources/{sidescan_ping_source_id}/targets"
|
|
1771
|
+
)
|
|
1763
1772
|
response.raise_for_status()
|
|
1764
1773
|
return [Target.model_validate(item) for item in response.json()]
|
|
1765
1774
|
|
|
1766
1775
|
async def update_target(
|
|
1767
1776
|
self, target_id: UUID | str, target: TargetUpdate
|
|
1768
1777
|
) -> Target:
|
|
1769
|
-
"""
|
|
1778
|
+
"""Partially update a target.
|
|
1779
|
+
|
|
1780
|
+
Targets MVP is the first endpoint in the API to use PATCH (older
|
|
1781
|
+
update endpoints use PUT for legacy reasons but already behave as
|
|
1782
|
+
PATCH server-side via ``exclude_unset=True``). Body fields are
|
|
1783
|
+
optional; only the supplied fields are mutated.
|
|
1770
1784
|
|
|
1771
1785
|
Args:
|
|
1772
1786
|
target_id: Target UUID or string identifier.
|
|
1773
|
-
target:
|
|
1787
|
+
target: Partial target update data.
|
|
1774
1788
|
|
|
1775
1789
|
Returns:
|
|
1776
1790
|
Updated target instance.
|
|
1777
1791
|
"""
|
|
1778
|
-
response = await self._client.
|
|
1792
|
+
response = await self._client.patch(
|
|
1779
1793
|
f"/api/v1/targets/{target_id}",
|
|
1780
1794
|
json=target.model_dump(mode="json", exclude_unset=True),
|
|
1781
1795
|
)
|
|
1782
1796
|
response.raise_for_status()
|
|
1783
1797
|
return Target.model_validate(response.json())
|
|
1784
1798
|
|
|
1799
|
+
async def delete_target(self, target_id: UUID | str) -> None:
|
|
1800
|
+
"""Hard-delete a target.
|
|
1801
|
+
|
|
1802
|
+
Args:
|
|
1803
|
+
target_id: Target UUID or string identifier.
|
|
1804
|
+
"""
|
|
1805
|
+
response = await self._client.delete(f"/api/v1/targets/{target_id}")
|
|
1806
|
+
response.raise_for_status()
|
|
1807
|
+
|
|
1785
1808
|
async def create_tow_system(self, tow_system: TowSystemCreate) -> TowSystem:
|
|
1786
1809
|
"""Create a new tow system.
|
|
1787
1810
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Pydantic models for target data.
|
|
2
|
+
|
|
3
|
+
Mirrors the sample-address shape introduced by clarity-server #119
|
|
4
|
+
(see ``clarity-server/src/model/target.py``). The ``derived`` block
|
|
5
|
+
required by the Targets MVP spec on every read response is computed by
|
|
6
|
+
``cti.positioning.target_geometry.derive`` and surfaced as
|
|
7
|
+
``Target.derived``; populating it server-side is clarity-server #120's
|
|
8
|
+
job.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_DEFAULT_NAME = "[unreviewed] [Unknown Object]"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TargetBase(BaseModel):
|
|
21
|
+
"""Sample-address fields shared across create/update/read models.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
channel: Sonar channel index (port/starboard/…).
|
|
25
|
+
ping: Ping address (along-track).
|
|
26
|
+
sample: Sample address within the ping (across-track, non-negative).
|
|
27
|
+
name: Operator-supplied name.
|
|
28
|
+
description: Optional free-form notes.
|
|
29
|
+
width_samples: Optional bounding-box width in sample addresses.
|
|
30
|
+
height_pings: Optional bounding-box height in ping addresses.
|
|
31
|
+
shadow_samples: Optional acoustic-shadow length in sample addresses.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
channel: int
|
|
35
|
+
ping: int
|
|
36
|
+
sample: int
|
|
37
|
+
name: str = _DEFAULT_NAME
|
|
38
|
+
description: str = ""
|
|
39
|
+
width_samples: int | None = None
|
|
40
|
+
height_pings: int | None = None
|
|
41
|
+
shadow_samples: int | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TargetCreate(TargetBase):
|
|
45
|
+
"""POST body shape — sample-address fields plus the parent FK.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
sidescan_ping_source_id: FK reference to SidescanPingSource.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
sidescan_ping_source_id: UUID
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TargetUpdate(BaseModel):
|
|
55
|
+
"""Partial update body — every field optional.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
channel: Sonar channel index.
|
|
59
|
+
ping: Ping address.
|
|
60
|
+
sample: Sample address.
|
|
61
|
+
name: Operator-supplied name.
|
|
62
|
+
description: Free-form notes.
|
|
63
|
+
width_samples: Bounding-box width in sample addresses.
|
|
64
|
+
height_pings: Bounding-box height in ping addresses.
|
|
65
|
+
shadow_samples: Acoustic-shadow length in sample addresses.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
channel: int | None = None
|
|
69
|
+
ping: int | None = None
|
|
70
|
+
sample: int | None = None
|
|
71
|
+
name: str | None = None
|
|
72
|
+
description: str | None = None
|
|
73
|
+
width_samples: int | None = None
|
|
74
|
+
height_pings: int | None = None
|
|
75
|
+
shadow_samples: int | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TargetDerived(BaseModel):
|
|
79
|
+
"""Read-only derived block — lat/lon, projected XY, SRID-unit dimensions.
|
|
80
|
+
|
|
81
|
+
Computed by :func:`cti.positioning.target_geometry.derive` from a
|
|
82
|
+
target's sample-address fields plus the parent line's current nav,
|
|
83
|
+
bottom track, and sample/ping spacing. Always populated on read
|
|
84
|
+
responses (no opt-in flag) per the Targets MVP spec.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
longitude: Geodetic longitude (EPSG:4326), decimal degrees.
|
|
88
|
+
latitude: Geodetic latitude (EPSG:4326), decimal degrees.
|
|
89
|
+
projected_x: Easting in the project SRID's linear unit.
|
|
90
|
+
projected_y: Northing in the project SRID's linear unit.
|
|
91
|
+
srid: Project SRID (e.g. ``"EPSG:32618"``).
|
|
92
|
+
width: Bounding-box width in SRID linear units, if width_samples set.
|
|
93
|
+
height: Bounding-box height in SRID linear units, if height_pings set.
|
|
94
|
+
shadow: Acoustic-shadow length in SRID linear units, if shadow_samples set.
|
|
95
|
+
linear_unit: SRID's linear unit name (typically ``"meter"``).
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
longitude: float
|
|
99
|
+
latitude: float
|
|
100
|
+
projected_x: float
|
|
101
|
+
projected_y: float
|
|
102
|
+
srid: str
|
|
103
|
+
width: float | None = None
|
|
104
|
+
height: float | None = None
|
|
105
|
+
shadow: float | None = None
|
|
106
|
+
linear_unit: str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Target(TargetBase):
|
|
110
|
+
"""Full read shape with database-managed fields and derived block.
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
target_id: Unique identifier for the target.
|
|
114
|
+
sidescan_ping_source_id: FK reference to SidescanPingSource.
|
|
115
|
+
derived: Derived geographic position and SRID-unit dimensions
|
|
116
|
+
(populated server-side; ``None`` only when the line lacks the
|
|
117
|
+
nav/bottom-track state required to compute it).
|
|
118
|
+
created_by_user_id: Authoring user (nullable until auth wiring lands).
|
|
119
|
+
last_modified_by_user_id: Last-editing user (nullable until auth wiring lands).
|
|
120
|
+
created_at: Row creation timestamp (UTC).
|
|
121
|
+
updated_at: Row last-modification timestamp (UTC).
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
target_id: UUID
|
|
125
|
+
sidescan_ping_source_id: UUID
|
|
126
|
+
derived: TargetDerived | None = None
|
|
127
|
+
created_by_user_id: UUID | None = None
|
|
128
|
+
last_modified_by_user_id: UUID | None = None
|
|
129
|
+
created_at: datetime
|
|
130
|
+
updated_at: datetime
|
|
131
|
+
|
|
132
|
+
model_config = ConfigDict(from_attributes=True)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Shared positioning math used by clarity-server and clarity-engine.
|
|
2
|
+
|
|
3
|
+
The Targets MVP spec requires that geographic position and SRID-unit
|
|
4
|
+
dimensions for a target be derived (not stored) from the parent line's
|
|
5
|
+
current nav, bottom track, and sample spacing — and that the engine and
|
|
6
|
+
server share one source of truth for that math. This package is that
|
|
7
|
+
source of truth.
|
|
8
|
+
"""
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Sample-address → geographic-position math for a single Target.
|
|
2
|
+
|
|
3
|
+
Given a target's ``(channel, ping, sample)`` address and the parent
|
|
4
|
+
line's per-ping nav state at that ping, produce the spec's ``derived``
|
|
5
|
+
block: geodetic lat/lon, projected XY in the project SRID, and the
|
|
6
|
+
optional SRID-unit width/height/shadow dimensions.
|
|
7
|
+
|
|
8
|
+
This module is the canonical implementation. Both ``clarity-server``
|
|
9
|
+
(read path: GET /targets/{id} populates ``Target.derived``) and
|
|
10
|
+
``clarity-engine`` (any internal use that needs target geometry) MUST
|
|
11
|
+
import :func:`derive` rather than re-implement it. See
|
|
12
|
+
``clarity-docs/designs/spec-targets-mvp.md`` §"Conversion responsibility
|
|
13
|
+
split".
|
|
14
|
+
|
|
15
|
+
Conventions
|
|
16
|
+
-----------
|
|
17
|
+
- ``channel == 0`` is port (left of vessel track).
|
|
18
|
+
- Any other ``channel`` is starboard (right of vessel track).
|
|
19
|
+
- Headings are compass bearings: 0° = north, increasing clockwise.
|
|
20
|
+
- Vessel position in :class:`LineState` is **projected** in the same
|
|
21
|
+
``target_srid`` passed to :func:`derive`. Both consumers (server and
|
|
22
|
+
engine) hold per-ping nav in projected coordinates after the
|
|
23
|
+
navigation phase (e.g. clarity-engine writes ``inner_x``/``inner_y``
|
|
24
|
+
in the project SRID), so taking projected input here avoids a
|
|
25
|
+
redundant geodetic round-trip.
|
|
26
|
+
- Slant range is corrected to ground range using the bottom-tracked
|
|
27
|
+
altitude when available; without altitude, slant range is used
|
|
28
|
+
directly (acceptable when the seafloor is shallow relative to range).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import math
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from functools import lru_cache
|
|
36
|
+
|
|
37
|
+
import pyproj
|
|
38
|
+
|
|
39
|
+
from cti.model.target import Target, TargetDerived
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_GEODETIC_CRS = "EPSG:4326"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class LineState:
|
|
47
|
+
"""Per-ping line state at a target's ping index.
|
|
48
|
+
|
|
49
|
+
Callers (typically the server's derive layer) extract these scalars
|
|
50
|
+
from the parent SidescanPingSource's nav/bottom-track Zarr arrays
|
|
51
|
+
and the sample-spacing computed from sound velocity and sample rate.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
projected_x: Vessel easting at the target's ping, in the project
|
|
55
|
+
SRID's linear unit (must match ``target_srid`` passed to
|
|
56
|
+
:func:`derive`).
|
|
57
|
+
projected_y: Vessel northing at the target's ping, same SRID.
|
|
58
|
+
heading_deg: Vessel compass heading at the target's ping
|
|
59
|
+
(0 = north, increasing clockwise).
|
|
60
|
+
sample_spacing_units: Across-track distance per sample, in the
|
|
61
|
+
project SRID's linear unit. Caller is responsible for the
|
|
62
|
+
metres-to-SRID-unit conversion before constructing the
|
|
63
|
+
LineState.
|
|
64
|
+
ping_spacing_units: Local along-track distance per ping, in the
|
|
65
|
+
project SRID's linear unit (typically metres for projected
|
|
66
|
+
SRIDs but feet for some state-plane SRIDs). Caller computes
|
|
67
|
+
this from consecutive vessel positions, which are themselves
|
|
68
|
+
already in SRID-projected coordinates, so the result inherits
|
|
69
|
+
the SRID's linear unit rather than being forced to metres.
|
|
70
|
+
altitude_units: Bottom-tracked altitude at the target's ping, in
|
|
71
|
+
SRID linear units, or ``None`` if no bottom track is
|
|
72
|
+
available (slant-to-ground correction is then skipped).
|
|
73
|
+
Caller converts from metres before constructing the LineState.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
projected_x: float
|
|
77
|
+
projected_y: float
|
|
78
|
+
heading_deg: float
|
|
79
|
+
sample_spacing_units: float
|
|
80
|
+
ping_spacing_units: float
|
|
81
|
+
altitude_units: float | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@lru_cache(maxsize=64)
|
|
85
|
+
def _transformer_from(target_srid: str) -> pyproj.Transformer:
|
|
86
|
+
"""Cached inverse transformer from ``target_srid`` to EPSG:4326."""
|
|
87
|
+
return pyproj.Transformer.from_crs(target_srid, _GEODETIC_CRS, always_xy=True)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@lru_cache(maxsize=64)
|
|
91
|
+
def _linear_unit(target_srid: str) -> str:
|
|
92
|
+
"""Linear unit name for ``target_srid`` (e.g. ``"metre"`` / ``"meter"``)."""
|
|
93
|
+
crs = pyproj.CRS.from_user_input(target_srid)
|
|
94
|
+
axis = crs.axis_info[0] if crs.axis_info else None
|
|
95
|
+
return axis.unit_name if axis is not None else "meter"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def derive( # pylint: disable=too-many-locals
|
|
99
|
+
target: Target, line_state: LineState, target_srid: str
|
|
100
|
+
) -> TargetDerived:
|
|
101
|
+
"""Derive a target's geographic position and SRID-unit dimensions.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
target: The target to position. Only ``channel``, ``sample``,
|
|
105
|
+
``width_samples``, ``height_pings``, and ``shadow_samples``
|
|
106
|
+
are read; ``ping`` is consumed by the caller when
|
|
107
|
+
constructing ``line_state``.
|
|
108
|
+
line_state: Per-ping nav + spacing at ``target.ping``.
|
|
109
|
+
target_srid: Project SRID to project into (e.g. ``"EPSG:32618"``).
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The derived block: geodetic lat/lon, projected XY in
|
|
113
|
+
``target_srid``, optional SRID-unit width/height/shadow, and the
|
|
114
|
+
SRID's linear-unit name.
|
|
115
|
+
"""
|
|
116
|
+
side = -1.0 if target.channel == 0 else 1.0
|
|
117
|
+
slant_range_units = target.sample * line_state.sample_spacing_units
|
|
118
|
+
ground_range_units = (
|
|
119
|
+
math.sqrt(slant_range_units**2 - line_state.altitude_units**2)
|
|
120
|
+
if line_state.altitude_units is not None
|
|
121
|
+
and slant_range_units > line_state.altitude_units
|
|
122
|
+
else slant_range_units
|
|
123
|
+
)
|
|
124
|
+
across_track_units = side * ground_range_units
|
|
125
|
+
|
|
126
|
+
perp_rad = math.radians(line_state.heading_deg) + math.pi / 2.0
|
|
127
|
+
target_x = line_state.projected_x + across_track_units * math.sin(perp_rad)
|
|
128
|
+
target_y = line_state.projected_y + across_track_units * math.cos(perp_rad)
|
|
129
|
+
|
|
130
|
+
inverse = _transformer_from(target_srid)
|
|
131
|
+
target_lon, target_lat = inverse.transform(target_x, target_y)
|
|
132
|
+
|
|
133
|
+
width = (
|
|
134
|
+
target.width_samples * line_state.sample_spacing_units
|
|
135
|
+
if target.width_samples is not None
|
|
136
|
+
else None
|
|
137
|
+
)
|
|
138
|
+
height = (
|
|
139
|
+
target.height_pings * line_state.ping_spacing_units
|
|
140
|
+
if target.height_pings is not None
|
|
141
|
+
else None
|
|
142
|
+
)
|
|
143
|
+
shadow = (
|
|
144
|
+
target.shadow_samples * line_state.sample_spacing_units
|
|
145
|
+
if target.shadow_samples is not None
|
|
146
|
+
else None
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return TargetDerived(
|
|
150
|
+
longitude=target_lon,
|
|
151
|
+
latitude=target_lat,
|
|
152
|
+
projected_x=target_x,
|
|
153
|
+
projected_y=target_y,
|
|
154
|
+
srid=target_srid,
|
|
155
|
+
width=width,
|
|
156
|
+
height=height,
|
|
157
|
+
shadow=shadow,
|
|
158
|
+
linear_unit=_linear_unit(target_srid),
|
|
159
|
+
)
|
{clarity_api_sdk_python-0.4.1 → clarity_api_sdk_python-0.4.3}/tests/test_sdk_async_methods.py
RENAMED
|
@@ -369,3 +369,163 @@ async def test_get_raw_file_device_mappings_accepts_string_uuids():
|
|
|
369
369
|
"/api/v1/raw-file-device-mappings",
|
|
370
370
|
params={"raw_file_id": str(RAW_FILE_ID), "device_id": str(DEVICE_ID)},
|
|
371
371
|
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
375
|
+
# Targets MVP — async client method coverage (issue #26)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
_TARGET_ID = UUID("00000000-0000-0000-0000-0000000000a1")
|
|
379
|
+
_SPS_ID = UUID("00000000-0000-0000-0000-0000000000a2")
|
|
380
|
+
_USER_ID = UUID("00000000-0000-0000-0000-000000000001")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _make_target_json(
|
|
384
|
+
*,
|
|
385
|
+
target_id: UUID = _TARGET_ID,
|
|
386
|
+
sidescan_ping_source_id: UUID = _SPS_ID,
|
|
387
|
+
name: str = "[unreviewed] [Unknown Object]",
|
|
388
|
+
derived: dict | None = None,
|
|
389
|
+
) -> dict:
|
|
390
|
+
"""Build a Target JSON dict matching the server's read response shape."""
|
|
391
|
+
return {
|
|
392
|
+
"target_id": str(target_id),
|
|
393
|
+
"sidescan_ping_source_id": str(sidescan_ping_source_id),
|
|
394
|
+
"channel": 0,
|
|
395
|
+
"ping": 100,
|
|
396
|
+
"sample": 250,
|
|
397
|
+
"name": name,
|
|
398
|
+
"description": "",
|
|
399
|
+
"width_samples": None,
|
|
400
|
+
"height_pings": None,
|
|
401
|
+
"shadow_samples": None,
|
|
402
|
+
"derived": derived,
|
|
403
|
+
"created_by_user_id": str(_USER_ID),
|
|
404
|
+
"last_modified_by_user_id": str(_USER_ID),
|
|
405
|
+
"created_at": "2026-05-05T12:00:00Z",
|
|
406
|
+
"updated_at": "2026-05-05T12:00:00Z",
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@pytest.mark.asyncio
|
|
411
|
+
async def test_create_target_posts_json_and_parses_target():
|
|
412
|
+
"""create_target POSTs sample-address JSON and returns a typed Target."""
|
|
413
|
+
from cti.model.target import TargetCreate
|
|
414
|
+
|
|
415
|
+
client = AsyncMock()
|
|
416
|
+
client.post.return_value = _mock_response(_make_target_json())
|
|
417
|
+
|
|
418
|
+
api = SonarWizAsyncApi(client)
|
|
419
|
+
payload = TargetCreate(
|
|
420
|
+
sidescan_ping_source_id=_SPS_ID,
|
|
421
|
+
channel=0,
|
|
422
|
+
ping=100,
|
|
423
|
+
sample=250,
|
|
424
|
+
)
|
|
425
|
+
result = await api.create_target(payload)
|
|
426
|
+
|
|
427
|
+
client.post.assert_awaited_once()
|
|
428
|
+
url, _ = client.post.call_args[0], client.post.call_args[1]
|
|
429
|
+
assert url[0] == "/api/v1/targets"
|
|
430
|
+
sent = client.post.call_args.kwargs["json"]
|
|
431
|
+
assert sent["sidescan_ping_source_id"] == str(_SPS_ID)
|
|
432
|
+
assert sent["channel"] == 0
|
|
433
|
+
assert sent["ping"] == 100
|
|
434
|
+
assert sent["sample"] == 250
|
|
435
|
+
assert result.target_id == _TARGET_ID
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@pytest.mark.asyncio
|
|
439
|
+
async def test_get_target_returns_typed_target_with_derived_block():
|
|
440
|
+
"""get_target hits /targets/{id} and surfaces the derived block."""
|
|
441
|
+
derived = {
|
|
442
|
+
"longitude": -76.123,
|
|
443
|
+
"latitude": 38.456,
|
|
444
|
+
"projected_x": 412345.67,
|
|
445
|
+
"projected_y": 4256789.01,
|
|
446
|
+
"srid": "EPSG:32618",
|
|
447
|
+
"width": None,
|
|
448
|
+
"height": None,
|
|
449
|
+
"shadow": None,
|
|
450
|
+
"linear_unit": "metre",
|
|
451
|
+
}
|
|
452
|
+
client = AsyncMock()
|
|
453
|
+
client.get.return_value = _mock_response(_make_target_json(derived=derived))
|
|
454
|
+
|
|
455
|
+
api = SonarWizAsyncApi(client)
|
|
456
|
+
result = await api.get_target(_TARGET_ID)
|
|
457
|
+
|
|
458
|
+
client.get.assert_awaited_once_with(f"/api/v1/targets/{_TARGET_ID}")
|
|
459
|
+
assert result.target_id == _TARGET_ID
|
|
460
|
+
assert result.derived is not None
|
|
461
|
+
assert result.derived.srid == "EPSG:32618"
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@pytest.mark.asyncio
|
|
465
|
+
async def test_list_targets_scopes_to_sidescan_ping_source():
|
|
466
|
+
"""list_targets hits the per-SPS endpoint, not the deprecated cross-source one."""
|
|
467
|
+
client = AsyncMock()
|
|
468
|
+
client.get.return_value = _mock_response(
|
|
469
|
+
[_make_target_json(target_id=UUID(int=i + 1), name=f"t{i}") for i in range(3)]
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
api = SonarWizAsyncApi(client)
|
|
473
|
+
results = await api.list_targets(_SPS_ID)
|
|
474
|
+
|
|
475
|
+
client.get.assert_awaited_once_with(
|
|
476
|
+
f"/api/v1/sidescan-ping-sources/{_SPS_ID}/targets"
|
|
477
|
+
)
|
|
478
|
+
assert len(results) == 3
|
|
479
|
+
assert {t.name for t in results} == {"t0", "t1", "t2"}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@pytest.mark.asyncio
|
|
483
|
+
async def test_update_target_uses_patch_with_partial_body():
|
|
484
|
+
"""update_target uses PATCH and serialises only the fields the caller set."""
|
|
485
|
+
from cti.model.target import TargetUpdate
|
|
486
|
+
|
|
487
|
+
client = AsyncMock()
|
|
488
|
+
client.patch.return_value = _mock_response(_make_target_json(name="renamed"))
|
|
489
|
+
|
|
490
|
+
api = SonarWizAsyncApi(client)
|
|
491
|
+
payload = TargetUpdate(name="renamed")
|
|
492
|
+
result = await api.update_target(_TARGET_ID, payload)
|
|
493
|
+
|
|
494
|
+
client.patch.assert_awaited_once()
|
|
495
|
+
assert client.patch.call_args[0][0] == f"/api/v1/targets/{_TARGET_ID}"
|
|
496
|
+
sent = client.patch.call_args.kwargs["json"]
|
|
497
|
+
# exclude_unset → only the field the caller set ships
|
|
498
|
+
assert sent == {"name": "renamed"}
|
|
499
|
+
assert result.name == "renamed"
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@pytest.mark.asyncio
|
|
503
|
+
async def test_delete_target_returns_none_and_calls_delete():
|
|
504
|
+
"""delete_target returns None after a successful DELETE."""
|
|
505
|
+
client = AsyncMock()
|
|
506
|
+
delete_resp = MagicMock(spec=httpx.Response)
|
|
507
|
+
delete_resp.status_code = 204
|
|
508
|
+
delete_resp.raise_for_status.return_value = None
|
|
509
|
+
client.delete.return_value = delete_resp
|
|
510
|
+
|
|
511
|
+
api = SonarWizAsyncApi(client)
|
|
512
|
+
result = await api.delete_target(_TARGET_ID)
|
|
513
|
+
|
|
514
|
+
client.delete.assert_awaited_once_with(f"/api/v1/targets/{_TARGET_ID}")
|
|
515
|
+
assert result is None
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@pytest.mark.asyncio
|
|
519
|
+
async def test_target_method_errors_propagate_via_raise_for_status():
|
|
520
|
+
"""Server errors bubble up as httpx.HTTPStatusError via raise_for_status."""
|
|
521
|
+
client = AsyncMock()
|
|
522
|
+
err_resp = MagicMock(spec=httpx.Response)
|
|
523
|
+
err_resp.status_code = 404
|
|
524
|
+
err_resp.raise_for_status.side_effect = httpx.HTTPStatusError(
|
|
525
|
+
"Not found", request=MagicMock(), response=err_resp
|
|
526
|
+
)
|
|
527
|
+
client.get.return_value = err_resp
|
|
528
|
+
|
|
529
|
+
api = SonarWizAsyncApi(client)
|
|
530
|
+
with pytest.raises(httpx.HTTPStatusError):
|
|
531
|
+
await api.get_target(_TARGET_ID)
|