clarity-api-sdk-python 0.3.36__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.36 → clarity_api_sdk_python-0.3.38}/PKG-INFO +1 -1
  2. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/pyproject.toml +1 -1
  3. {clarity_api_sdk_python-0.3.36 → 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.36 → clarity_api_sdk_python-0.3.38}/src/cti/api/sonar_wiz_api.py +17 -9
  5. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/api/sonar_wiz_async_api.py +16 -10
  6. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/cli/main.py +180 -55
  7. clarity_api_sdk_python-0.3.38/src/cti/model/raw_file_state.py +21 -0
  8. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/tests/test_cli.py +225 -71
  9. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/tests/test_sdk_async_methods.py +44 -17
  10. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/tests/test_sdk_methods.py +42 -20
  11. clarity_api_sdk_python-0.3.36/src/cti/model/raw_file_state.py +0 -12
  12. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/README.md +0 -0
  13. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/setup.cfg +0 -0
  14. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/SOURCES.txt +0 -0
  15. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/dependency_links.txt +0 -0
  16. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/entry_points.txt +0 -0
  17. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/requires.txt +0 -0
  18. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/clarity_api_sdk_python.egg-info/top_level.txt +0 -0
  19. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/__init__.py +0 -0
  20. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/api/__init__.py +0 -0
  21. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/api/async_client.py +0 -0
  22. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/api/client.py +0 -0
  23. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/api/session.py +0 -0
  24. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/cli/__init__.py +0 -0
  25. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/cli/__main__.py +0 -0
  26. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/cli/client.py +0 -0
  27. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/logger/__init__.py +0 -0
  28. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/logger/logger.py +0 -0
  29. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/main.py +0 -0
  30. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/main_api.py +0 -0
  31. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/__init__.py +0 -0
  32. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/altitude_source.py +0 -0
  33. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/attitude_source.py +0 -0
  34. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/deferred_object_deletion.py +0 -0
  35. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/depth_source.py +0 -0
  36. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/device.py +0 -0
  37. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/device_type.py +0 -0
  38. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/final_product.py +0 -0
  39. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/hierarchy.py +0 -0
  40. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/layback_algorithm.py +0 -0
  41. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/layback_source.py +0 -0
  42. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/layback_type.py +0 -0
  43. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/organization.py +0 -0
  44. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/platform.py +0 -0
  45. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/platform_type.py +0 -0
  46. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/position_source.py +0 -0
  47. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/processing_job.py +0 -0
  48. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/processing_log.py +0 -0
  49. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/project.py +0 -0
  50. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/projection_option.py +0 -0
  51. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/raw_file.py +0 -0
  52. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/raw_file_configuration.py +0 -0
  53. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/raw_file_device_mapping.py +0 -0
  54. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/s3.py +0 -0
  55. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/sidescan_ping_source.py +0 -0
  56. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/source.py +0 -0
  57. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/survey.py +0 -0
  58. {clarity_api_sdk_python-0.3.36 → clarity_api_sdk_python-0.3.38}/src/cti/model/target.py +0 -0
  59. {clarity_api_sdk_python-0.3.36 → 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.36
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.36"
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.36
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,10 +18,14 @@ 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
 
24
- delete_app = typer.Typer(help="Delete resources (rawfile, survey, project).", no_args_is_help=True)
26
+ delete_app = typer.Typer(
27
+ help="Delete resources (rawfile, survey, project).", no_args_is_help=True
28
+ )
25
29
  app.add_typer(delete_app, name="delete")
26
30
 
27
31
 
@@ -45,10 +49,21 @@ def _get_error_detail(e: httpx.HTTPStatusError) -> str:
45
49
  def upload(
46
50
  file: Path = typer.Argument(..., help="Path to sonar file (.xtf, .jsf, etc.)"),
47
51
  survey: UUID = typer.Option(..., "--survey", "-s", help="Survey ID to upload to"),
48
- config_id: UUID | None = typer.Option(None, "--config", "-c", help="Raw file configuration ID (default: survey's default)"),
49
- no_mappings: bool = typer.Option(False, "--no-mappings", help="Skip automatic device mapping creation"),
50
- sidescan_stream: str = typer.Option("0", "--sidescan-stream", help="Source stream identifier for sidescan device"),
51
- 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
+ ),
52
67
  output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
53
68
  ) -> None:
54
69
  """Upload a sonar file to a survey.
@@ -75,7 +90,9 @@ def upload(
75
90
  typer.echo(f"Error: {_get_error_detail(e)}", err=True)
76
91
  raise typer.Exit(1)
77
92
 
78
- 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
+ )
79
96
 
80
97
  # Auto-create device mappings
81
98
  if not no_mappings:
@@ -104,30 +121,45 @@ def upload(
104
121
  else:
105
122
  continue
106
123
 
107
- api.create_raw_file_device_mapping(RawFileDeviceMappingCreate(
108
- raw_file_id=result.raw_file_id,
109
- device_id=device.device_id,
110
- source_stream_identifier=stream_id,
111
- input_srid=4326,
112
- input_timezone=tz,
113
- ))
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
+ )
114
133
  typer.echo(f"Mapped: {device.name} → stream {stream_id}")
115
134
  mappings_created += 1
116
135
 
117
136
  if mappings_created == 0:
118
- 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
+ )
119
141
  except httpx.HTTPStatusError as e:
120
142
  if e.response.status_code == 404:
121
- 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
+ )
122
147
  else:
123
- 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
+ )
124
152
 
125
153
  _output(
126
154
  {
127
155
  "raw_file_id": str(result.raw_file_id),
128
156
  "file_name": result.file_name,
129
157
  "file_size": result.file_size,
130
- "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
+ ),
131
163
  },
132
164
  output_json,
133
165
  )
@@ -136,16 +168,25 @@ def upload(
136
168
  @app.command()
137
169
  def process(
138
170
  survey: UUID = typer.Argument(..., help="Survey ID to process"),
139
- wait: bool = typer.Option(False, "--wait", "-w", help="Wait for processing to complete"),
140
- 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
+ ),
141
182
  output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
142
183
  ) -> None:
143
184
  """Trigger processing for a survey."""
144
185
  api = get_api()
145
- typer.echo(f"Triggering processing for survey {survey}...")
186
+ typer.echo(f"Triggering processing for survey {survey} (target: {target_phase})...")
146
187
 
147
188
  try:
148
- job = api.trigger_processing(survey_id=survey)
189
+ job = api.process(survey_id=survey, target_phase=target_phase)
149
190
  except httpx.HTTPStatusError as e:
150
191
  typer.echo(f"Error: {_get_error_detail(e)}", err=True)
151
192
  raise typer.Exit(1)
@@ -234,12 +275,27 @@ def status(
234
275
  def init(
235
276
  project: str = typer.Option(..., "--project", "-p", help="Project name"),
236
277
  srid: str = typer.Option(..., "--srid", help="Projected CRS (e.g. EPSG:32618)"),
237
- timezone: str = typer.Option("Etc/UTC", "--timezone", "-tz", help="IANA timezone (default: Etc/UTC)"),
238
- org: str = typer.Option("Default Organization", "--org", "-o", help="Organization name (found or created)"),
239
- survey_name: Optional[str] = typer.Option(None, "--survey", "-s", help="Survey name (default: project name)"),
240
- description: Optional[str] = typer.Option(None, "--description", "-d", help="Survey description"),
241
- platform_type: str = typer.Option("Towed Body", "--platform-type", help="Platform type name (from seed data)"),
242
- 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
+ ),
243
299
  output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
244
300
  ) -> None:
245
301
  """Initialize a new project and survey for processing.
@@ -307,7 +363,10 @@ def init(
307
363
  pt = next((p for p in pt_list if p.name == platform_type), None)
308
364
  if not pt:
309
365
  available = ", ".join(p.name for p in pt_list)
310
- 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
+ )
311
370
  raise typer.Exit(1)
312
371
  except httpx.HTTPStatusError as e:
313
372
  typer.echo(f"Error listing platform types: {_get_error_detail(e)}", err=True)
@@ -338,21 +397,43 @@ def init(
338
397
  sidescan_dt = dt_by_enum.get("sidescan_sonar")
339
398
  gnss_dt = dt_by_enum.get("gnss_gps_position_sensor")
340
399
  if not sidescan_dt or not gnss_dt:
341
- 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
+ )
342
404
  raise typer.Exit(1)
343
405
 
344
- 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
+ )
345
416
  try:
346
- ss_device = api.create_device(DeviceCreate(
347
- platform_id=new_platform.platform_id, device_type_id=sidescan_dt.device_type_id,
348
- name="Sidescan", is_towed=True, **device_defaults,
349
- ))
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
+ )
350
426
  typer.echo(f"Created device: Sidescan ({ss_device.device_id})")
351
427
 
352
- gnss_device = api.create_device(DeviceCreate(
353
- platform_id=new_platform.platform_id, device_type_id=gnss_dt.device_type_id,
354
- name="GNSS", is_towed=False, **device_defaults,
355
- ))
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
+ )
356
437
  typer.echo(f"Created device: GNSS ({gnss_device.device_id})")
357
438
  except httpx.HTTPStatusError as e:
358
439
  typer.echo(f"Error creating device: {_get_error_detail(e)}", err=True)
@@ -379,7 +460,9 @@ def init(
379
460
 
380
461
  @list_app.command("surveys")
381
462
  def list_surveys(
382
- 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
+ ),
383
466
  output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
384
467
  ) -> None:
385
468
  """List surveys."""
@@ -396,18 +479,35 @@ def list_surveys(
396
479
  raise typer.Exit(0)
397
480
 
398
481
  if output_json:
399
- typer.echo(json.dumps(
400
- [{"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],
401
- indent=2, default=str,
402
- ))
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
+ )
403
499
  else:
404
500
  for s in surveys:
405
- 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
+ )
406
504
 
407
505
 
408
506
  @list_app.command("projects")
409
507
  def list_projects(
410
- 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
+ ),
411
511
  output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
412
512
  ) -> None:
413
513
  """List projects."""
@@ -424,10 +524,21 @@ def list_projects(
424
524
  raise typer.Exit(0)
425
525
 
426
526
  if output_json:
427
- typer.echo(json.dumps(
428
- [{"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],
429
- indent=2, default=str,
430
- ))
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
+ )
431
542
  else:
432
543
  for p in projects:
433
544
  typer.echo(f" {p.project_id} {p.project_name}")
@@ -447,16 +558,30 @@ def list_jobs(
447
558
  raise typer.Exit(0)
448
559
 
449
560
  if output_json:
450
- typer.echo(json.dumps(
451
- [{"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],
452
- indent=2, default=str,
453
- ))
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
+ )
454
577
  else:
455
578
  for j in result.jobs:
456
579
  typer.echo(f" {j.job_id} {j.status} {j.job_type} {j.created_at}")
457
580
 
458
581
 
459
- def _delete_resource(path: str, resource_label: str, resource_id: UUID, yes: bool) -> None:
582
+ def _delete_resource(
583
+ path: str, resource_label: str, resource_id: UUID, yes: bool
584
+ ) -> None:
460
585
  """Confirm and DELETE a resource by its API path. Exits non-zero on error."""
461
586
  if not yes and not typer.confirm(f"Delete {resource_label} {resource_id}?"):
462
587
  typer.echo("Aborted.")
@@ -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"