clarity-api-sdk-python 0.3.38__tar.gz → 0.4.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.
Files changed (60) hide show
  1. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/PKG-INFO +1 -1
  2. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/pyproject.toml +1 -1
  3. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/clarity_api_sdk_python.egg-info/PKG-INFO +1 -1
  4. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/clarity_api_sdk_python.egg-info/SOURCES.txt +2 -0
  5. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/api/sonar_wiz_api.py +104 -4
  6. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/api/sonar_wiz_async_api.py +108 -4
  7. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/cli/main.py +31 -6
  8. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/raw_file_device_mapping.py +43 -2
  9. clarity_api_sdk_python-0.4.0/src/cti/model/user_layer.py +125 -0
  10. clarity_api_sdk_python-0.4.0/tests/test_raw_file_device_mapping_model.py +110 -0
  11. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/tests/test_sdk_async_methods.py +78 -0
  12. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/tests/test_sdk_methods.py +55 -0
  13. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/README.md +0 -0
  14. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/setup.cfg +0 -0
  15. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/clarity_api_sdk_python.egg-info/dependency_links.txt +0 -0
  16. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/clarity_api_sdk_python.egg-info/entry_points.txt +0 -0
  17. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/clarity_api_sdk_python.egg-info/requires.txt +0 -0
  18. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/clarity_api_sdk_python.egg-info/top_level.txt +0 -0
  19. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/__init__.py +0 -0
  20. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/api/__init__.py +0 -0
  21. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/api/async_client.py +0 -0
  22. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/api/client.py +0 -0
  23. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/api/session.py +0 -0
  24. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/cli/__init__.py +0 -0
  25. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/cli/__main__.py +0 -0
  26. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/cli/client.py +0 -0
  27. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/logger/__init__.py +0 -0
  28. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/logger/logger.py +0 -0
  29. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/main.py +0 -0
  30. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/main_api.py +0 -0
  31. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/__init__.py +0 -0
  32. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/altitude_source.py +0 -0
  33. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/attitude_source.py +0 -0
  34. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/deferred_object_deletion.py +0 -0
  35. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/depth_source.py +0 -0
  36. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/device.py +0 -0
  37. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/device_type.py +0 -0
  38. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/final_product.py +0 -0
  39. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/hierarchy.py +0 -0
  40. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/layback_algorithm.py +0 -0
  41. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/layback_source.py +0 -0
  42. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/layback_type.py +0 -0
  43. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/organization.py +0 -0
  44. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/platform.py +0 -0
  45. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/platform_type.py +0 -0
  46. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/position_source.py +0 -0
  47. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/processing_job.py +0 -0
  48. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/processing_log.py +0 -0
  49. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/project.py +0 -0
  50. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/projection_option.py +0 -0
  51. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/raw_file.py +0 -0
  52. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/raw_file_configuration.py +0 -0
  53. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/raw_file_state.py +0 -0
  54. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/s3.py +0 -0
  55. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/sidescan_ping_source.py +0 -0
  56. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/source.py +0 -0
  57. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/survey.py +0 -0
  58. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/target.py +0 -0
  59. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/src/cti/model/tow_system.py +0 -0
  60. {clarity_api_sdk_python-0.3.38 → clarity_api_sdk_python-0.4.0}/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.38
3
+ Version: 0.4.0
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
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "clarity-api-sdk-python"
8
- version = "0.3.38"
8
+ version = "0.4.0"
9
9
  authors = [
10
10
  { name="Chesapeake Technology Inc.", email="support@chesapeaketech.com" },
11
11
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clarity-api-sdk-python
3
- Version: 0.3.38
3
+ Version: 0.4.0
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
 
@@ -1975,13 +1979,34 @@ class SonarWizApi:
1975
1979
  response.raise_for_status()
1976
1980
  return ProcessingLog.model_validate(response.json())
1977
1981
 
1978
- def list_processing_logs(self) -> list[ProcessingLog]:
1979
- """List all processing logs.
1982
+ def list_processing_logs(
1983
+ self,
1984
+ *,
1985
+ raw_file_id: UUID | str | None = None,
1986
+ survey_id: UUID | str | None = None,
1987
+ processing_step: str | None = None,
1988
+ ) -> list[ProcessingLog]:
1989
+ """List processing logs, optionally filtered.
1990
+
1991
+ Filters compose: each provided filter narrows the result set.
1992
+
1993
+ Args:
1994
+ raw_file_id: Match only logs for this raw file.
1995
+ survey_id: Match only logs for this survey.
1996
+ processing_step: Match only logs with this processing step
1997
+ (e.g. ``"scan"``, ``"ingest"``, ``"products"``).
1980
1998
 
1981
1999
  Returns:
1982
- List of processing log instances.
2000
+ List of matching processing logs (empty list if none match).
1983
2001
  """
1984
- response = self._client.get("/api/v1/processing-logs")
2002
+ params: dict[str, str] = {}
2003
+ if raw_file_id is not None:
2004
+ params["raw_file_id"] = str(raw_file_id)
2005
+ if survey_id is not None:
2006
+ params["survey_id"] = str(survey_id)
2007
+ if processing_step is not None:
2008
+ params["processing_step"] = processing_step
2009
+ response = self._client.get("/api/v1/processing-logs", params=params or None)
1985
2010
  response.raise_for_status()
1986
2011
  return [ProcessingLog.model_validate(item) for item in response.json()]
1987
2012
 
@@ -2004,6 +2029,81 @@ class SonarWizApi:
2004
2029
  response.raise_for_status()
2005
2030
  return ProcessingLog.model_validate(response.json())
2006
2031
 
2032
+ # ── User Layers ──────────────────────────────────────────────────────
2033
+
2034
+ def create_user_layer(self, user_layer: UserLayerCreate) -> UserLayerWithUpload:
2035
+ """Create a new user layer and initiate upload.
2036
+
2037
+ Args:
2038
+ user_layer: User layer creation data.
2039
+
2040
+ Returns:
2041
+ Created user layer instance with upload information.
2042
+ """
2043
+ response = self._client.post(
2044
+ "/api/v1/user-layers/upload",
2045
+ json=user_layer.model_dump(mode="json"),
2046
+ )
2047
+ response.raise_for_status()
2048
+ return UserLayerWithUpload.model_validate(response.json())
2049
+
2050
+ def get_user_layer(self, user_layer_id: UUID) -> UserLayer:
2051
+ """Fetch a user layer by ID.
2052
+
2053
+ Args:
2054
+ user_layer_id: User layer UUID to fetch.
2055
+
2056
+ Returns:
2057
+ UserLayer instance.
2058
+ """
2059
+ response = self._client.get(f"/api/v1/user-layers/{user_layer_id}")
2060
+ response.raise_for_status()
2061
+ return UserLayer.model_validate(response.json())
2062
+
2063
+ def list_user_layers(self, project_id: UUID) -> list[UserLayer]:
2064
+ """List all user layers for a project.
2065
+
2066
+ Args:
2067
+ project_id: Project UUID to filter by.
2068
+
2069
+ Returns:
2070
+ List of user layer instances.
2071
+ """
2072
+ response = self._client.get(
2073
+ "/api/v1/user-layers",
2074
+ params={"project_id": str(project_id)},
2075
+ )
2076
+ response.raise_for_status()
2077
+ return [UserLayer.model_validate(item) for item in response.json()]
2078
+
2079
+ def update_user_layer(
2080
+ self, user_layer_id: UUID, user_layer: UserLayerUpdate
2081
+ ) -> UserLayer:
2082
+ """Update a user layer.
2083
+
2084
+ Args:
2085
+ user_layer_id: User layer UUID to update.
2086
+ user_layer: User layer update data.
2087
+
2088
+ Returns:
2089
+ Updated user layer instance.
2090
+ """
2091
+ response = self._client.patch(
2092
+ f"/api/v1/user-layers/{user_layer_id}",
2093
+ json=user_layer.model_dump(mode="json", exclude_none=True),
2094
+ )
2095
+ response.raise_for_status()
2096
+ return UserLayer.model_validate(response.json())
2097
+
2098
+ def delete_user_layer(self, user_layer_id: UUID) -> None:
2099
+ """Delete a user layer.
2100
+
2101
+ Args:
2102
+ user_layer_id: User layer UUID to delete.
2103
+ """
2104
+ response = self._client.delete(f"/api/v1/user-layers/{user_layer_id}")
2105
+ response.raise_for_status()
2106
+
2007
2107
  # ── Processing Jobs ──────────────────────────────────────────────────
2008
2108
 
2009
2109
  def create_job(self, job_data: ProcessingJobCreate) -> ProcessingJob:
@@ -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
 
@@ -2023,13 +2027,36 @@ class SonarWizAsyncApi:
2023
2027
  response.raise_for_status()
2024
2028
  return ProcessingLog.model_validate(response.json())
2025
2029
 
2026
- async def list_processing_logs(self) -> list[ProcessingLog]:
2027
- """List all processing logs.
2030
+ async def list_processing_logs(
2031
+ self,
2032
+ *,
2033
+ raw_file_id: UUID | str | None = None,
2034
+ survey_id: UUID | str | None = None,
2035
+ processing_step: str | None = None,
2036
+ ) -> list[ProcessingLog]:
2037
+ """List processing logs, optionally filtered.
2038
+
2039
+ Filters compose: each provided filter narrows the result set.
2040
+
2041
+ Args:
2042
+ raw_file_id: Match only logs for this raw file.
2043
+ survey_id: Match only logs for this survey.
2044
+ processing_step: Match only logs with this processing step
2045
+ (e.g. ``"scan"``, ``"ingest"``, ``"products"``).
2028
2046
 
2029
2047
  Returns:
2030
- List of processing log instances.
2048
+ List of matching processing logs (empty list if none match).
2031
2049
  """
2032
- response = await self._client.get("/api/v1/processing-logs")
2050
+ params: dict[str, str] = {}
2051
+ if raw_file_id is not None:
2052
+ params["raw_file_id"] = str(raw_file_id)
2053
+ if survey_id is not None:
2054
+ params["survey_id"] = str(survey_id)
2055
+ if processing_step is not None:
2056
+ params["processing_step"] = processing_step
2057
+ response = await self._client.get(
2058
+ "/api/v1/processing-logs", params=params or None
2059
+ )
2033
2060
  response.raise_for_status()
2034
2061
  return [ProcessingLog.model_validate(item) for item in response.json()]
2035
2062
 
@@ -2052,6 +2079,83 @@ class SonarWizAsyncApi:
2052
2079
  response.raise_for_status()
2053
2080
  return ProcessingLog.model_validate(response.json())
2054
2081
 
2082
+ # ── User Layers ──────────────────────────────────────────────────────
2083
+
2084
+ async def create_user_layer(
2085
+ self, user_layer: UserLayerCreate
2086
+ ) -> UserLayerWithUpload:
2087
+ """Create a new user layer and initiate upload.
2088
+
2089
+ Args:
2090
+ user_layer: User layer creation data.
2091
+
2092
+ Returns:
2093
+ Created user layer instance with upload information.
2094
+ """
2095
+ response = await self._client.post(
2096
+ "/api/v1/user-layers/upload",
2097
+ json=user_layer.model_dump(mode="json"),
2098
+ )
2099
+ response.raise_for_status()
2100
+ return UserLayerWithUpload.model_validate(response.json())
2101
+
2102
+ async def get_user_layer(self, user_layer_id: UUID) -> UserLayer:
2103
+ """Fetch a user layer by ID.
2104
+
2105
+ Args:
2106
+ user_layer_id: User layer UUID to fetch.
2107
+
2108
+ Returns:
2109
+ UserLayer instance.
2110
+ """
2111
+ response = await self._client.get(f"/api/v1/user-layers/{user_layer_id}")
2112
+ response.raise_for_status()
2113
+ return UserLayer.model_validate(response.json())
2114
+
2115
+ async def list_user_layers(self, project_id: UUID) -> list[UserLayer]:
2116
+ """List all user layers for a project.
2117
+
2118
+ Args:
2119
+ project_id: Project UUID to filter by.
2120
+
2121
+ Returns:
2122
+ List of user layer instances.
2123
+ """
2124
+ response = await self._client.get(
2125
+ "/api/v1/user-layers",
2126
+ params={"project_id": str(project_id)},
2127
+ )
2128
+ response.raise_for_status()
2129
+ return [UserLayer.model_validate(item) for item in response.json()]
2130
+
2131
+ async def update_user_layer(
2132
+ self, user_layer_id: UUID, user_layer: UserLayerUpdate
2133
+ ) -> UserLayer:
2134
+ """Update a user layer.
2135
+
2136
+ Args:
2137
+ user_layer_id: User layer UUID to update.
2138
+ user_layer: User layer update data.
2139
+
2140
+ Returns:
2141
+ Updated user layer instance.
2142
+ """
2143
+ response = await self._client.patch(
2144
+ f"/api/v1/user-layers/{user_layer_id}",
2145
+ json=user_layer.model_dump(mode="json", exclude_none=True),
2146
+ )
2147
+ response.raise_for_status()
2148
+ return UserLayer.model_validate(response.json())
2149
+
2150
+ async def delete_user_layer(self, user_layer_id: UUID) -> None:
2151
+ """Delete a user layer.
2152
+
2153
+ Args:
2154
+ user_layer_id: User layer UUID to delete.
2155
+ """
2156
+ response = await self._client.delete(f"/api/v1/user-layers/{user_layer_id}")
2157
+ response.raise_for_status()
2158
+
2055
2159
  # ── Processing Jobs ──────────────────────────────────────────────────
2056
2160
 
2057
2161
  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
- "0", "--sidescan-stream", help="Source stream identifier for sidescan device"
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
- "1", "--gnss-stream", help="Source stream identifier for GNSS device"
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 RawFileDeviceMappingCreate
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 = sidescan_stream
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 = gnss_stream
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(f"Mapped: {device.name} → stream {stream_id}")
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: Identifier for the source stream.
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: str
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
+ )
@@ -18,6 +18,7 @@ from .conftest import (
18
18
  RAW_FILE_ID,
19
19
  SURVEY_ID,
20
20
  make_job_json,
21
+ make_processing_log_json,
21
22
  make_raw_file_json,
22
23
  make_raw_file_with_upload_json,
23
24
  )
@@ -219,3 +220,80 @@ async def test_wait_for_job_raises_on_timeout():
219
220
  with patch.object(loop, "time", side_effect=mock_time):
220
221
  with pytest.raises(TimeoutError, match="did not complete"):
221
222
  await api.wait_for_job(job_id=JOB_ID, timeout=10)
223
+
224
+
225
+ # ── list_processing_logs filter kwargs (WI-0106 A1) ─────────────────────
226
+
227
+
228
+ @pytest.mark.asyncio
229
+ async def test_list_processing_logs_no_filters_omits_params():
230
+ """No kwargs → request sent with params=None (preserves existing behavior)."""
231
+ client = AsyncMock()
232
+ client.get.return_value = _mock_response([make_processing_log_json()])
233
+
234
+ api = SonarWizAsyncApi(client)
235
+ logs = await api.list_processing_logs()
236
+
237
+ assert len(logs) == 1
238
+ client.get.assert_awaited_once_with("/api/v1/processing-logs", params=None)
239
+
240
+
241
+ @pytest.mark.asyncio
242
+ async def test_list_processing_logs_all_filters_passed_as_query_params():
243
+ """All three filters compose into the query string; UUIDs stringified."""
244
+ client = AsyncMock()
245
+ client.get.return_value = _mock_response(
246
+ [make_processing_log_json(processing_step="scan", result='{"streams":[]}')]
247
+ )
248
+
249
+ api = SonarWizAsyncApi(client)
250
+ logs = await api.list_processing_logs(
251
+ raw_file_id=RAW_FILE_ID,
252
+ survey_id=SURVEY_ID,
253
+ processing_step="scan",
254
+ )
255
+
256
+ client.get.assert_awaited_once_with(
257
+ "/api/v1/processing-logs",
258
+ params={
259
+ "raw_file_id": str(RAW_FILE_ID),
260
+ "survey_id": str(SURVEY_ID),
261
+ "processing_step": "scan",
262
+ },
263
+ )
264
+ assert logs[0].processing_step == "scan"
265
+ assert logs[0].result == '{"streams":[]}'
266
+ assert logs[0].processing_hash == "a" * 64
267
+
268
+
269
+ @pytest.mark.asyncio
270
+ async def test_list_processing_logs_partial_filter_only_includes_provided():
271
+ """Unset filters are omitted from the query string."""
272
+ client = AsyncMock()
273
+ client.get.return_value = _mock_response([])
274
+
275
+ api = SonarWizAsyncApi(client)
276
+ logs = await api.list_processing_logs(
277
+ raw_file_id=RAW_FILE_ID, processing_step="scan"
278
+ )
279
+
280
+ client.get.assert_awaited_once_with(
281
+ "/api/v1/processing-logs",
282
+ params={"raw_file_id": str(RAW_FILE_ID), "processing_step": "scan"},
283
+ )
284
+ assert logs == []
285
+
286
+
287
+ @pytest.mark.asyncio
288
+ async def test_list_processing_logs_accepts_string_uuid():
289
+ """String UUIDs are accepted and stringified the same way."""
290
+ client = AsyncMock()
291
+ client.get.return_value = _mock_response([])
292
+
293
+ api = SonarWizAsyncApi(client)
294
+ await api.list_processing_logs(raw_file_id=str(RAW_FILE_ID))
295
+
296
+ client.get.assert_awaited_once_with(
297
+ "/api/v1/processing-logs",
298
+ params={"raw_file_id": str(RAW_FILE_ID)},
299
+ )
@@ -19,6 +19,7 @@ from .conftest import (
19
19
  RAW_FILE_ID,
20
20
  SURVEY_ID,
21
21
  make_job_json,
22
+ make_processing_log_json,
22
23
  make_raw_file_json,
23
24
  make_raw_file_with_upload_json,
24
25
  )
@@ -262,3 +263,57 @@ class TestWaitForJob:
262
263
  mock_mono.side_effect = [0.0, 0.0, 0.0, 100.0]
263
264
  with pytest.raises(TimeoutError, match="did not complete"):
264
265
  api.wait_for_job(job_id=JOB_ID, timeout=10)
266
+
267
+
268
+ # ── list_processing_logs filter kwargs (WI-0106 A1) ─────────────────────
269
+
270
+
271
+ def test_list_processing_logs_no_filters_omits_params():
272
+ """No kwargs → request sent with params=None."""
273
+ client = MagicMock()
274
+ client.get.return_value = _mock_response([make_processing_log_json()])
275
+
276
+ api = SonarWizApi(client)
277
+ logs = api.list_processing_logs()
278
+
279
+ assert len(logs) == 1
280
+ client.get.assert_called_once_with("/api/v1/processing-logs", params=None)
281
+
282
+
283
+ def test_list_processing_logs_all_filters_passed_as_query_params():
284
+ """All three filters compose into the query string."""
285
+ client = MagicMock()
286
+ client.get.return_value = _mock_response(
287
+ [make_processing_log_json(processing_step="scan")]
288
+ )
289
+
290
+ api = SonarWizApi(client)
291
+ api.list_processing_logs(
292
+ raw_file_id=RAW_FILE_ID,
293
+ survey_id=SURVEY_ID,
294
+ processing_step="scan",
295
+ )
296
+
297
+ client.get.assert_called_once_with(
298
+ "/api/v1/processing-logs",
299
+ params={
300
+ "raw_file_id": str(RAW_FILE_ID),
301
+ "survey_id": str(SURVEY_ID),
302
+ "processing_step": "scan",
303
+ },
304
+ )
305
+
306
+
307
+ def test_list_processing_logs_partial_filter_only_includes_provided():
308
+ """Unset filters are omitted from the query string."""
309
+ client = MagicMock()
310
+ client.get.return_value = _mock_response([])
311
+
312
+ api = SonarWizApi(client)
313
+ logs = api.list_processing_logs(survey_id=SURVEY_ID)
314
+
315
+ client.get.assert_called_once_with(
316
+ "/api/v1/processing-logs",
317
+ params={"survey_id": str(SURVEY_ID)},
318
+ )
319
+ assert logs == []