clarity-api-sdk-python 0.3.35__tar.gz → 0.3.38__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 (59) hide show
  1. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/PKG-INFO +1 -1
  2. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/pyproject.toml +1 -1
  3. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/PKG-INFO +1 -1
  4. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/api/sonar_wiz_api.py +17 -9
  5. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/api/sonar_wiz_async_api.py +16 -10
  6. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/cli/main.py +228 -53
  7. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/processing_log.py +11 -4
  8. clarity_api_sdk_python-0.3.38/src/cti/model/raw_file_state.py +21 -0
  9. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/tests/test_cli.py +302 -65
  10. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/tests/test_sdk_async_methods.py +44 -17
  11. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/tests/test_sdk_methods.py +42 -20
  12. clarity_api_sdk_python-0.3.35/src/cti/model/raw_file_state.py +0 -12
  13. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/README.md +0 -0
  14. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/setup.cfg +0 -0
  15. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/SOURCES.txt +0 -0
  16. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/dependency_links.txt +0 -0
  17. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/entry_points.txt +0 -0
  18. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/requires.txt +0 -0
  19. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/top_level.txt +0 -0
  20. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/__init__.py +0 -0
  21. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/api/__init__.py +0 -0
  22. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/api/async_client.py +0 -0
  23. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/api/client.py +0 -0
  24. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/api/session.py +0 -0
  25. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/cli/__init__.py +0 -0
  26. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/cli/__main__.py +0 -0
  27. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/cli/client.py +0 -0
  28. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/logger/__init__.py +0 -0
  29. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/logger/logger.py +0 -0
  30. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/main.py +0 -0
  31. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/main_api.py +0 -0
  32. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/__init__.py +0 -0
  33. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/altitude_source.py +0 -0
  34. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/attitude_source.py +0 -0
  35. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/deferred_object_deletion.py +0 -0
  36. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/depth_source.py +0 -0
  37. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/device.py +0 -0
  38. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/device_type.py +0 -0
  39. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/final_product.py +0 -0
  40. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/hierarchy.py +0 -0
  41. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/layback_algorithm.py +0 -0
  42. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/layback_source.py +0 -0
  43. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/layback_type.py +0 -0
  44. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/organization.py +0 -0
  45. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/platform.py +0 -0
  46. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/platform_type.py +0 -0
  47. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/position_source.py +0 -0
  48. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/processing_job.py +0 -0
  49. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/project.py +0 -0
  50. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/projection_option.py +0 -0
  51. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/raw_file.py +0 -0
  52. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/raw_file_configuration.py +0 -0
  53. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/raw_file_device_mapping.py +0 -0
  54. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/s3.py +0 -0
  55. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/sidescan_ping_source.py +0 -0
  56. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/source.py +0 -0
  57. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/survey.py +0 -0
  58. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/target.py +0 -0
  59. {clarity_api_sdk_python-0.3.35 → clarity_api_sdk_python-0.3.38}/src/cti/model/tow_system.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clarity-api-sdk-python
3
- Version: 0.3.35
3
+ Version: 0.3.38
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.35"
8
+ version = "0.3.38"
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.35
3
+ Version: 0.3.38
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
@@ -2116,7 +2116,9 @@ class SonarWizApi:
2116
2116
  response.raise_for_status()
2117
2117
  configs = [RawFileConfiguration.model_validate(c) for c in response.json()]
2118
2118
  if not configs:
2119
- raise ValueError(f"No raw file configuration found for survey {survey_id}")
2119
+ raise ValueError(
2120
+ f"No raw file configuration found for survey {survey_id}"
2121
+ )
2120
2122
  raw_file_configuration_id = configs[0].raw_file_configuration_id
2121
2123
 
2122
2124
  # Initiate multipart upload
@@ -2156,7 +2158,9 @@ class SonarWizApi:
2156
2158
  f"/api/v1/rawfiles/{raw_file.raw_file_id}/upload-parts/{part_number}/url"
2157
2159
  )
2158
2160
  url_response.raise_for_status()
2159
- presigned_url = PartPresignedURL.model_validate(url_response.json()).url
2161
+ presigned_url = PartPresignedURL.model_validate(
2162
+ url_response.json()
2163
+ ).url
2160
2164
 
2161
2165
  # The server generates presigned URLs using its S3_ENDPOINT_URL,
2162
2166
  # which may be a Docker-internal hostname (e.g. minio:9000).
@@ -2210,23 +2214,27 @@ class SonarWizApi:
2210
2214
  )
2211
2215
  return completed
2212
2216
 
2213
- def trigger_processing(self, survey_id: UUID) -> ProcessingJob:
2214
- """Trigger processing for a survey.
2215
-
2216
- Creates a processing job and dispatches it via SQS.
2217
+ def process(self, survey_id: UUID, target_phase: str = "products") -> ProcessingJob:
2218
+ """Dispatch a pipeline run for a survey.
2217
2219
 
2218
2220
  Args:
2219
2221
  survey_id: Survey to process.
2222
+ target_phase: Pipeline phase to run up to. Defaults to "products"
2223
+ (full pipeline). "scan" runs only setup → scan. The engine's
2224
+ pull-chain resolves any required predecessor phases.
2220
2225
 
2221
2226
  Returns:
2222
2227
  The created ProcessingJob.
2223
2228
  """
2224
- response = self._client.post(f"/api/v1/ingest/{survey_id}")
2229
+ response = self._client.post(
2230
+ "/api/v1/process",
2231
+ json={"survey_id": str(survey_id), "target_phase": target_phase},
2232
+ )
2225
2233
  response.raise_for_status()
2226
2234
  data = response.json()
2227
2235
 
2228
- # The ingest endpoint returns a dict with job_id, not a full ProcessingJob.
2229
- # Fetch the full job record.
2236
+ # The /process endpoint returns a dict with job_id, not a full
2237
+ # ProcessingJob. Fetch the full job record.
2230
2238
  if job_id := data.get("job_id"):
2231
2239
  return self.get_job(job_id)
2232
2240
 
@@ -2164,7 +2164,9 @@ class SonarWizAsyncApi:
2164
2164
  response.raise_for_status()
2165
2165
  configs = [RawFileConfiguration.model_validate(c) for c in response.json()]
2166
2166
  if not configs:
2167
- raise ValueError(f"No raw file configuration found for survey {survey_id}")
2167
+ raise ValueError(
2168
+ f"No raw file configuration found for survey {survey_id}"
2169
+ )
2168
2170
  raw_file_configuration_id = configs[0].raw_file_configuration_id
2169
2171
 
2170
2172
  # Initiate multipart upload
@@ -2206,7 +2208,9 @@ class SonarWizAsyncApi:
2206
2208
  f"/api/v1/rawfiles/{raw_file.raw_file_id}/upload-parts/{part_number}/url"
2207
2209
  )
2208
2210
  url_response.raise_for_status()
2209
- presigned_url = PartPresignedURL.model_validate(url_response.json()).url
2211
+ presigned_url = PartPresignedURL.model_validate(
2212
+ url_response.json()
2213
+ ).url
2210
2214
 
2211
2215
  # Upload directly to S3/MinIO (not through API client)
2212
2216
  put_response = await s3_client.put(presigned_url, content=chunk)
@@ -2246,27 +2250,29 @@ class SonarWizAsyncApi:
2246
2250
  )
2247
2251
  return completed
2248
2252
 
2249
- async def trigger_processing(
2250
- self, survey_id: UUID
2253
+ async def process(
2254
+ self, survey_id: UUID, target_phase: str = "products"
2251
2255
  ) -> ProcessingJob:
2252
- """Trigger processing for a survey.
2253
-
2254
- Creates a processing job and dispatches it via SQS.
2256
+ """Dispatch a pipeline run for a survey.
2255
2257
 
2256
2258
  Args:
2257
2259
  survey_id: Survey to process.
2260
+ target_phase: Pipeline phase to run up to. Defaults to "products"
2261
+ (full pipeline). "scan" runs only setup → scan. The engine's
2262
+ pull-chain resolves any required predecessor phases.
2258
2263
 
2259
2264
  Returns:
2260
2265
  The created ProcessingJob.
2261
2266
  """
2262
2267
  response = await self._client.post(
2263
- f"/api/v1/ingest/{survey_id}",
2268
+ "/api/v1/process",
2269
+ json={"survey_id": str(survey_id), "target_phase": target_phase},
2264
2270
  )
2265
2271
  response.raise_for_status()
2266
2272
  data = response.json()
2267
2273
 
2268
- # The ingest endpoint returns a dict with job_id, not a full ProcessingJob.
2269
- # Fetch the full job record.
2274
+ # The /process endpoint returns a dict with job_id, not a full
2275
+ # ProcessingJob. Fetch the full job record.
2270
2276
  if job_id := data.get("job_id"):
2271
2277
  return await self.get_job(job_id)
2272
2278
 
@@ -18,9 +18,16 @@ app = typer.Typer(
18
18
  no_args_is_help=True,
19
19
  )
20
20
 
21
- list_app = typer.Typer(help="List resources (surveys, projects, jobs).", no_args_is_help=True)
21
+ list_app = typer.Typer(
22
+ help="List resources (surveys, projects, jobs).", no_args_is_help=True
23
+ )
22
24
  app.add_typer(list_app, name="list")
23
25
 
26
+ delete_app = typer.Typer(
27
+ help="Delete resources (rawfile, survey, project).", no_args_is_help=True
28
+ )
29
+ app.add_typer(delete_app, name="delete")
30
+
24
31
 
25
32
  def _output(data: dict, as_json: bool) -> None:
26
33
  """Print output as JSON or human-readable."""
@@ -42,10 +49,21 @@ def _get_error_detail(e: httpx.HTTPStatusError) -> str:
42
49
  def upload(
43
50
  file: Path = typer.Argument(..., help="Path to sonar file (.xtf, .jsf, etc.)"),
44
51
  survey: UUID = typer.Option(..., "--survey", "-s", help="Survey ID to upload to"),
45
- config_id: UUID | None = typer.Option(None, "--config", "-c", help="Raw file configuration ID (default: survey's default)"),
46
- no_mappings: bool = typer.Option(False, "--no-mappings", help="Skip automatic device mapping creation"),
47
- sidescan_stream: str = typer.Option("0", "--sidescan-stream", help="Source stream identifier for sidescan device"),
48
- gnss_stream: str = typer.Option("1", "--gnss-stream", help="Source stream identifier for GNSS device"),
52
+ config_id: UUID | None = typer.Option(
53
+ None,
54
+ "--config",
55
+ "-c",
56
+ help="Raw file configuration ID (default: survey's default)",
57
+ ),
58
+ no_mappings: bool = typer.Option(
59
+ False, "--no-mappings", help="Skip automatic device mapping creation"
60
+ ),
61
+ sidescan_stream: str = typer.Option(
62
+ "0", "--sidescan-stream", help="Source stream identifier for sidescan device"
63
+ ),
64
+ gnss_stream: str = typer.Option(
65
+ "1", "--gnss-stream", help="Source stream identifier for GNSS device"
66
+ ),
49
67
  output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
50
68
  ) -> None:
51
69
  """Upload a sonar file to a survey.
@@ -72,7 +90,9 @@ def upload(
72
90
  typer.echo(f"Error: {_get_error_detail(e)}", err=True)
73
91
  raise typer.Exit(1)
74
92
 
75
- typer.echo(f"Uploaded: {result.raw_file_id} ({result.file_name}, {result.file_size} bytes)")
93
+ typer.echo(
94
+ f"Uploaded: {result.raw_file_id} ({result.file_name}, {result.file_size} bytes)"
95
+ )
76
96
 
77
97
  # Auto-create device mappings
78
98
  if not no_mappings:
@@ -101,30 +121,45 @@ def upload(
101
121
  else:
102
122
  continue
103
123
 
104
- api.create_raw_file_device_mapping(RawFileDeviceMappingCreate(
105
- raw_file_id=result.raw_file_id,
106
- device_id=device.device_id,
107
- source_stream_identifier=stream_id,
108
- input_srid=4326,
109
- input_timezone=tz,
110
- ))
124
+ api.create_raw_file_device_mapping(
125
+ RawFileDeviceMappingCreate(
126
+ raw_file_id=result.raw_file_id,
127
+ device_id=device.device_id,
128
+ source_stream_identifier=stream_id,
129
+ input_srid=4326,
130
+ input_timezone=tz,
131
+ )
132
+ )
111
133
  typer.echo(f"Mapped: {device.name} → stream {stream_id}")
112
134
  mappings_created += 1
113
135
 
114
136
  if mappings_created == 0:
115
- typer.echo("Warning: no sidescan/GNSS devices found on survey platform — no mappings created", err=True)
137
+ typer.echo(
138
+ "Warning: no sidescan/GNSS devices found on survey platform — no mappings created",
139
+ err=True,
140
+ )
116
141
  except httpx.HTTPStatusError as e:
117
142
  if e.response.status_code == 404:
118
- typer.echo("Warning: no platform found on survey — skipping device mappings. Run 'ftm init' first.", err=True)
143
+ typer.echo(
144
+ "Warning: no platform found on survey — skipping device mappings. Run 'ftm init' first.",
145
+ err=True,
146
+ )
119
147
  else:
120
- typer.echo(f"Warning: failed to create device mappings: {_get_error_detail(e)}", err=True)
148
+ typer.echo(
149
+ f"Warning: failed to create device mappings: {_get_error_detail(e)}",
150
+ err=True,
151
+ )
121
152
 
122
153
  _output(
123
154
  {
124
155
  "raw_file_id": str(result.raw_file_id),
125
156
  "file_name": result.file_name,
126
157
  "file_size": result.file_size,
127
- "state": result.state.value if hasattr(result.state, "value") else str(result.state),
158
+ "state": (
159
+ result.state.value
160
+ if hasattr(result.state, "value")
161
+ else str(result.state)
162
+ ),
128
163
  },
129
164
  output_json,
130
165
  )
@@ -133,16 +168,25 @@ def upload(
133
168
  @app.command()
134
169
  def process(
135
170
  survey: UUID = typer.Argument(..., help="Survey ID to process"),
136
- wait: bool = typer.Option(False, "--wait", "-w", help="Wait for processing to complete"),
137
- timeout: float = typer.Option(300, "--timeout", "-t", help="Wait timeout in seconds"),
171
+ target_phase: str = typer.Option(
172
+ "products",
173
+ "--target-phase",
174
+ help="Pipeline phase to run up to. 'products' (default) runs the full pipeline; 'scan' runs scan only.",
175
+ ),
176
+ wait: bool = typer.Option(
177
+ False, "--wait", "-w", help="Wait for processing to complete"
178
+ ),
179
+ timeout: float = typer.Option(
180
+ 300, "--timeout", "-t", help="Wait timeout in seconds"
181
+ ),
138
182
  output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
139
183
  ) -> None:
140
184
  """Trigger processing for a survey."""
141
185
  api = get_api()
142
- typer.echo(f"Triggering processing for survey {survey}...")
186
+ typer.echo(f"Triggering processing for survey {survey} (target: {target_phase})...")
143
187
 
144
188
  try:
145
- job = api.trigger_processing(survey_id=survey)
189
+ job = api.process(survey_id=survey, target_phase=target_phase)
146
190
  except httpx.HTTPStatusError as e:
147
191
  typer.echo(f"Error: {_get_error_detail(e)}", err=True)
148
192
  raise typer.Exit(1)
@@ -231,12 +275,27 @@ def status(
231
275
  def init(
232
276
  project: str = typer.Option(..., "--project", "-p", help="Project name"),
233
277
  srid: str = typer.Option(..., "--srid", help="Projected CRS (e.g. EPSG:32618)"),
234
- timezone: str = typer.Option("Etc/UTC", "--timezone", "-tz", help="IANA timezone (default: Etc/UTC)"),
235
- org: str = typer.Option("Default Organization", "--org", "-o", help="Organization name (found or created)"),
236
- survey_name: Optional[str] = typer.Option(None, "--survey", "-s", help="Survey name (default: project name)"),
237
- description: Optional[str] = typer.Option(None, "--description", "-d", help="Survey description"),
238
- platform_type: str = typer.Option("Towed Body", "--platform-type", help="Platform type name (from seed data)"),
239
- platform_name: str = typer.Option("Platform", "--platform-name", help="Platform name"),
278
+ timezone: str = typer.Option(
279
+ "Etc/UTC", "--timezone", "-tz", help="IANA timezone (default: Etc/UTC)"
280
+ ),
281
+ org: str = typer.Option(
282
+ "Default Organization",
283
+ "--org",
284
+ "-o",
285
+ help="Organization name (found or created)",
286
+ ),
287
+ survey_name: Optional[str] = typer.Option(
288
+ None, "--survey", "-s", help="Survey name (default: project name)"
289
+ ),
290
+ description: Optional[str] = typer.Option(
291
+ None, "--description", "-d", help="Survey description"
292
+ ),
293
+ platform_type: str = typer.Option(
294
+ "Towed Body", "--platform-type", help="Platform type name (from seed data)"
295
+ ),
296
+ platform_name: str = typer.Option(
297
+ "Platform", "--platform-name", help="Platform name"
298
+ ),
240
299
  output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
241
300
  ) -> None:
242
301
  """Initialize a new project and survey for processing.
@@ -304,7 +363,10 @@ def init(
304
363
  pt = next((p for p in pt_list if p.name == platform_type), None)
305
364
  if not pt:
306
365
  available = ", ".join(p.name for p in pt_list)
307
- typer.echo(f"Error: platform type '{platform_type}' not found. Available: {available}", err=True)
366
+ typer.echo(
367
+ f"Error: platform type '{platform_type}' not found. Available: {available}",
368
+ err=True,
369
+ )
308
370
  raise typer.Exit(1)
309
371
  except httpx.HTTPStatusError as e:
310
372
  typer.echo(f"Error listing platform types: {_get_error_detail(e)}", err=True)
@@ -335,21 +397,43 @@ def init(
335
397
  sidescan_dt = dt_by_enum.get("sidescan_sonar")
336
398
  gnss_dt = dt_by_enum.get("gnss_gps_position_sensor")
337
399
  if not sidescan_dt or not gnss_dt:
338
- typer.echo("Error: required device types (sidescan_sonar, gnss_gps_position_sensor) not found in seed data", err=True)
400
+ typer.echo(
401
+ "Error: required device types (sidescan_sonar, gnss_gps_position_sensor) not found in seed data",
402
+ err=True,
403
+ )
339
404
  raise typer.Exit(1)
340
405
 
341
- device_defaults = dict(channel=0, offset_x=0, offset_y=0, offset_z=0, offset_heading=0, offset_pitch=0, offset_roll=0, latency=0)
406
+ device_defaults = dict(
407
+ channel=0,
408
+ offset_x=0,
409
+ offset_y=0,
410
+ offset_z=0,
411
+ offset_heading=0,
412
+ offset_pitch=0,
413
+ offset_roll=0,
414
+ latency=0,
415
+ )
342
416
  try:
343
- ss_device = api.create_device(DeviceCreate(
344
- platform_id=new_platform.platform_id, device_type_id=sidescan_dt.device_type_id,
345
- name="Sidescan", is_towed=True, **device_defaults,
346
- ))
417
+ ss_device = api.create_device(
418
+ DeviceCreate(
419
+ platform_id=new_platform.platform_id,
420
+ device_type_id=sidescan_dt.device_type_id,
421
+ name="Sidescan",
422
+ is_towed=True,
423
+ **device_defaults,
424
+ )
425
+ )
347
426
  typer.echo(f"Created device: Sidescan ({ss_device.device_id})")
348
427
 
349
- gnss_device = api.create_device(DeviceCreate(
350
- platform_id=new_platform.platform_id, device_type_id=gnss_dt.device_type_id,
351
- name="GNSS", is_towed=False, **device_defaults,
352
- ))
428
+ gnss_device = api.create_device(
429
+ DeviceCreate(
430
+ platform_id=new_platform.platform_id,
431
+ device_type_id=gnss_dt.device_type_id,
432
+ name="GNSS",
433
+ is_towed=False,
434
+ **device_defaults,
435
+ )
436
+ )
353
437
  typer.echo(f"Created device: GNSS ({gnss_device.device_id})")
354
438
  except httpx.HTTPStatusError as e:
355
439
  typer.echo(f"Error creating device: {_get_error_detail(e)}", err=True)
@@ -376,7 +460,9 @@ def init(
376
460
 
377
461
  @list_app.command("surveys")
378
462
  def list_surveys(
379
- project_name: Optional[str] = typer.Option(None, "--project", "-p", help="Filter by project name"),
463
+ project_name: Optional[str] = typer.Option(
464
+ None, "--project", "-p", help="Filter by project name"
465
+ ),
380
466
  output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
381
467
  ) -> None:
382
468
  """List surveys."""
@@ -393,18 +479,35 @@ def list_surveys(
393
479
  raise typer.Exit(0)
394
480
 
395
481
  if output_json:
396
- typer.echo(json.dumps(
397
- [{"survey_id": str(s.survey_id), "name": s.name, "project_id": str(s.project_id), "geodesy_srid": s.geodesy_srid, "timezone": s.timezone_name, "created": str(s.created_date)} for s in surveys],
398
- indent=2, default=str,
399
- ))
482
+ typer.echo(
483
+ json.dumps(
484
+ [
485
+ {
486
+ "survey_id": str(s.survey_id),
487
+ "name": s.name,
488
+ "project_id": str(s.project_id),
489
+ "geodesy_srid": s.geodesy_srid,
490
+ "timezone": s.timezone_name,
491
+ "created": str(s.created_date),
492
+ }
493
+ for s in surveys
494
+ ],
495
+ indent=2,
496
+ default=str,
497
+ )
498
+ )
400
499
  else:
401
500
  for s in surveys:
402
- typer.echo(f" {s.survey_id} {s.name} (srid={s.geodesy_srid}, tz={s.timezone_name})")
501
+ typer.echo(
502
+ f" {s.survey_id} {s.name} (srid={s.geodesy_srid}, tz={s.timezone_name})"
503
+ )
403
504
 
404
505
 
405
506
  @list_app.command("projects")
406
507
  def list_projects(
407
- org_name: Optional[str] = typer.Option(None, "--org", "-o", help="Filter by organization name"),
508
+ org_name: Optional[str] = typer.Option(
509
+ None, "--org", "-o", help="Filter by organization name"
510
+ ),
408
511
  output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
409
512
  ) -> None:
410
513
  """List projects."""
@@ -421,10 +524,21 @@ def list_projects(
421
524
  raise typer.Exit(0)
422
525
 
423
526
  if output_json:
424
- typer.echo(json.dumps(
425
- [{"project_id": str(p.project_id), "name": p.project_name, "organization_id": str(p.organization_id), "created": str(p.created_date)} for p in projects],
426
- indent=2, default=str,
427
- ))
527
+ typer.echo(
528
+ json.dumps(
529
+ [
530
+ {
531
+ "project_id": str(p.project_id),
532
+ "name": p.project_name,
533
+ "organization_id": str(p.organization_id),
534
+ "created": str(p.created_date),
535
+ }
536
+ for p in projects
537
+ ],
538
+ indent=2,
539
+ default=str,
540
+ )
541
+ )
428
542
  else:
429
543
  for p in projects:
430
544
  typer.echo(f" {p.project_id} {p.project_name}")
@@ -444,15 +558,76 @@ def list_jobs(
444
558
  raise typer.Exit(0)
445
559
 
446
560
  if output_json:
447
- typer.echo(json.dumps(
448
- [{"job_id": str(j.job_id), "status": j.status, "job_type": j.job_type, "created_at": str(j.created_at), "completed_at": str(j.completed_at) if j.completed_at else None} for j in result.jobs],
449
- indent=2, default=str,
450
- ))
561
+ typer.echo(
562
+ json.dumps(
563
+ [
564
+ {
565
+ "job_id": str(j.job_id),
566
+ "status": j.status,
567
+ "job_type": j.job_type,
568
+ "created_at": str(j.created_at),
569
+ "completed_at": str(j.completed_at) if j.completed_at else None,
570
+ }
571
+ for j in result.jobs
572
+ ],
573
+ indent=2,
574
+ default=str,
575
+ )
576
+ )
451
577
  else:
452
578
  for j in result.jobs:
453
579
  typer.echo(f" {j.job_id} {j.status} {j.job_type} {j.created_at}")
454
580
 
455
581
 
582
+ def _delete_resource(
583
+ path: str, resource_label: str, resource_id: UUID, yes: bool
584
+ ) -> None:
585
+ """Confirm and DELETE a resource by its API path. Exits non-zero on error."""
586
+ if not yes and not typer.confirm(f"Delete {resource_label} {resource_id}?"):
587
+ typer.echo("Aborted.")
588
+ raise typer.Exit(1)
589
+
590
+ api = get_api()
591
+ try:
592
+ response = api._client.delete(path)
593
+ response.raise_for_status()
594
+ except httpx.HTTPStatusError as e:
595
+ if e.response.status_code == 404:
596
+ typer.echo(f"Error: {resource_label} {resource_id} not found.", err=True)
597
+ else:
598
+ typer.echo(f"Error: {_get_error_detail(e)}", err=True)
599
+ raise typer.Exit(1)
600
+
601
+ typer.echo(f"Deleted {resource_label} {resource_id}.")
602
+
603
+
604
+ @delete_app.command("rawfile")
605
+ def delete_rawfile(
606
+ raw_file_id: UUID = typer.Argument(..., help="Raw file ID"),
607
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
608
+ ) -> None:
609
+ """Delete a raw file."""
610
+ _delete_resource(f"/api/v1/rawfiles/{raw_file_id}", "rawfile", raw_file_id, yes)
611
+
612
+
613
+ @delete_app.command("survey")
614
+ def delete_survey(
615
+ survey_id: UUID = typer.Argument(..., help="Survey ID"),
616
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
617
+ ) -> None:
618
+ """Delete a survey."""
619
+ _delete_resource(f"/api/v1/surveys/{survey_id}", "survey", survey_id, yes)
620
+
621
+
622
+ @delete_app.command("project")
623
+ def delete_project(
624
+ project_id: UUID = typer.Argument(..., help="Project ID"),
625
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
626
+ ) -> None:
627
+ """Delete a project."""
628
+ _delete_resource(f"/api/v1/projects/{project_id}", "project", project_id, yes)
629
+
630
+
456
631
  def main() -> None:
457
632
  """Entry point for the ftm CLI."""
458
633
  app()
@@ -17,6 +17,9 @@ class ProcessingLogBase(BaseModel):
17
17
  end_timestamp: End timestamp of processing.
18
18
  status: Status of the processing step.
19
19
  error_details: Error details if processing failed.
20
+ result: Processing step result payload (e.g., stream manifest JSON
21
+ for scan steps); opaque string.
22
+ processing_hash: Content-addressable cache key (SHA-256 hex digest).
20
23
  """
21
24
 
22
25
  processing_step: str
@@ -26,6 +29,8 @@ class ProcessingLogBase(BaseModel):
26
29
  end_timestamp: datetime
27
30
  status: str
28
31
  error_details: str | None = None
32
+ result: str | None = None
33
+ processing_hash: str
29
34
 
30
35
 
31
36
  class ProcessingLogCreate(ProcessingLogBase):
@@ -33,11 +38,11 @@ class ProcessingLogCreate(ProcessingLogBase):
33
38
 
34
39
  Attributes:
35
40
  survey_id: Foreign key reference to Survey.
36
- source_id: Foreign key reference to Source.
41
+ raw_file_id: Foreign key reference to RawFile.
37
42
  """
38
43
 
39
44
  survey_id: UUID
40
- source_id: UUID
45
+ raw_file_id: UUID
41
46
 
42
47
 
43
48
  class ProcessingLogUpdate(ProcessingLogBase):
@@ -51,6 +56,8 @@ class ProcessingLogUpdate(ProcessingLogBase):
51
56
  end_timestamp: End timestamp of processing.
52
57
  status: Status of the processing step.
53
58
  error_details: Error details if processing failed.
59
+ result: Processing step result payload.
60
+ processing_hash: Content-addressable cache key.
54
61
  """
55
62
 
56
63
 
@@ -60,11 +67,11 @@ class ProcessingLog(ProcessingLogBase):
60
67
  Attributes:
61
68
  processing_log_id: Unique identifier for the processing log.
62
69
  survey_id: Foreign key reference to Survey.
63
- source_id: Foreign key reference to Source.
70
+ raw_file_id: Foreign key reference to RawFile.
64
71
  """
65
72
 
66
73
  processing_log_id: UUID
67
74
  survey_id: UUID
68
- source_id: UUID
75
+ raw_file_id: UUID
69
76
 
70
77
  model_config = ConfigDict(from_attributes=True)
@@ -0,0 +1,21 @@
1
+ """Raw file state"""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class RawFileState(str, Enum):
7
+ """Status of a raw file through upload, scan, and ingest.
8
+
9
+ The legal transition graph is enforced server-side; this enum is a
10
+ passthrough carrying the value over the wire. See clarity-server
11
+ `model.raw_file_state` for the authoritative transition rules.
12
+ """
13
+
14
+ UPLOADING = "uploading"
15
+ READY = "ready"
16
+ ERROR_UPLOAD = "error_upload"
17
+ SCANNING = "scanning"
18
+ SCANNED = "scanned"
19
+ SCAN_ERROR = "scan_error"
20
+ ERROR_INGEST = "error_ingest"
21
+ INGEST_COMPLETE = "ingest_complete"