Enable LT1 and GF3 result delivery
This commit is contained in:
@@ -43,6 +43,7 @@ MIGRATION_FILES = [
|
||||
"011_source_metadata_documents.sql",
|
||||
"012_source_archive_integrity.sql",
|
||||
"013_result_delivery_requests.sql",
|
||||
"014_result_delivery_ortho_sources.sql",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -377,6 +377,8 @@ class ResultDeliveryItemORM(Base):
|
||||
source_product_id = Column(Integer, ForeignKey("result_products.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
source_result_id = Column(Integer, ForeignKey("dinsar_results.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
source_asset_id = Column(Integer, ForeignKey("result_assets.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
source_radar_data_id = Column(Integer, ForeignKey("radar_data.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
source_scene_geo_id = Column(Integer, ForeignKey("sar_scene_geo.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
display_name = Column(String(255), nullable=False)
|
||||
source_path = Column(String, nullable=False)
|
||||
relative_path = Column(String, nullable=True)
|
||||
@@ -391,10 +393,14 @@ class ResultDeliveryItemORM(Base):
|
||||
product = relationship("ResultProductORM", foreign_keys=[source_product_id])
|
||||
compat_result = relationship("DinsarResultORM", foreign_keys=[source_result_id])
|
||||
asset = relationship("ResultAssetORM", foreign_keys=[source_asset_id])
|
||||
radar_data = relationship("RadarDataORM", foreign_keys=[source_radar_data_id])
|
||||
scene_geo = relationship("SARSceneGeoORM", foreign_keys=[source_scene_geo_id])
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_result_delivery_items_delivery_status", "delivery_id", "status"),
|
||||
Index("idx_result_delivery_items_product", "source_product_id"),
|
||||
Index("idx_result_delivery_items_radar", "source_radar_data_id"),
|
||||
Index("idx_result_delivery_items_scene_geo", "source_scene_geo_id"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class ResultDeliveryCreateRequest(BaseModel):
|
||||
channel: str
|
||||
product_ids: Optional[List[int]] = None
|
||||
compat_result_ids: Optional[List[int]] = None
|
||||
item_ids: Optional[List[int]] = None
|
||||
package_mode: str = "directory"
|
||||
include_checksums: Optional[bool] = None
|
||||
|
||||
@@ -64,6 +65,7 @@ async def create_result_delivery(
|
||||
channel=request.channel,
|
||||
product_ids=request.product_ids,
|
||||
compat_result_ids=request.compat_result_ids,
|
||||
item_ids=request.item_ids,
|
||||
package_mode=request.package_mode,
|
||||
include_checksums=request.include_checksums,
|
||||
)
|
||||
@@ -89,6 +91,28 @@ async def create_result_delivery(
|
||||
return result_delivery_service.serialize_delivery(delivery)
|
||||
|
||||
|
||||
@router.get("/result-deliveries/catalog/{channel}")
|
||||
async def list_result_delivery_catalog(
|
||||
channel: str,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
query: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AuthUserORM = Depends(_get_current_user),
|
||||
):
|
||||
_ = current_user
|
||||
try:
|
||||
return await result_delivery_service.list_channel_catalog(
|
||||
db,
|
||||
channel=channel,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
query=query,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/result-deliveries")
|
||||
async def list_result_deliveries(
|
||||
mine: bool = True,
|
||||
|
||||
@@ -20,10 +20,12 @@ from ..config import settings
|
||||
from ..models import (
|
||||
AuthUserORM,
|
||||
DinsarResultORM,
|
||||
RadarDataORM,
|
||||
ResultAssetORM,
|
||||
ResultDeliveryItemORM,
|
||||
ResultDeliveryRequestORM,
|
||||
ResultProductORM,
|
||||
SARSceneGeoORM,
|
||||
)
|
||||
from .dinsar_read_service import dinsar_read_service
|
||||
from .job_queue_service import job_queue_service
|
||||
@@ -46,7 +48,17 @@ ITEM_STATUS_FAILED = "FAILED"
|
||||
ITEM_STATUS_SKIPPED = "SKIPPED"
|
||||
|
||||
CHANNEL_DINSAR = "dinsar"
|
||||
SUPPORTED_READY_CHANNELS = {CHANNEL_DINSAR}
|
||||
CHANNEL_LT1_ORTHO = "lt1_ortho"
|
||||
CHANNEL_GF3_ORTHO = "gf3_ortho"
|
||||
CHANNEL_SBAS = "sbas"
|
||||
CHANNEL_S1_ORTHO = "s1_ortho"
|
||||
SUPPORTED_READY_CHANNELS = {CHANNEL_DINSAR, CHANNEL_LT1_ORTHO, CHANNEL_GF3_ORTHO}
|
||||
|
||||
LT1_ANALYSIS_ENGINE = "lt_gamma"
|
||||
LT1_ANALYSIS_PROFILE = "lt1_gamma_geocoded_mli"
|
||||
GF3_STANDARD_SOURCE_FORMAT = "GF3_SARSCAPE_L2"
|
||||
GF3_NATIVE_PREVIEW_SOURCE_FORMAT = "GF3_SARSCAPE_NATIVE_PREVIEW"
|
||||
GF3_DELIVERABLE_SOURCE_FORMATS = {GF3_STANDARD_SOURCE_FORMAT, GF3_NATIVE_PREVIEW_SOURCE_FORMAT}
|
||||
|
||||
PACKAGE_MODE_DIRECTORY = "directory"
|
||||
PACKAGE_MODE_ZIP = "zip"
|
||||
@@ -58,11 +70,16 @@ _SAFE_ID_RE = re.compile(r"[^0-9A-Za-z_-]+")
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeliverySource:
|
||||
product: ResultProductORM
|
||||
compat_row: Optional[DinsarResultORM]
|
||||
display_name: str
|
||||
source_path: str
|
||||
product: Optional[ResultProductORM] = None
|
||||
compat_row: Optional[DinsarResultORM] = None
|
||||
source_asset_id: Optional[int] = None
|
||||
source_radar_data_id: Optional[int] = None
|
||||
source_scene_geo_id: Optional[int] = None
|
||||
product_id: Optional[str] = None
|
||||
task_name: Optional[str] = None
|
||||
source_kind: str = "result_product"
|
||||
|
||||
|
||||
def _new_session() -> AsyncSession:
|
||||
@@ -208,6 +225,59 @@ def _resolve_source_path(product: ResultProductORM, assets: List[ResultAssetORM]
|
||||
return "", None
|
||||
|
||||
|
||||
def _scene_product_id(scene: SARSceneGeoORM) -> str:
|
||||
return f"sar_scene_geo:{scene.id}"
|
||||
|
||||
|
||||
def _scene_display_name(scene: SARSceneGeoORM, radar: RadarDataORM) -> str:
|
||||
for value in (
|
||||
radar.product_unique_id,
|
||||
radar.unique_id,
|
||||
radar.source_product_token,
|
||||
os.path.basename(str(radar.file_path or "").rstrip("/\\")),
|
||||
_scene_product_id(scene),
|
||||
):
|
||||
text = str(value or "").strip()
|
||||
if text:
|
||||
return text
|
||||
return f"scene_{scene.id}"
|
||||
|
||||
|
||||
def _radar_display_name(radar: RadarDataORM) -> str:
|
||||
for value in (
|
||||
radar.product_unique_id,
|
||||
radar.unique_id,
|
||||
radar.source_product_token,
|
||||
os.path.basename(str(radar.file_path or "").rstrip("/\\")),
|
||||
f"radar_{radar.id}",
|
||||
):
|
||||
text = str(value or "").strip()
|
||||
if text:
|
||||
return text
|
||||
return f"radar_{radar.id}"
|
||||
|
||||
|
||||
def _manifest_path_for_scene(scene: SARSceneGeoORM) -> Optional[str]:
|
||||
metadata = scene.analysis_metadata_json if isinstance(scene.analysis_metadata_json, dict) else {}
|
||||
text = str(metadata.get("manifest_path") or "").strip()
|
||||
if text:
|
||||
return text
|
||||
if scene.analysis_dir:
|
||||
return os.path.join(scene.analysis_dir, "manifest.json")
|
||||
return None
|
||||
|
||||
|
||||
def _quality_path_for_scene(scene: SARSceneGeoORM) -> Optional[str]:
|
||||
if scene.analysis_dir:
|
||||
return os.path.join(scene.analysis_dir, "quality.json")
|
||||
return None
|
||||
|
||||
|
||||
def _catalog_file_size(path: Any) -> int:
|
||||
text = str(path or "").strip()
|
||||
return _file_size(text) if text else 0
|
||||
|
||||
|
||||
class ResultDeliveryService:
|
||||
def channels(self) -> List[Dict[str, Any]]:
|
||||
return [
|
||||
@@ -220,7 +290,7 @@ class ResultDeliveryService:
|
||||
"description": "已登记 D-InSAR catalog,可创建后台交付包并下载到本地。",
|
||||
},
|
||||
{
|
||||
"key": "sbas",
|
||||
"key": CHANNEL_SBAS,
|
||||
"group": "InSAR 成果",
|
||||
"label": "SBAS-InSAR 结果",
|
||||
"state": "planned",
|
||||
@@ -228,15 +298,15 @@ class ResultDeliveryService:
|
||||
"description": "SBAS 结果 catalog 已有基础能力,本阶段暂不开放交付打包。",
|
||||
},
|
||||
{
|
||||
"key": "lt1_ortho",
|
||||
"key": CHANNEL_LT1_ORTHO,
|
||||
"group": "正射成果",
|
||||
"label": "LT-1 正射结果",
|
||||
"state": "placeholder",
|
||||
"state_text": "待接入",
|
||||
"description": "陆探一正射生产将由 LandSAR 生产链注册后接入统一交付。",
|
||||
"state": "ready",
|
||||
"state_text": "可交付",
|
||||
"description": "服务器生产的 LT-1 分析就绪正射 GeoTIFF 已接入交付,可打包下载到本地。",
|
||||
},
|
||||
{
|
||||
"key": "s1_ortho",
|
||||
"key": CHANNEL_S1_ORTHO,
|
||||
"group": "正射成果",
|
||||
"label": "Sentinel-1 正射结果",
|
||||
"state": "placeholder",
|
||||
@@ -244,12 +314,12 @@ class ResultDeliveryService:
|
||||
"description": "Sentinel-1 正射生产尚未接入,当前只保留交付通道占位。",
|
||||
},
|
||||
{
|
||||
"key": "gf3_ortho",
|
||||
"key": CHANNEL_GF3_ORTHO,
|
||||
"group": "正射成果",
|
||||
"label": "GF3 SARscape _geo",
|
||||
"state": "placeholder",
|
||||
"state_text": "待接入",
|
||||
"description": "GF3 外部生产成果登记后再接入统一交付。",
|
||||
"state": "ready",
|
||||
"state_text": "可交付",
|
||||
"description": "已登记的 GF3 SARscape 标准化正射成品可直接创建交付包。",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -282,11 +352,14 @@ class ResultDeliveryService:
|
||||
seen_products.add(int(product.id))
|
||||
sources.append(
|
||||
DeliverySource(
|
||||
product=product,
|
||||
compat_row=None,
|
||||
display_name=dinsar_read_service.get_display_name(product),
|
||||
source_path=source_path,
|
||||
product=product,
|
||||
compat_row=None,
|
||||
source_asset_id=source_asset_id,
|
||||
product_id=product.product_id,
|
||||
task_name=product.task_alias or product.task_name,
|
||||
source_kind="dinsar_product",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -319,16 +392,344 @@ class ResultDeliveryService:
|
||||
seen_products.add(int(product.id))
|
||||
sources.append(
|
||||
DeliverySource(
|
||||
product=product,
|
||||
compat_row=record.compat_row,
|
||||
display_name=record.display_name,
|
||||
source_path=source_path,
|
||||
product=product,
|
||||
compat_row=record.compat_row,
|
||||
source_asset_id=source_asset_id,
|
||||
product_id=product.product_id,
|
||||
task_name=product.task_alias or product.task_name,
|
||||
source_kind="dinsar_product",
|
||||
)
|
||||
)
|
||||
|
||||
return sources
|
||||
|
||||
async def _resolve_lt1_sources(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
item_ids: List[int],
|
||||
product_ids: List[int],
|
||||
) -> List[DeliverySource]:
|
||||
selected_ids = item_ids or product_ids
|
||||
if not selected_ids:
|
||||
return []
|
||||
result = await db.execute(
|
||||
select(SARSceneGeoORM, RadarDataORM)
|
||||
.join(RadarDataORM, SARSceneGeoORM.radar_data_id == RadarDataORM.id)
|
||||
.where(
|
||||
SARSceneGeoORM.id.in_(selected_ids),
|
||||
SARSceneGeoORM.status == "DONE",
|
||||
SARSceneGeoORM.analysis_tif_path.isnot(None),
|
||||
SARSceneGeoORM.analysis_engine == LT1_ANALYSIS_ENGINE,
|
||||
SARSceneGeoORM.analysis_profile == LT1_ANALYSIS_PROFILE,
|
||||
)
|
||||
.order_by(SARSceneGeoORM.id.asc())
|
||||
)
|
||||
sources: List[DeliverySource] = []
|
||||
for scene, radar in result.all():
|
||||
analysis_dir = str(scene.analysis_dir or "").strip()
|
||||
analysis_tif = str(scene.analysis_tif_path or "").strip()
|
||||
source_path = analysis_dir if analysis_dir and os.path.isdir(analysis_dir) else analysis_tif
|
||||
if not source_path:
|
||||
continue
|
||||
product_id = _scene_product_id(scene)
|
||||
display_name = _scene_display_name(scene, radar)
|
||||
sources.append(
|
||||
DeliverySource(
|
||||
display_name=display_name,
|
||||
source_path=source_path,
|
||||
source_radar_data_id=int(radar.id) if radar.id is not None else None,
|
||||
source_scene_geo_id=int(scene.id) if scene.id is not None else None,
|
||||
product_id=product_id,
|
||||
task_name=display_name,
|
||||
source_kind="lt1_scene_geo",
|
||||
)
|
||||
)
|
||||
return sources
|
||||
|
||||
async def _resolve_gf3_sources(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
item_ids: List[int],
|
||||
) -> List[DeliverySource]:
|
||||
if not item_ids:
|
||||
return []
|
||||
result = await db.execute(
|
||||
select(RadarDataORM, SARSceneGeoORM)
|
||||
.outerjoin(SARSceneGeoORM, SARSceneGeoORM.radar_data_id == RadarDataORM.id)
|
||||
.where(
|
||||
RadarDataORM.id.in_(item_ids),
|
||||
RadarDataORM.source_format.in_(GF3_DELIVERABLE_SOURCE_FORMATS),
|
||||
RadarDataORM.geocoded_flag.is_(True),
|
||||
)
|
||||
.order_by(RadarDataORM.id.asc())
|
||||
)
|
||||
sources: List[DeliverySource] = []
|
||||
for radar, scene in result.all():
|
||||
source_path = ""
|
||||
source_scene_geo_id = None
|
||||
if scene is not None and scene.analysis_tif_path:
|
||||
analysis_dir = str(scene.analysis_dir or "").strip()
|
||||
analysis_tif = str(scene.analysis_tif_path or "").strip()
|
||||
source_path = analysis_dir if analysis_dir and os.path.isdir(analysis_dir) else analysis_tif
|
||||
source_scene_geo_id = int(scene.id) if scene.id is not None else None
|
||||
if not source_path:
|
||||
source_path = str(radar.file_path or "").strip()
|
||||
if not source_path:
|
||||
continue
|
||||
display_name = _radar_display_name(radar)
|
||||
sources.append(
|
||||
DeliverySource(
|
||||
display_name=display_name,
|
||||
source_path=source_path,
|
||||
source_radar_data_id=int(radar.id) if radar.id is not None else None,
|
||||
source_scene_geo_id=source_scene_geo_id,
|
||||
product_id=f"gf3_sarscape:{radar.id}",
|
||||
task_name=display_name,
|
||||
source_kind="gf3_radar_data",
|
||||
)
|
||||
)
|
||||
return sources
|
||||
|
||||
async def _resolve_sources_for_channel(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
channel: str,
|
||||
product_ids: List[int],
|
||||
compat_result_ids: List[int],
|
||||
item_ids: List[int],
|
||||
) -> List[DeliverySource]:
|
||||
if channel == CHANNEL_DINSAR:
|
||||
return await self._resolve_dinsar_sources(
|
||||
db,
|
||||
product_ids=product_ids,
|
||||
compat_result_ids=compat_result_ids,
|
||||
)
|
||||
if channel == CHANNEL_LT1_ORTHO:
|
||||
return await self._resolve_lt1_sources(db, item_ids=item_ids, product_ids=product_ids)
|
||||
if channel == CHANNEL_GF3_ORTHO:
|
||||
return await self._resolve_gf3_sources(db, item_ids=item_ids or product_ids)
|
||||
return []
|
||||
|
||||
async def list_channel_catalog(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
channel: str,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
query: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
channel = _normalize_channel(channel)
|
||||
safe_limit = min(500, max(1, int(limit or 100)))
|
||||
safe_offset = max(0, int(offset or 0))
|
||||
query_text = str(query or "").strip()
|
||||
|
||||
if channel == CHANNEL_LT1_ORTHO:
|
||||
filters = [
|
||||
SARSceneGeoORM.status == "DONE",
|
||||
SARSceneGeoORM.analysis_tif_path.isnot(None),
|
||||
SARSceneGeoORM.analysis_engine == LT1_ANALYSIS_ENGINE,
|
||||
SARSceneGeoORM.analysis_profile == LT1_ANALYSIS_PROFILE,
|
||||
]
|
||||
if query_text:
|
||||
like = f"%{query_text}%"
|
||||
filters.append(
|
||||
or_(
|
||||
RadarDataORM.product_unique_id.ilike(like),
|
||||
RadarDataORM.unique_id.ilike(like),
|
||||
RadarDataORM.file_path.ilike(like),
|
||||
RadarDataORM.imaging_date.ilike(like),
|
||||
)
|
||||
)
|
||||
total_result = await db.execute(
|
||||
select(func.count(SARSceneGeoORM.id))
|
||||
.join(RadarDataORM, SARSceneGeoORM.radar_data_id == RadarDataORM.id)
|
||||
.where(*filters)
|
||||
)
|
||||
rows_result = await db.execute(
|
||||
select(SARSceneGeoORM, RadarDataORM)
|
||||
.join(RadarDataORM, SARSceneGeoORM.radar_data_id == RadarDataORM.id)
|
||||
.where(*filters)
|
||||
.order_by(SARSceneGeoORM.updated_at.desc().nullslast(), SARSceneGeoORM.id.desc())
|
||||
.limit(safe_limit)
|
||||
.offset(safe_offset)
|
||||
)
|
||||
items = []
|
||||
for scene, radar in rows_result.all():
|
||||
display_name = _scene_display_name(scene, radar)
|
||||
manifest_path = _manifest_path_for_scene(scene)
|
||||
quality_path = _quality_path_for_scene(scene)
|
||||
items.append(
|
||||
{
|
||||
"id": scene.id,
|
||||
"item_id": scene.id,
|
||||
"source_kind": "lt1_scene_geo",
|
||||
"product_id": _scene_product_id(scene),
|
||||
"catalog_name": "sar_scene_geo",
|
||||
"product_family": "lt1_analysis_ready_geotiff",
|
||||
"product_type": "analysis_ready_geotiff",
|
||||
"display_name": display_name,
|
||||
"task_name": display_name,
|
||||
"status": scene.status,
|
||||
"health_status": "OK",
|
||||
"engine_code": scene.analysis_engine,
|
||||
"profile_code": scene.analysis_profile,
|
||||
"radar_data_id": radar.id,
|
||||
"scene_geo_id": scene.id,
|
||||
"imaging_date": radar.imaging_date,
|
||||
"polarization": radar.polarization,
|
||||
"pixel_size_m": scene.pixel_size_m,
|
||||
"backscatter_unit": scene.analysis_backscatter_unit,
|
||||
"publish_dir": scene.analysis_dir,
|
||||
"manifest_path": manifest_path,
|
||||
"quality_path": quality_path,
|
||||
"primary_asset_path": scene.analysis_tif_path,
|
||||
"file_size": _catalog_file_size(scene.analysis_tif_path),
|
||||
"summary": {
|
||||
"radar_data_id": radar.id,
|
||||
"source_asset_ids": [radar.source_product_ref_id] if radar.source_product_ref_id else [],
|
||||
"imaging_date": radar.imaging_date,
|
||||
"polarization": radar.polarization,
|
||||
"pixel_size_m": scene.pixel_size_m,
|
||||
},
|
||||
"produced_at": scene.updated_at.isoformat() if scene.updated_at else None,
|
||||
"published_at": scene.updated_at.isoformat() if scene.updated_at else None,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"items": items,
|
||||
"total": int(total_result.scalar_one() or 0),
|
||||
"limit": safe_limit,
|
||||
"offset": safe_offset,
|
||||
}
|
||||
|
||||
if channel == CHANNEL_GF3_ORTHO:
|
||||
filters = [
|
||||
RadarDataORM.source_format.in_(GF3_DELIVERABLE_SOURCE_FORMATS),
|
||||
RadarDataORM.geocoded_flag.is_(True),
|
||||
]
|
||||
if query_text:
|
||||
like = f"%{query_text}%"
|
||||
filters.append(
|
||||
or_(
|
||||
RadarDataORM.product_unique_id.ilike(like),
|
||||
RadarDataORM.unique_id.ilike(like),
|
||||
RadarDataORM.file_path.ilike(like),
|
||||
RadarDataORM.imaging_date.ilike(like),
|
||||
)
|
||||
)
|
||||
total_result = await db.execute(select(func.count(RadarDataORM.id)).where(*filters))
|
||||
rows_result = await db.execute(
|
||||
select(RadarDataORM, SARSceneGeoORM)
|
||||
.outerjoin(SARSceneGeoORM, SARSceneGeoORM.radar_data_id == RadarDataORM.id)
|
||||
.where(*filters)
|
||||
.order_by(RadarDataORM.acquisition_start_time_utc.desc().nullslast(), RadarDataORM.id.desc())
|
||||
.limit(safe_limit)
|
||||
.offset(safe_offset)
|
||||
)
|
||||
items = []
|
||||
for radar, scene in rows_result.all():
|
||||
display_name = _radar_display_name(radar)
|
||||
primary_path = (
|
||||
str(scene.analysis_tif_path or "").strip()
|
||||
if scene is not None and scene.analysis_tif_path
|
||||
else str(radar.file_path or "").strip()
|
||||
)
|
||||
publish_dir = (
|
||||
str(scene.analysis_dir or "").strip()
|
||||
if scene is not None and scene.analysis_dir
|
||||
else str(radar.file_path or "").strip()
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"id": radar.id,
|
||||
"item_id": radar.id,
|
||||
"source_kind": "gf3_radar_data",
|
||||
"product_id": f"gf3_sarscape:{radar.id}",
|
||||
"catalog_name": "radar_data",
|
||||
"product_family": "gf3_ortho",
|
||||
"product_type": "sarscape_l2_geotiff" if radar.source_format == GF3_STANDARD_SOURCE_FORMAT else "sarscape_native_geo",
|
||||
"display_name": display_name,
|
||||
"task_name": display_name,
|
||||
"status": "READY",
|
||||
"health_status": "OK",
|
||||
"engine_code": (scene.analysis_engine if scene is not None else None) or "gf3_sarscape",
|
||||
"profile_code": (
|
||||
(scene.analysis_profile if scene is not None else None)
|
||||
or ("gf3_standard_geotiff" if radar.source_format == GF3_STANDARD_SOURCE_FORMAT else "gf3_native_geo")
|
||||
),
|
||||
"source_format": radar.source_format,
|
||||
"radar_data_id": radar.id,
|
||||
"scene_geo_id": scene.id if scene is not None else None,
|
||||
"imaging_date": radar.imaging_date,
|
||||
"polarization": radar.polarization,
|
||||
"pixel_size_m": scene.pixel_size_m if scene is not None else None,
|
||||
"backscatter_unit": scene.analysis_backscatter_unit if scene is not None else None,
|
||||
"publish_dir": publish_dir,
|
||||
"manifest_path": _manifest_path_for_scene(scene) if scene is not None else None,
|
||||
"primary_asset_path": primary_path,
|
||||
"file_size": _catalog_file_size(primary_path),
|
||||
"summary": {
|
||||
"radar_data_id": radar.id,
|
||||
"source_asset_ids": [radar.source_product_ref_id] if radar.source_product_ref_id else [],
|
||||
"imaging_date": radar.imaging_date,
|
||||
"polarization": radar.polarization,
|
||||
},
|
||||
"produced_at": (
|
||||
scene.updated_at.isoformat() if scene is not None and scene.updated_at else None
|
||||
),
|
||||
"published_at": (
|
||||
scene.updated_at.isoformat() if scene is not None and scene.updated_at else None
|
||||
),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"items": items,
|
||||
"total": int(total_result.scalar_one() or 0),
|
||||
"limit": safe_limit,
|
||||
"offset": safe_offset,
|
||||
}
|
||||
|
||||
if channel == CHANNEL_DINSAR:
|
||||
total = await dinsar_read_service.count_compat_records(db)
|
||||
records = await dinsar_read_service.list_compat_records(db, limit=safe_limit, offset=safe_offset)
|
||||
items = []
|
||||
for record in records:
|
||||
product = record.product
|
||||
compat_row = record.compat_row
|
||||
items.append(
|
||||
{
|
||||
"id": int(compat_row.id if compat_row is not None else product.id),
|
||||
"item_id": int(compat_row.id if compat_row is not None else product.id),
|
||||
"source_kind": "dinsar_result",
|
||||
"product_id": product.product_id,
|
||||
"catalog_name": product.catalog_name,
|
||||
"product_family": product.product_family,
|
||||
"product_type": product.product_type,
|
||||
"display_name": record.display_name,
|
||||
"task_name": product.task_alias or product.task_name,
|
||||
"status": product.status,
|
||||
"health_status": product.health_status,
|
||||
"engine_code": product.engine_code,
|
||||
"profile_code": product.profile_code,
|
||||
"pair_key": product.pair_key,
|
||||
"publish_dir": product.publish_dir,
|
||||
"manifest_path": product.manifest_path,
|
||||
"primary_asset_path": product.primary_asset_path or product.source_primary_path,
|
||||
"file_size": _catalog_file_size(product.primary_asset_path or product.source_primary_path),
|
||||
"produced_at": product.produced_at.isoformat() if product.produced_at else None,
|
||||
"published_at": product.published_at.isoformat() if product.published_at else None,
|
||||
}
|
||||
)
|
||||
return {"items": items, "total": total, "limit": safe_limit, "offset": safe_offset}
|
||||
|
||||
return {"items": [], "total": 0, "limit": safe_limit, "offset": safe_offset}
|
||||
|
||||
async def create_delivery(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
@@ -337,6 +738,7 @@ class ResultDeliveryService:
|
||||
channel: str,
|
||||
product_ids: Optional[List[int]] = None,
|
||||
compat_result_ids: Optional[List[int]] = None,
|
||||
item_ids: Optional[List[int]] = None,
|
||||
package_mode: str = PACKAGE_MODE_DIRECTORY,
|
||||
include_checksums: Optional[bool] = None,
|
||||
) -> ResultDeliveryRequestORM:
|
||||
@@ -348,20 +750,20 @@ class ResultDeliveryService:
|
||||
max_items = max(1, int(settings.RESULT_DELIVERY_MAX_ITEMS or 500))
|
||||
normalized_product_ids = _normalize_int_ids(product_ids, max_count=max_items)
|
||||
normalized_compat_ids = _normalize_int_ids(compat_result_ids, max_count=max_items)
|
||||
if not normalized_product_ids and not normalized_compat_ids:
|
||||
normalized_item_ids = _normalize_int_ids(item_ids, max_count=max_items)
|
||||
if not normalized_product_ids and not normalized_compat_ids and not normalized_item_ids:
|
||||
raise ValueError("select at least one result")
|
||||
|
||||
if len(normalized_product_ids) + len(normalized_compat_ids) > max_items:
|
||||
if len(normalized_product_ids) + len(normalized_compat_ids) + len(normalized_item_ids) > max_items:
|
||||
raise ValueError(f"selected item count exceeds max limit ({max_items})")
|
||||
|
||||
if channel == CHANNEL_DINSAR:
|
||||
sources = await self._resolve_dinsar_sources(
|
||||
db,
|
||||
product_ids=normalized_product_ids,
|
||||
compat_result_ids=normalized_compat_ids,
|
||||
)
|
||||
else:
|
||||
sources = []
|
||||
sources = await self._resolve_sources_for_channel(
|
||||
db,
|
||||
channel=channel,
|
||||
product_ids=normalized_product_ids,
|
||||
compat_result_ids=normalized_compat_ids,
|
||||
item_ids=normalized_item_ids,
|
||||
)
|
||||
|
||||
if not sources:
|
||||
raise ValueError("selected results do not have deliverable files")
|
||||
@@ -376,6 +778,7 @@ class ResultDeliveryService:
|
||||
"channel": channel,
|
||||
"product_ids": normalized_product_ids,
|
||||
"compat_result_ids": normalized_compat_ids,
|
||||
"item_ids": normalized_item_ids,
|
||||
"package_mode": package_mode,
|
||||
"include_checksums": (
|
||||
bool(settings.RESULT_DELIVERY_CHECKSUM_ENABLED)
|
||||
@@ -544,6 +947,8 @@ class ResultDeliveryService:
|
||||
"source_product_id": item.source_product_id,
|
||||
"source_result_id": item.source_result_id,
|
||||
"source_asset_id": item.source_asset_id,
|
||||
"source_radar_data_id": item.source_radar_data_id,
|
||||
"source_scene_geo_id": item.source_scene_geo_id,
|
||||
"display_name": item.display_name,
|
||||
"relative_path": item.relative_path,
|
||||
"file_size": item.file_size,
|
||||
@@ -636,14 +1041,14 @@ class ResultDeliveryService:
|
||||
)
|
||||
delivery = result.scalar_one()
|
||||
request_json = delivery.request_json if isinstance(delivery.request_json, dict) else {}
|
||||
if delivery.channel == CHANNEL_DINSAR:
|
||||
sources = await self._resolve_dinsar_sources(
|
||||
db,
|
||||
product_ids=_normalize_int_ids(request_json.get("product_ids"), max_count=int(settings.RESULT_DELIVERY_MAX_ITEMS or 500)),
|
||||
compat_result_ids=_normalize_int_ids(request_json.get("compat_result_ids"), max_count=int(settings.RESULT_DELIVERY_MAX_ITEMS or 500)),
|
||||
)
|
||||
else:
|
||||
sources = []
|
||||
max_items = int(settings.RESULT_DELIVERY_MAX_ITEMS or 500)
|
||||
sources = await self._resolve_sources_for_channel(
|
||||
db,
|
||||
channel=delivery.channel,
|
||||
product_ids=_normalize_int_ids(request_json.get("product_ids"), max_count=max_items),
|
||||
compat_result_ids=_normalize_int_ids(request_json.get("compat_result_ids"), max_count=max_items),
|
||||
item_ids=_normalize_int_ids(request_json.get("item_ids"), max_count=max_items),
|
||||
)
|
||||
task_id = delivery.task_id
|
||||
|
||||
if not sources:
|
||||
@@ -672,10 +1077,9 @@ class ResultDeliveryService:
|
||||
total_sources = len(sources)
|
||||
for source_index, source in enumerate(sources, start=1):
|
||||
folder = _sanitize_segment(
|
||||
source.product.task_alias
|
||||
or source.product.task_name
|
||||
source.task_name
|
||||
or source.display_name
|
||||
or source.product.product_id,
|
||||
or source.product_id,
|
||||
default=f"product_{source_index}",
|
||||
)
|
||||
source_files = _iter_associated_files(source.source_path)
|
||||
@@ -684,9 +1088,11 @@ class ResultDeliveryService:
|
||||
item_payloads.append(
|
||||
{
|
||||
"delivery_id": delivery_id,
|
||||
"source_product_id": source.product.id,
|
||||
"source_product_id": source.product.id if source.product else None,
|
||||
"source_result_id": source.compat_row.id if source.compat_row else None,
|
||||
"source_asset_id": source.source_asset_id,
|
||||
"source_radar_data_id": source.source_radar_data_id,
|
||||
"source_scene_geo_id": source.source_scene_geo_id,
|
||||
"display_name": source.display_name,
|
||||
"source_path": source.source_path,
|
||||
"relative_path": None,
|
||||
@@ -736,9 +1142,11 @@ class ResultDeliveryService:
|
||||
|
||||
item_payload = {
|
||||
"delivery_id": delivery_id,
|
||||
"source_product_id": source.product.id,
|
||||
"source_product_id": source.product.id if source.product else None,
|
||||
"source_result_id": source.compat_row.id if source.compat_row else None,
|
||||
"source_asset_id": source.source_asset_id if file_index == 1 else None,
|
||||
"source_radar_data_id": source.source_radar_data_id,
|
||||
"source_scene_geo_id": source.source_scene_geo_id,
|
||||
"display_name": source.display_name,
|
||||
"source_path": source_file,
|
||||
"relative_path": relative_path,
|
||||
@@ -751,9 +1159,12 @@ class ResultDeliveryService:
|
||||
manifest_items.append(
|
||||
{
|
||||
"display_name": source.display_name,
|
||||
"product_id": source.product.product_id,
|
||||
"product_ref_id": source.product.id,
|
||||
"product_id": source.product_id or (source.product.product_id if source.product else None),
|
||||
"product_ref_id": source.product.id if source.product else None,
|
||||
"source_result_id": source.compat_row.id if source.compat_row else None,
|
||||
"source_radar_data_id": source.source_radar_data_id,
|
||||
"source_scene_geo_id": source.source_scene_geo_id,
|
||||
"source_kind": source.source_kind,
|
||||
"relative_path": relative_path.replace(os.sep, "/"),
|
||||
"file_size": size,
|
||||
"checksum_sha256": checksum,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE result_delivery_items ADD COLUMN IF NOT EXISTS source_radar_data_id INTEGER NULL REFERENCES radar_data(id) ON DELETE SET NULL;
|
||||
ALTER TABLE result_delivery_items ADD COLUMN IF NOT EXISTS source_scene_geo_id INTEGER NULL REFERENCES sar_scene_geo(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_result_delivery_items_source_radar_data_id ON result_delivery_items(source_radar_data_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_result_delivery_items_source_scene_geo_id ON result_delivery_items(source_scene_geo_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_result_delivery_items_radar ON result_delivery_items(source_radar_data_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_result_delivery_items_scene_geo ON result_delivery_items(source_scene_geo_id);
|
||||
Reference in New Issue
Block a user