Files
insar-management-system-v2/backend/app/routers/sbas_insar_production.py
T

699 lines
28 KiB
Python

from __future__ import annotations
import asyncio
import mimetypes
import subprocess
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field, field_validator, model_validator
from ..services.job_queue_service import job_queue_service
from ..services.sbas_insar_production_service import sbas_insar_production_service
from ..services.task_service import task_service
router = APIRouter(prefix="/sbas-insar-production", tags=["sbas-insar-production"])
class SbasAoiBbox(BaseModel):
min_lon: float = Field(ge=-180, le=180)
min_lat: float = Field(ge=-90, le=90)
max_lon: float = Field(ge=-180, le=180)
max_lat: float = Field(ge=-90, le=90)
@model_validator(mode="after")
def _validate_order(self):
if self.min_lon >= self.max_lon or self.min_lat >= self.max_lat:
raise ValueError("aoi_bbox min values must be smaller than max values")
return self
class SbasStackDiscoverRequest(BaseModel):
source_roots: list[str] | None = None
orbit_roots: list[str] | None = None
min_scenes: int = Field(default=3, ge=2, le=100)
require_orbits: bool = True
include_scenes: bool = False
limit: int = Field(default=30, ge=0, le=500)
platform: str | None = Field(default=None, max_length=16)
relative_orbit: str | None = Field(default=None, max_length=32)
orbit_direction: str | None = Field(default=None, max_length=32)
admin_region: str | None = Field(default=None, max_length=120)
discovery_mode: str = Field(default="strict", pattern="^(strict|aoi)$")
aoi_bbox: SbasAoiBbox | None = None
min_aoi_coverage_ratio: float = Field(default=0.01, ge=0, le=1)
min_common_overlap_ratio: float = Field(default=0.0, ge=0, le=1)
@field_validator("source_roots", "orbit_roots", mode="before")
@classmethod
def _normalize_roots(cls, value):
if value is None:
return None
if isinstance(value, str):
items = [value]
else:
items = list(value)
cleaned = [str(item or "").strip() for item in items if str(item or "").strip()]
return cleaned or None
@field_validator("platform", "relative_orbit", "orbit_direction", "admin_region", mode="before")
@classmethod
def _normalize_optional_text(cls, value):
if value is None:
return None
text = str(value).strip()
return text or None
@field_validator("discovery_mode", mode="before")
@classmethod
def _normalize_discovery_mode(cls, value):
text = str(value or "strict").strip().lower()
return text or "strict"
class SbasMonitorPoint(BaseModel):
point_id: str | None = Field(default=None, max_length=64)
label: str | None = Field(default=None, max_length=120)
lon: float = Field(ge=-180, le=180)
lat: float = Field(ge=-90, le=90)
class SbasRunSubmitRequest(SbasStackDiscoverRequest):
run_label: str | None = Field(default=None, max_length=120)
dry_run: bool = True
monitor_point_strategy: str = Field(default="auto_representative_points", max_length=64)
monitor_points: list[SbasMonitorPoint] | None = None
class SbasBaselineAuditRequest(BaseModel):
execute: bool = True
rlks: int = Field(default=8, ge=1, le=64)
azlks: int = Field(default=8, ge=1, le=64)
max_delta_n: int = Field(default=1, ge=1, le=100)
timeout_seconds: int = Field(default=21600, ge=60, le=86400)
class SbasItabDecisionRequest(BaseModel):
decision: str = Field(pattern="^(approve|reject)$")
reviewer: str | None = Field(default=None, max_length=120)
note: str | None = Field(default=None, max_length=1000)
class SbasCoregistrationRequest(BaseModel):
execute: bool = False
rlks: int = Field(default=8, ge=1, le=64)
azlks: int = Field(default=8, ge=1, le=64)
class SbasCoregistrationJobRequest(BaseModel):
rlks: int = Field(default=8, ge=1, le=64)
azlks: int = Field(default=8, ge=1, le=64)
timeout_seconds: int = Field(default=43200, ge=60, le=172800)
class SbasRdcDemRequest(BaseModel):
execute: bool = False
rlks: int = Field(default=8, ge=1, le=64)
class SbasRdcDemJobRequest(BaseModel):
rlks: int = Field(default=8, ge=1, le=64)
timeout_seconds: int = Field(default=43200, ge=60, le=172800)
class SbasInterferogramsRequest(BaseModel):
execute: bool = False
rlks: int = Field(default=8, ge=1, le=64)
azlks: int = Field(default=8, ge=1, le=64)
unwrap_threshold: float = Field(default=0.20, ge=0.01, le=0.95)
class SbasInterferogramsJobRequest(BaseModel):
rlks: int = Field(default=8, ge=1, le=64)
azlks: int = Field(default=8, ge=1, le=64)
unwrap_threshold: float = Field(default=0.20, ge=0.01, le=0.95)
timeout_seconds: int = Field(default=43200, ge=60, le=172800)
class SbasIptaTimeseriesRequest(BaseModel):
execute: bool = False
rlks: int = Field(default=8, ge=1, le=64)
reference_window: int = Field(default=16, ge=1, le=256)
mb_mode: int = Field(default=0, ge=0, le=2)
class SbasIptaTimeseriesJobRequest(BaseModel):
rlks: int = Field(default=8, ge=1, le=64)
reference_window: int = Field(default=16, ge=1, le=256)
mb_mode: int = Field(default=0, ge=0, le=2)
timeout_seconds: int = Field(default=43200, ge=60, le=172800)
class SbasWorkflowPrepareRequest(BaseModel):
force: bool = False
rlks: int = Field(default=8, ge=1, le=64)
azlks: int = Field(default=8, ge=1, le=64)
reference_window: int = Field(default=16, ge=1, le=256)
mb_mode: int = Field(default=0, ge=0, le=2)
class SbasWorkflowJobRequest(SbasWorkflowPrepareRequest):
from_step: str | None = Field(default=None, max_length=64)
to_step: str | None = Field(default=None, max_length=64)
only_steps: list[str] | None = None
timeout_seconds: int = Field(default=172800, ge=60, le=604800)
@router.get("/capabilities")
async def get_sbas_insar_capabilities():
return sbas_insar_production_service.get_capabilities()
@router.post("/stacks/discover")
async def discover_sbas_insar_stacks(request: SbasStackDiscoverRequest):
try:
return await asyncio.to_thread(
sbas_insar_production_service.discover_stacks,
source_roots=request.source_roots,
orbit_roots=request.orbit_roots,
min_scenes=request.min_scenes,
require_orbits=request.require_orbits,
include_scenes=request.include_scenes,
limit=request.limit,
platform=request.platform,
relative_orbit=request.relative_orbit,
orbit_direction=request.orbit_direction,
admin_region=request.admin_region,
discovery_mode=request.discovery_mode,
aoi_bbox=request.aoi_bbox.model_dump() if request.aoi_bbox else None,
min_aoi_coverage_ratio=request.min_aoi_coverage_ratio,
min_common_overlap_ratio=request.min_common_overlap_ratio,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/stacks/{stack_id}/audit")
async def audit_sbas_insar_stack(stack_id: str, request: SbasStackDiscoverRequest):
try:
return await asyncio.to_thread(
sbas_insar_production_service.audit_stack,
stack_id,
source_roots=request.source_roots,
orbit_roots=request.orbit_roots,
min_scenes=request.min_scenes,
require_orbits=request.require_orbits,
discovery_mode=request.discovery_mode,
admin_region=request.admin_region,
aoi_bbox=request.aoi_bbox.model_dump() if request.aoi_bbox else None,
min_aoi_coverage_ratio=request.min_aoi_coverage_ratio,
min_common_overlap_ratio=request.min_common_overlap_ratio,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/stacks/{stack_id}/runs", status_code=202)
async def submit_sbas_insar_run(stack_id: str, request: SbasRunSubmitRequest):
try:
return await asyncio.to_thread(
sbas_insar_production_service.create_run,
stack_id,
run_label=request.run_label,
source_roots=request.source_roots,
orbit_roots=request.orbit_roots,
min_scenes=request.min_scenes,
require_orbits=request.require_orbits,
discovery_mode=request.discovery_mode,
admin_region=request.admin_region,
aoi_bbox=request.aoi_bbox.model_dump() if request.aoi_bbox else None,
min_aoi_coverage_ratio=request.min_aoi_coverage_ratio,
min_common_overlap_ratio=request.min_common_overlap_ratio,
monitor_points=[
point.model_dump(exclude_none=True)
for point in (request.monitor_points or [])
],
monitor_point_strategy=request.monitor_point_strategy,
dry_run=request.dry_run,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.get("/runs")
async def list_sbas_insar_runs():
return await asyncio.to_thread(sbas_insar_production_service.list_runs)
@router.get("/runs/{run_id}")
async def get_sbas_insar_run(run_id: str):
try:
return await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/runs/{run_id}/workflow", status_code=202)
async def prepare_sbas_insar_workflow(run_id: str, request: SbasWorkflowPrepareRequest):
try:
return await asyncio.to_thread(
sbas_insar_production_service.prepare_workflow,
run_id,
force=request.force,
rlks=request.rlks,
azlks=request.azlks,
reference_window=request.reference_window,
mb_mode=request.mb_mode,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/runs/{run_id}/workflow/jobs", status_code=202)
async def submit_sbas_insar_workflow_job(run_id: str, request: SbasWorkflowJobRequest):
try:
await asyncio.to_thread(
sbas_insar_production_service.prepare_workflow,
run_id,
force=request.force,
rlks=request.rlks,
azlks=request.azlks,
reference_window=request.reference_window,
mb_mode=request.mb_mode,
)
from ..services.job_handlers import JOB_TYPE_SBAS_GAMMA_WORKFLOW
payload = {
"run_id": run_id,
"force": request.force,
"rlks": request.rlks,
"azlks": request.azlks,
"reference_window": request.reference_window,
"mb_mode": request.mb_mode,
"from_step": request.from_step,
"to_step": request.to_step,
"only_steps": request.only_steps or [],
"timeout_seconds": request.timeout_seconds,
}
task_id = await task_service.create_task(
task_type=JOB_TYPE_SBAS_GAMMA_WORKFLOW,
task_name=f"Gamma SBAS Workflow {run_id}",
params=payload,
)
job_id = await job_queue_service.create_job(
job_type=JOB_TYPE_SBAS_GAMMA_WORKFLOW,
payload=payload,
task_id=task_id,
max_attempts=1,
)
return {
"message": "Gamma SBAS workflow job queued.",
"run_id": run_id,
"task_id": task_id,
"job_id": job_id,
"job_type": JOB_TYPE_SBAS_GAMMA_WORKFLOW,
}
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
message = str(exc)
status_code = 409 if "already running" in message.lower() or "conflict" in message.lower() else 400
raise HTTPException(status_code=status_code, detail=message) from exc
@router.post("/runs/{run_id}/baseline-audit", status_code=202)
async def run_sbas_insar_baseline_audit(run_id: str, request: SbasBaselineAuditRequest):
try:
return await asyncio.to_thread(
sbas_insar_production_service.run_baseline_audit,
run_id,
execute=request.execute,
rlks=request.rlks,
azlks=request.azlks,
max_delta_n=request.max_delta_n,
timeout_seconds=request.timeout_seconds,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except subprocess.TimeoutExpired as exc:
raise HTTPException(status_code=504, detail=f"baseline audit timed out after {exc.timeout}s") from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/runs/{run_id}/itab-decision")
async def decide_sbas_insar_itab(run_id: str, request: SbasItabDecisionRequest):
try:
return await asyncio.to_thread(
sbas_insar_production_service.decide_itab,
run_id,
decision=request.decision,
reviewer=request.reviewer,
note=request.note,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/runs/{run_id}/coregistration", status_code=202)
async def prepare_sbas_insar_coregistration(run_id: str, request: SbasCoregistrationRequest):
try:
return await asyncio.to_thread(
sbas_insar_production_service.prepare_coregistration,
run_id,
execute=request.execute,
rlks=request.rlks,
azlks=request.azlks,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/runs/{run_id}/coregistration/jobs", status_code=202)
async def submit_sbas_insar_coregistration_job(run_id: str, request: SbasCoregistrationJobRequest):
try:
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
status = str((run_detail.get("run") or {}).get("status") or "").strip()
if status in {"ITAB_APPROVED", "COREGISTRATION_FAILED"}:
await asyncio.to_thread(
sbas_insar_production_service.prepare_coregistration,
run_id,
execute=False,
rlks=request.rlks,
azlks=request.azlks,
)
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
status = str((run_detail.get("run") or {}).get("status") or "").strip()
if status not in {"COREGISTRATION_SCRIPT_READY", "COREGISTRATION_RUNNING"}:
raise ValueError(f"run status does not allow coregistration job submission: {status}")
if status == "COREGISTRATION_RUNNING":
raise ValueError("coregistration is already running for this run")
from ..services.job_handlers import JOB_TYPE_SBAS_COREGISTRATION
payload = {
"run_id": run_id,
"rlks": request.rlks,
"azlks": request.azlks,
"timeout_seconds": request.timeout_seconds,
}
task_id = await task_service.create_task(
task_type=JOB_TYPE_SBAS_COREGISTRATION,
task_name=f"SBAS-InSAR 共参考配准: {run_id}",
params=payload,
)
job_id = await job_queue_service.create_job(
job_type=JOB_TYPE_SBAS_COREGISTRATION,
payload=payload,
task_id=task_id,
max_attempts=1,
)
return {
"message": "SBAS-InSAR coregistration job queued.",
"run_id": run_id,
"task_id": task_id,
"job_id": job_id,
"job_type": JOB_TYPE_SBAS_COREGISTRATION,
}
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
message = str(exc)
status_code = 409 if "冲突" in message or "conflict" in message.lower() else 400
raise HTTPException(status_code=status_code, detail=message) from exc
@router.post("/runs/{run_id}/rdc-dem", status_code=202)
async def prepare_sbas_insar_rdc_dem(run_id: str, request: SbasRdcDemRequest):
try:
return await asyncio.to_thread(
sbas_insar_production_service.prepare_rdc_dem,
run_id,
execute=request.execute,
rlks=request.rlks,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/runs/{run_id}/rdc-dem/jobs", status_code=202)
async def submit_sbas_insar_rdc_dem_job(run_id: str, request: SbasRdcDemJobRequest):
try:
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
status = str((run_detail.get("run") or {}).get("status") or "").strip()
if status in {"COREGISTRATION_READY", "RDC_DEM_FAILED"}:
await asyncio.to_thread(
sbas_insar_production_service.prepare_rdc_dem,
run_id,
execute=False,
rlks=request.rlks,
)
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
status = str((run_detail.get("run") or {}).get("status") or "").strip()
if status not in {"RDC_DEM_SCRIPT_READY", "RDC_DEM_RUNNING"}:
raise ValueError(f"run status does not allow RDC DEM job submission: {status}")
if status == "RDC_DEM_RUNNING":
raise ValueError("RDC DEM is already running for this run")
from ..services.job_handlers import JOB_TYPE_SBAS_RDC_DEM
payload = {
"run_id": run_id,
"rlks": request.rlks,
"timeout_seconds": request.timeout_seconds,
}
task_id = await task_service.create_task(
task_type=JOB_TYPE_SBAS_RDC_DEM,
task_name=f"SBAS-InSAR RDC DEM {run_id}",
params=payload,
)
job_id = await job_queue_service.create_job(
job_type=JOB_TYPE_SBAS_RDC_DEM,
payload=payload,
task_id=task_id,
max_attempts=1,
)
return {
"message": "SBAS-InSAR RDC DEM job queued.",
"run_id": run_id,
"task_id": task_id,
"job_id": job_id,
"job_type": JOB_TYPE_SBAS_RDC_DEM,
}
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
message = str(exc)
status_code = 409 if "already running" in message.lower() or "conflict" in message.lower() else 400
raise HTTPException(status_code=status_code, detail=message) from exc
@router.post("/runs/{run_id}/interferograms", status_code=202)
async def prepare_sbas_insar_interferograms(run_id: str, request: SbasInterferogramsRequest):
try:
return await asyncio.to_thread(
sbas_insar_production_service.prepare_interferograms,
run_id,
execute=request.execute,
rlks=request.rlks,
azlks=request.azlks,
unwrap_threshold=request.unwrap_threshold,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/runs/{run_id}/interferograms/jobs", status_code=202)
async def submit_sbas_insar_interferograms_job(run_id: str, request: SbasInterferogramsJobRequest):
try:
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
status = str((run_detail.get("run") or {}).get("status") or "").strip()
if status in {"RDC_DEM_READY", "INTERFEROGRAMS_FAILED"}:
await asyncio.to_thread(
sbas_insar_production_service.prepare_interferograms,
run_id,
execute=False,
rlks=request.rlks,
azlks=request.azlks,
unwrap_threshold=request.unwrap_threshold,
)
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
status = str((run_detail.get("run") or {}).get("status") or "").strip()
if status not in {"INTERFEROGRAMS_SCRIPT_READY", "INTERFEROGRAMS_RUNNING"}:
raise ValueError(f"run status does not allow interferogram job submission: {status}")
if status == "INTERFEROGRAMS_RUNNING":
raise ValueError("interferograms are already running for this run")
from ..services.job_handlers import JOB_TYPE_SBAS_INTERFEROGRAMS
payload = {
"run_id": run_id,
"rlks": request.rlks,
"azlks": request.azlks,
"unwrap_threshold": request.unwrap_threshold,
"timeout_seconds": request.timeout_seconds,
}
task_id = await task_service.create_task(
task_type=JOB_TYPE_SBAS_INTERFEROGRAMS,
task_name=f"SBAS-InSAR Interferograms {run_id}",
params=payload,
)
job_id = await job_queue_service.create_job(
job_type=JOB_TYPE_SBAS_INTERFEROGRAMS,
payload=payload,
task_id=task_id,
max_attempts=1,
)
return {
"message": "SBAS-InSAR interferogram job queued.",
"run_id": run_id,
"task_id": task_id,
"job_id": job_id,
"job_type": JOB_TYPE_SBAS_INTERFEROGRAMS,
}
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
message = str(exc)
status_code = 409 if "already running" in message.lower() or "conflict" in message.lower() else 400
raise HTTPException(status_code=status_code, detail=message) from exc
@router.post("/runs/{run_id}/ipta-timeseries", status_code=202)
async def prepare_sbas_insar_ipta_timeseries(run_id: str, request: SbasIptaTimeseriesRequest):
try:
return await asyncio.to_thread(
sbas_insar_production_service.prepare_ipta_timeseries,
run_id,
execute=request.execute,
rlks=request.rlks,
reference_window=request.reference_window,
mb_mode=request.mb_mode,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/runs/{run_id}/ipta-timeseries/jobs", status_code=202)
async def submit_sbas_insar_ipta_timeseries_job(run_id: str, request: SbasIptaTimeseriesJobRequest):
try:
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
status = str((run_detail.get("run") or {}).get("status") or "").strip()
if status in {"INTERFEROGRAMS_READY", "IPTA_TIMESERIES_FAILED"}:
await asyncio.to_thread(
sbas_insar_production_service.prepare_ipta_timeseries,
run_id,
execute=False,
rlks=request.rlks,
reference_window=request.reference_window,
mb_mode=request.mb_mode,
)
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
status = str((run_detail.get("run") or {}).get("status") or "").strip()
if status not in {"IPTA_TIMESERIES_SCRIPT_READY", "IPTA_TIMESERIES_RUNNING"}:
raise ValueError(f"run status does not allow IPTA time-series job submission: {status}")
if status == "IPTA_TIMESERIES_RUNNING":
raise ValueError("IPTA time-series is already running for this run")
from ..services.job_handlers import JOB_TYPE_SBAS_IPTA_TIMESERIES
payload = {
"run_id": run_id,
"rlks": request.rlks,
"reference_window": request.reference_window,
"mb_mode": request.mb_mode,
"timeout_seconds": request.timeout_seconds,
}
task_id = await task_service.create_task(
task_type=JOB_TYPE_SBAS_IPTA_TIMESERIES,
task_name=f"SBAS-InSAR IPTA Timeseries {run_id}",
params=payload,
)
job_id = await job_queue_service.create_job(
job_type=JOB_TYPE_SBAS_IPTA_TIMESERIES,
payload=payload,
task_id=task_id,
max_attempts=1,
)
return {
"message": "SBAS-InSAR IPTA time-series job queued.",
"run_id": run_id,
"task_id": task_id,
"job_id": job_id,
"job_type": JOB_TYPE_SBAS_IPTA_TIMESERIES,
}
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
message = str(exc)
status_code = 409 if "already running" in message.lower() or "conflict" in message.lower() else 400
raise HTTPException(status_code=status_code, detail=message) from exc
@router.get("/runs/{run_id}/artifacts/{relative_path:path}")
async def get_sbas_insar_run_artifact(run_id: str, relative_path: str):
try:
artifact_path = sbas_insar_production_service.resolve_run_artifact_path(run_id, relative_path)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
media_type = mimetypes.guess_type(str(artifact_path))[0] or "application/octet-stream"
return FileResponse(
artifact_path,
media_type=media_type,
filename=artifact_path.name,
)
@router.get("/trial-runs")
async def list_sbas_insar_trial_runs():
return await asyncio.to_thread(sbas_insar_production_service.list_trial_runs)
@router.get("/trial-runs/{trial_id}")
async def get_sbas_insar_trial_run(trial_id: str):
try:
return await asyncio.to_thread(sbas_insar_production_service.get_trial_detail, trial_id)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.get("/trial-runs/{trial_id}/artifacts/{relative_path:path}")
async def get_sbas_insar_artifact(trial_id: str, relative_path: str):
try:
artifact_path = sbas_insar_production_service.resolve_artifact_path(trial_id, relative_path)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
media_type = mimetypes.guess_type(str(artifact_path))[0] or "application/octet-stream"
return FileResponse(
artifact_path,
media_type=media_type,
filename=artifact_path.name,
)