Apply current workspace changes

This commit is contained in:
2026-04-22 01:52:21 +08:00
parent cf34b9a8ed
commit cb518b431f
449 changed files with 92252 additions and 321 deletions
+176 -11
View File
@@ -74,6 +74,7 @@ JOB_TYPE_WATER_DETECT = "WATER_DETECT"
JOB_TYPE_GF3_PROCESS = "GF3_PROCESS"
JOB_TYPE_GF3_BATCH_PROCESS = "GF3_BATCH_PROCESS"
JOB_TYPE_ISCE2_RUN = "ISCE2_RUN"
JOB_TYPE_PYINT_RUN = "PYINT_RUN"
JOB_TYPE_PUBLISH_DINSAR_PRODUCTS = "PUBLISH_DINSAR_PRODUCTS"
JOB_TYPE_REBUILD_DINSAR_CATALOG = "REBUILD_DINSAR_CATALOG"
JOB_TYPE_REBUILD_PSINSAR_CATALOG = "REBUILD_PSINSAR_CATALOG"
@@ -1878,8 +1879,13 @@ async def _handle_idl_run_dinsar(job: SystemJobORM) -> None:
)
async def _handle_isce2_run(job: SystemJobORM) -> None:
"""ISCE2 生产任务 handler — 通过 WSL 执行 run_lt1_dinsar_pipeline.py。"""
async def _handle_queued_engine_run(
job: SystemJobORM,
*,
engine_title: str,
fallback_timeout_seconds: int,
) -> None:
"""Run a queued D-InSAR engine task through the shared WSL execution path."""
payload = job.payload or {}
engine_code = payload.get("engine_code", "isce2")
profile = payload.get("profile", "lt1_stripmap")
@@ -1887,17 +1893,26 @@ async def _handle_isce2_run(job: SystemJobORM) -> None:
num_to_process = payload.get("num_to_process", 0)
timeout_seconds = payload.get("timeout_seconds")
extra = payload.get("extra", {})
selected_task_count = max(1, int(extra.get("__validated_task_count") or 0 or 1))
pair_timeout_seconds = int(timeout_seconds or fallback_timeout_seconds)
await task_service.start_task(
job.task_id,
message=f"[{engine_code}/{profile}] 启动 ISCE2 处理...",
message=f"[{engine_code}/{profile}] 启动 {engine_title} 处理...",
)
await task_service.add_log(
job.task_id,
"INFO",
f"ISCE2 job accepted. root_dir={root_dir}, profile={profile}, timeout={timeout_seconds or 21600}s, extra={extra}",
f"{engine_title} job accepted. root_dir={root_dir}, profile={profile}, timeout={pair_timeout_seconds}s, extra={extra}",
)
await task_service.add_log(
job.task_id,
"INFO",
(
f"{engine_title} batch contains {selected_task_count} pair task(s). "
f"Pairs run sequentially and each pair uses timeout={pair_timeout_seconds}s."
),
)
from ..dinsar_engines.base import RunRequest
from ..dinsar_engines import registry
@@ -1905,6 +1920,25 @@ async def _handle_isce2_run(job: SystemJobORM) -> None:
if not engine:
raise RuntimeError(f"引擎 '{engine_code}' 未注册")
loop = asyncio.get_running_loop()
progress_queue: asyncio.Queue[Optional[Dict[str, Any]]] = asyncio.Queue()
progress_state: Dict[str, Any] = {
"progress": 5,
"message": f"[{engine_code}/{profile}] Running in WSL...",
"pair_index": 0,
"pair_total": selected_task_count,
"pair_label": "",
"pair_started_monotonic": None,
}
def _emit_progress(event: Dict[str, Any]) -> None:
if not event:
return
try:
loop.call_soon_threadsafe(progress_queue.put_nowait, dict(event))
except RuntimeError:
return
request = RunRequest(
engine_code=engine_code,
profile=profile,
@@ -1913,6 +1947,7 @@ async def _handle_isce2_run(job: SystemJobORM) -> None:
num_to_process=num_to_process,
timeout_seconds=timeout_seconds,
extra=extra,
progress_callback=_emit_progress,
)
await task_service.update_task(
@@ -1921,18 +1956,119 @@ async def _handle_isce2_run(job: SystemJobORM) -> None:
message=f"[{engine_code}/{profile}] 正在执行,请等待...",
)
async def _consume_progress() -> None:
while True:
event = await progress_queue.get()
if event is None:
return
event_type = str(event.get("event") or "").strip().lower()
pair_total = max(1, int(event.get("pair_total") or progress_state["pair_total"] or 1))
pair_index = max(0, int(event.get("pair_index") or 0))
task_label = str(event.get("task_alias") or event.get("task_name") or "").strip()
if event_type == "pair_started":
progress = min(
90,
max(
int(progress_state["progress"] or 5),
5 + int((max(pair_index - 1, 0) / pair_total) * 80),
),
)
progress_state.update(
{
"progress": progress,
"pair_index": pair_index,
"pair_total": pair_total,
"pair_label": task_label,
"pair_started_monotonic": time.monotonic(),
"message": f"[{engine_code}/{profile}] Running {pair_index}/{pair_total}: {task_label}",
}
)
await task_service.add_log(
job.task_id,
"INFO",
(
f"{engine_title} pair {pair_index}/{pair_total} started: {task_label} "
f"(work_dir={event.get('work_dir')})"
),
)
await task_service.update_task(
job.task_id,
progress=progress_state["progress"],
message=progress_state["message"],
)
continue
if event_type == "pair_finished":
success = bool(event.get("success"))
returncode = int(event.get("returncode") or 0)
progress = min(
90,
max(
int(progress_state["progress"] or 5),
5 + int((max(pair_index, 0) / pair_total) * 80) if success else int(progress_state["progress"] or 5),
),
)
progress_state.update(
{
"progress": progress,
"pair_index": pair_index,
"pair_total": pair_total,
"pair_label": task_label,
"pair_started_monotonic": None,
}
)
if success:
progress_state["message"] = f"[{engine_code}/{profile}] Finished {pair_index}/{pair_total}: {task_label}"
await task_service.add_log(
job.task_id,
"INFO",
f"{engine_title} pair {pair_index}/{pair_total} completed: {task_label}",
)
else:
error_text = str(event.get("error") or "").strip()
timeout_note = " (timeout)" if returncode == -1 else ""
progress_state["message"] = f"[{engine_code}/{profile}] Failed {pair_index}/{pair_total}: {task_label}"
await task_service.add_log(
job.task_id,
"WARNING",
(
f"{engine_title} pair {pair_index}/{pair_total} failed{timeout_note}: "
f"{task_label} (rc={returncode})"
f"{f', error={error_text}' if error_text else ''}"
),
)
if pair_index < pair_total:
await task_service.add_log(
job.task_id,
"WARNING",
f"{engine_title} will continue with the next pair ({pair_index + 1}/{pair_total}).",
)
await task_service.update_task(
job.task_id,
progress=progress_state["progress"],
message=progress_state["message"],
)
async def _task_keepalive():
while True:
await asyncio.sleep(30)
try:
message = str(progress_state.get("message") or f"[{engine_code}/{profile}] Running in WSL...")
started_monotonic = progress_state.get("pair_started_monotonic")
if isinstance(started_monotonic, (int, float)):
elapsed_seconds = max(0, int(time.monotonic() - float(started_monotonic)))
message = f"{message} (elapsed={elapsed_seconds}s)"
await task_service.update_task(
job.task_id,
progress=5,
message=f"[{engine_code}/{profile}] Running in WSL...",
progress=int(progress_state.get("progress") or 5),
message=message,
)
except Exception as exc:
print(f"[keepalive] WARNING: failed to update task {job.task_id}: {exc}")
progress_task = asyncio.create_task(_consume_progress())
keepalive_task = asyncio.create_task(_task_keepalive())
try:
result = await asyncio.to_thread(engine.run, request)
@@ -1942,6 +2078,8 @@ async def _handle_isce2_run(job: SystemJobORM) -> None:
await keepalive_task
except asyncio.CancelledError:
pass
await progress_queue.put(None)
await progress_task
detail = result.detail or {}
@@ -1949,7 +2087,7 @@ async def _handle_isce2_run(job: SystemJobORM) -> None:
await task_service.add_log(
job.task_id,
"INFO",
f"ISCE2 run mode={detail.get('mode', 'unknown')}, task_count={detail.get('task_count', 0)}",
f"{engine_title} run mode={detail.get('mode', 'unknown')}, task_count={detail.get('task_count', 0)}",
)
for invalid in detail.get("invalid_candidates", []) or []:
@@ -2062,16 +2200,26 @@ async def _handle_isce2_run(job: SystemJobORM) -> None:
job.task_id,
"INFO",
(
f"Auto-published ISCE2 results from {len(output_dirs)} directory(s). "
f"Auto-published {engine_title} results from {len(output_dirs)} directory(s). "
f"processed={publish_result.get('processed', 0)} "
f"issues={rebuild_result.get('issue_count', 0) if rebuild_result else 0}"
),
)
if int(publish_result.get("processed", 0) or 0) <= 0:
await task_service.add_log(
job.task_id,
"WARNING",
(
f"No publishable {engine_title} result bundle was detected under "
f"{len(output_dirs)} output director"
f"{'y' if len(output_dirs) == 1 else 'ies'}."
),
)
except Exception as exc:
await task_service.add_log(
job.task_id,
"WARNING",
f"Auto-publish ISCE2 results failed: {exc}",
f"Auto-publish {engine_title} results failed: {exc}",
)
if result.success:
@@ -2080,7 +2228,7 @@ async def _handle_isce2_run(job: SystemJobORM) -> None:
status="COMPLETED",
progress=100,
message=(
f"[{engine_code}/{profile}] 完成"
f"[{engine_code}/{profile}] 完成"
f"成功 {result.pairs_processed} 对,失败 {result.pairs_failed}"
),
)
@@ -2095,6 +2243,22 @@ async def _handle_isce2_run(job: SystemJobORM) -> None:
)
async def _handle_isce2_run(job: SystemJobORM) -> None:
await _handle_queued_engine_run(
job,
engine_title="ISCE2",
fallback_timeout_seconds=settings.ISCE2_PER_TASK_TIMEOUT_SECONDS,
)
async def _handle_pyint_run(job: SystemJobORM) -> None:
await _handle_queued_engine_run(
job,
engine_title="PyINT",
fallback_timeout_seconds=settings.PYINT_DEFAULT_TIMEOUT_SECONDS,
)
async def _handle_water_geocode(job: SystemJobORM) -> None:
"""单景 SAR 地理编码 job handler(多视 + 地理编码 + 辐射定标)。"""
from .water_service import run_geocoding_workflow, WATER_RESULTS_DIR
@@ -2860,6 +3024,7 @@ _HANDLERS = {
JOB_TYPE_IDL_RUN_IMPORT: _handle_idl_run_import,
JOB_TYPE_IDL_RUN_DINSAR: _handle_idl_run_dinsar,
JOB_TYPE_ISCE2_RUN: _handle_isce2_run,
JOB_TYPE_PYINT_RUN: _handle_pyint_run,
JOB_TYPE_WATER_GEOCODE: _handle_water_geocode,
JOB_TYPE_WATER_FLOOD: _handle_water_flood,
JOB_TYPE_WATER_DETECT: _handle_water_detect,
@@ -0,0 +1,729 @@
"""PyINT input-asset resolution and materialization helpers."""
from __future__ import annotations
import json
import os
import shutil
from datetime import datetime
from typing import Any, Dict, List
from ..config import settings
from .orbit_converter import get_source_orbit_inventory
from .pyint_service import (
discover_lt1_archives,
infer_scene_date_from_archives,
infer_task_identity,
validate_pyint_root_dir,
)
VALID_DEM_MODES = {"local_fabdem", "opentopo", "prepared_file"}
VALID_ORBIT_POLICIES = {"validate_only", "require_txt", "stage_txt"}
VALID_PRECISE_ORBIT_MODES = {"replace", "replace_and_validate"}
def _utc_now_text() -> str:
return datetime.utcnow().isoformat(timespec="seconds") + "Z"
def _normalize_path(path: Any) -> str:
text = str(path or "").strip().strip('"').strip("'")
if not text:
return ""
return os.path.normpath(os.path.abspath(text))
def _copy_json_safe(value: Any) -> Any:
return json.loads(json.dumps(value, ensure_ascii=False, default=str))
def _normalize_lt1_satellite(value: Any) -> str:
text = str(value or "").strip().upper().replace("-", "").replace("_", "")
if "LT1A" in text:
return "LT1A"
if "LT1B" in text:
return "LT1B"
if text in {"A", "LTA"}:
return "LT1A"
if text in {"B", "LTB"}:
return "LT1B"
return ""
def _infer_satellite_from_archives(paths: List[str]) -> str:
satellites = {
satellite
for path in paths
for satellite in [_normalize_lt1_satellite(os.path.basename(path))]
if satellite
}
if len(satellites) == 1:
return next(iter(satellites))
return ""
def _get_dem_mode() -> str:
raw_mode = str(getattr(settings, "PYINT_DEM_MODE", "local_fabdem") or "local_fabdem").strip().lower()
if raw_mode not in VALID_DEM_MODES:
return "local_fabdem"
return raw_mode
def _get_orbit_policy() -> str:
raw_policy = str(getattr(settings, "PYINT_ORBIT_POLICY", "require_txt") or "require_txt").strip().lower()
if raw_policy not in VALID_ORBIT_POLICIES:
return "require_txt"
return raw_policy
def _get_orbit_pool_root() -> str:
explicit = _normalize_path(getattr(settings, "PYINT_ORBIT_POOL_TXT", ""))
if explicit:
return explicit
return _normalize_path(settings.ORBIT_POOL_ENVI)
def get_pyint_precise_orbit_bridge_summary() -> Dict[str, Any]:
mode = str(getattr(settings, "PYINT_LT1_PRECISE_ORBIT_MODE", "replace") or "replace").strip().lower()
if mode not in VALID_PRECISE_ORBIT_MODES:
mode = "replace"
return {
"enabled": bool(getattr(settings, "PYINT_LT1_PRECISE_ORBIT_ENABLED", True)),
"mode": mode,
"strict": bool(getattr(settings, "PYINT_LT1_PRECISE_ORBIT_STRICT", True)),
"validate_with_orb_filt": bool(getattr(settings, "PYINT_LT1_PRECISE_ORBIT_VALIDATE_WITH_ORB_FILT", False)),
"backup": bool(getattr(settings, "PYINT_LT1_PRECISE_ORBIT_BACKUP", True)),
"orb_filt_degree": max(1, int(getattr(settings, "PYINT_LT1_PRECISE_ORBIT_ORB_FILT_DEGREE", 5) or 5)),
}
def _prepared_dem_variants(value: Any) -> List[str]:
text = str(value or "").strip().strip('"').strip("'")
if not text:
return []
normalized = _normalize_path(text)
if not normalized:
return []
candidates = [normalized]
root, ext = os.path.splitext(normalized)
if ext.lower() == ".wgs84":
candidates.append(root)
elif not ext:
candidates.append(normalized + ".wgs84")
unique: List[str] = []
seen: set[str] = set()
for candidate in candidates:
item = _normalize_path(candidate)
if not item or item in seen:
continue
seen.add(item)
unique.append(item)
return unique
def _resolve_prepared_dem_path() -> Dict[str, str]:
explicit_value = getattr(settings, "PYINT_PREPARED_DEM_PATH", "")
explicit_candidates = _prepared_dem_variants(explicit_value)
if str(explicit_value or "").strip():
for candidate in explicit_candidates:
if os.path.isfile(candidate):
return {
"path": candidate,
"resolved_from": "explicit",
}
return {
"path": "",
"resolved_from": "explicit",
}
sources = [
("isce2_dem_path", getattr(settings, "ISCE2_DEM_PATH", "")),
("idl_dinsar_dem_base_file", getattr(settings, "IDL_DINSAR_DEM_BASE_FILE", "")),
]
for source_name, raw_value in sources:
for candidate in _prepared_dem_variants(raw_value):
if os.path.isfile(candidate):
return {
"path": candidate,
"resolved_from": source_name,
}
return {
"path": "",
"resolved_from": "",
}
def _inspect_prepared_dem_path(path: Any) -> Dict[str, Any]:
normalized = _normalize_path(path)
if not normalized:
return {
"path": "",
"exists": False,
"kind": "",
"gamma_par_path": "",
"gamma_par_exists": False,
"xml_path": "",
"xml_exists": False,
"hdr_path": "",
"hdr_exists": False,
"vrt_path": "",
"vrt_exists": False,
"open_path": "",
}
gamma_par_path = normalized + ".par"
xml_path = normalized + ".xml"
hdr_path = normalized + ".hdr"
vrt_path = normalized + ".vrt"
path_exists = os.path.isfile(normalized)
gamma_par_exists = os.path.isfile(gamma_par_path)
xml_exists = os.path.isfile(xml_path)
hdr_exists = os.path.isfile(hdr_path)
vrt_exists = os.path.isfile(vrt_path)
kind = ""
open_path = ""
if path_exists and gamma_par_exists:
kind = "gamma_ready"
open_path = normalized
elif path_exists and (xml_exists or hdr_exists or vrt_exists):
kind = "source_dem"
open_path = vrt_path if vrt_exists else normalized
return {
"path": normalized,
"exists": path_exists,
"kind": kind,
"gamma_par_path": gamma_par_path,
"gamma_par_exists": gamma_par_exists,
"xml_path": xml_path,
"xml_exists": xml_exists,
"hdr_path": hdr_path,
"hdr_exists": hdr_exists,
"vrt_path": vrt_path,
"vrt_exists": vrt_exists,
"open_path": open_path,
}
def get_pyint_dem_summary() -> Dict[str, Any]:
mode = _get_dem_mode()
strict = bool(getattr(settings, "PYINT_DEM_STRICT", True))
cache_root = _normalize_path(settings.PYINT_DEM_ROOT)
fabdem_root = _normalize_path(getattr(settings, "PYINT_FABDEM_ROOT", ""))
prepared_dem_resolution = _resolve_prepared_dem_path()
prepared_dem_info = _inspect_prepared_dem_path(prepared_dem_resolution.get("path"))
opentopo_dem_type = str(getattr(settings, "PYINT_OPENTOPO_DEM_TYPE", "SRTMGL1") or "SRTMGL1").strip() or "SRTMGL1"
opentopo_api_key = str(getattr(settings, "PYINT_OPENTOPO_API_KEY", "") or "").strip()
warnings: List[str] = []
blockers: List[str] = []
source_root = ""
source_exists = False
if mode == "local_fabdem":
source_root = fabdem_root
source_exists = bool(source_root and os.path.isdir(source_root))
elif mode == "prepared_file":
source_root = str(prepared_dem_info.get("path") or "")
source_exists = bool(prepared_dem_info.get("exists"))
cache_root_exists = bool(cache_root and os.path.isdir(cache_root))
if mode == "local_fabdem":
if not fabdem_root:
message = "未配置 PYINT_FABDEM_ROOT。"
if strict:
blockers.append(message)
else:
warnings.append(message)
elif not os.path.isdir(fabdem_root):
message = f"本地 FABDEM 根目录不存在: {fabdem_root}"
if strict:
blockers.append(message)
else:
warnings.append(message)
elif mode == "opentopo":
if not opentopo_api_key:
message = "DEM 策略为 OpenTopography,但未配置 PYINT_OPENTOPO_API_KEY。"
if strict:
blockers.append(message)
else:
warnings.append(message)
elif mode == "prepared_file":
if not prepared_dem_info.get("path"):
if prepared_dem_resolution.get("resolved_from") == "explicit":
message = "PYINT_PREPARED_DEM_PATH 已配置,但目标文件不存在。"
else:
message = (
"未配置 PYINT_PREPARED_DEM_PATH,且未能从 ISCE2_DEM_PATH / "
"IDL_DINSAR_DEM_BASE_FILE 解析现有 DEM。"
)
if strict:
blockers.append(message)
else:
warnings.append(message)
elif prepared_dem_info.get("kind") not in {"gamma_ready", "source_dem"}:
message = (
"现有 DEM 缺少可识别 sidecar,至少需要同名 .par,或 .xml/.hdr/.vrt 中的一个: "
+ str(prepared_dem_info.get("path") or "")
)
if strict:
blockers.append(message)
else:
warnings.append(message)
if not cache_root:
blockers.append("未配置 PYINT_DEM_ROOT。")
elif not cache_root_exists:
warnings.append(f"DEM 缓存目录当前不存在,运行时将尝试创建: {cache_root}")
status = "ok"
if blockers:
status = "blocked"
elif warnings:
status = "warning"
if mode == "local_fabdem":
detail = "使用本地 FABDEM 瓦片目录,由 PyINT 在 DEMDIR 中生成运行期 DEM。"
elif mode == "prepared_file":
if prepared_dem_info.get("kind") == "gamma_ready":
detail = "使用现有 Gamma DEM,运行时将直接注入到 PyINT 模板。"
else:
detail = "使用现有系统 DEM,运行时将按任务覆盖区裁剪并转换为本次任务的 Gamma DEM。"
else:
detail = f"使用 OpenTopography 在线 DEM 源,DEM 类型为 {opentopo_dem_type}"
return {
"mode": mode,
"strict": strict,
"source_root": source_root,
"source_exists": source_exists,
"cache_root": cache_root,
"cache_root_exists": cache_root_exists,
"fabdem_root": fabdem_root,
"prepared_dem_path": str(prepared_dem_info.get("path") or ""),
"prepared_dem_resolved_from": str(prepared_dem_resolution.get("resolved_from") or ""),
"prepared_dem_kind": str(prepared_dem_info.get("kind") or ""),
"prepared_dem_open_path": str(prepared_dem_info.get("open_path") or ""),
"prepared_dem_support": {
"gamma_par_exists": bool(prepared_dem_info.get("gamma_par_exists")),
"xml_exists": bool(prepared_dem_info.get("xml_exists")),
"hdr_exists": bool(prepared_dem_info.get("hdr_exists")),
"vrt_exists": bool(prepared_dem_info.get("vrt_exists")),
},
"opentopo_dem_type": opentopo_dem_type,
"opentopo_api_key_configured": bool(opentopo_api_key),
"status": status,
"detail": detail,
"warnings": warnings,
"blockers": blockers,
"allow_submit": not blockers,
}
def _load_orbit_inventory() -> Dict[str, Any]:
pool_root = _get_orbit_pool_root()
if not pool_root:
return {
"pool_root": "",
"pool_exists": False,
"files": {},
"warnings": ["未配置 PYINT_ORBIT_POOL_TXT,且 ORBIT_POOL_ENVI 为空。"],
}
if not os.path.isdir(pool_root):
return {
"pool_root": pool_root,
"pool_exists": False,
"files": {},
"warnings": [f"轨道池目录不存在: {pool_root}"],
}
inventory = get_source_orbit_inventory(pool_root, recursive=True)
return {
"pool_root": pool_root,
"pool_exists": True,
"files": inventory.get("files", {}),
"warnings": list(inventory.get("errors", []) or []),
"duplicate_count": int(inventory.get("duplicate_count", 0) or 0),
}
def get_pyint_orbit_context() -> Dict[str, Any]:
return _load_orbit_inventory()
def _resolve_orbit_file(
*,
role: str,
satellite: str,
date_text: str,
pool_root: str,
orbit_files: Dict[str, Dict[str, Any]],
) -> Dict[str, Any]:
satellite_text = _normalize_lt1_satellite(satellite)
normalized_date = str(date_text or "").strip()
expected_name = (
f"{satellite_text}_GpsData_GAS_C_{normalized_date}.txt"
if satellite_text and normalized_date
else ""
)
result: Dict[str, Any] = {
"role": role,
"satellite": satellite_text,
"date": normalized_date,
"expected_name": expected_name,
"pool_root": pool_root,
"resolved": False,
"path": "",
"resolution_method": "",
"staged_path": "",
}
if not satellite_text:
result["error"] = f"{role} 场景未能识别 LT-1 卫星型号。"
return result
if not normalized_date:
result["error"] = f"{role} 场景未能识别成像日期。"
return result
stem = os.path.splitext(expected_name)[0]
item = orbit_files.get(stem)
if item and os.path.isfile(item.get("path", "")):
result.update(
{
"resolved": True,
"path": _normalize_path(item["path"]),
"resolution_method": "indexed_pool_scan",
}
)
return result
direct_candidate = os.path.join(pool_root, satellite_text, expected_name)
if os.path.isfile(direct_candidate):
result.update(
{
"resolved": True,
"path": _normalize_path(direct_candidate),
"resolution_method": "direct_satellite_subdir",
}
)
return result
flat_candidate = os.path.join(pool_root, expected_name)
if os.path.isfile(flat_candidate):
result.update(
{
"resolved": True,
"path": _normalize_path(flat_candidate),
"resolution_method": "direct_pool_root",
}
)
return result
result["error"] = f"轨道池中缺少 {expected_name}"
return result
def resolve_pyint_task_input_assets(
task_dir: str,
*,
dem_summary: Dict[str, Any] | None = None,
orbit_context: Dict[str, Any] | None = None,
) -> Dict[str, Any]:
task_dir = _normalize_path(task_dir)
task_identity = infer_task_identity(task_dir)
pair_meta = task_identity["pair_meta"]
archives = discover_lt1_archives(task_dir)
master_archives = list(archives.get("master", []) or [])
slave_archives = list(archives.get("slave", []) or [])
warnings: List[str] = []
blockers: List[str] = []
master_date = task_identity["master_date"] or infer_scene_date_from_archives(master_archives)
slave_date = task_identity["slave_date"] or infer_scene_date_from_archives(slave_archives)
master_satellite = _normalize_lt1_satellite(pair_meta.get("master_satellite")) or _infer_satellite_from_archives(master_archives)
slave_satellite = _normalize_lt1_satellite(pair_meta.get("slave_satellite")) or _infer_satellite_from_archives(slave_archives)
if not master_archives:
blockers.append("master/ 下未发现 LT-1 原始输入(LT1*.tar.gz 或 LT1*.tiff)。")
if not slave_archives:
blockers.append("slave/ 下未发现 LT-1 原始输入(LT1*.tar.gz 或 LT1*.tiff)。")
if not master_date:
blockers.append("未能识别主影像日期。")
if not slave_date:
blockers.append("未能识别从影像日期。")
orbit_policy = _get_orbit_policy()
orbit_context = orbit_context or get_pyint_orbit_context()
orbit_pool_root = orbit_context.get("pool_root", "")
orbit_pool_exists = bool(orbit_context.get("pool_exists"))
orbit_files = orbit_context.get("files", {}) or {}
orbit_warnings: List[str] = []
if orbit_context.get("warnings"):
orbit_warnings.extend(str(item) for item in orbit_context["warnings"] if item)
master_orbit = _resolve_orbit_file(
role="master",
satellite=master_satellite,
date_text=master_date,
pool_root=orbit_pool_root,
orbit_files=orbit_files,
)
slave_orbit = _resolve_orbit_file(
role="slave",
satellite=slave_satellite,
date_text=slave_date,
pool_root=orbit_pool_root,
orbit_files=orbit_files,
)
for orbit_item in (master_orbit, slave_orbit):
if orbit_item.get("resolved"):
continue
message = str(orbit_item.get("error") or f"{orbit_item.get('role')} 轨道缺失").strip()
if orbit_policy == "validate_only":
orbit_warnings.append(message)
else:
blockers.append(message)
if not orbit_pool_root:
if orbit_policy == "validate_only":
orbit_warnings.append("轨道池未配置,当前仅记录警告。")
else:
blockers.append("轨道池未配置。")
elif not orbit_pool_exists:
if orbit_policy == "validate_only":
orbit_warnings.append(f"轨道池目录不可用: {orbit_pool_root}")
else:
blockers.append(f"轨道池目录不可用: {orbit_pool_root}")
warnings.extend(orbit_warnings)
task_source = {
"task_dir": task_dir,
"task_name": task_identity["task_name"],
"task_alias": task_identity["task_alias"],
"pair_key": task_identity["pair_key"],
"master_date": master_date,
"slave_date": slave_date,
"master_satellite": master_satellite,
"slave_satellite": slave_satellite,
"archives": {
"master": master_archives,
"slave": slave_archives,
},
}
precise_orbit_bridge = get_pyint_precise_orbit_bridge_summary()
orbits_summary = {
"policy": orbit_policy,
"pool_root": orbit_pool_root,
"pool_exists": orbit_pool_exists,
"master": master_orbit,
"slave": slave_orbit,
"resolved_count": int(bool(master_orbit.get("resolved"))) + int(bool(slave_orbit.get("resolved"))),
"missing_count": int(not master_orbit.get("resolved")) + int(not slave_orbit.get("resolved")),
"warnings": orbit_warnings,
"stage_mode": "copy" if orbit_policy == "stage_txt" or precise_orbit_bridge.get("enabled") else "none",
"precise_orbit_bridge": precise_orbit_bridge,
}
dem_payload = _copy_json_safe(dem_summary or get_pyint_dem_summary())
allow_submit = not blockers and bool(dem_payload.get("allow_submit", True))
return {
"task_name": task_identity["task_name"],
"task_alias": task_identity["task_alias"],
"pair_key": task_identity["pair_key"],
"task_dir": task_dir,
"master_date": master_date,
"slave_date": slave_date,
"master_satellite": master_satellite,
"slave_satellite": slave_satellite,
"archive_counts": {
"master": len(master_archives),
"slave": len(slave_archives),
},
"warnings": warnings,
"blockers": blockers,
"allow_submit": allow_submit,
"task_source": task_source,
"dem": dem_payload,
"orbit_resolution": {
"master": master_orbit,
"slave": slave_orbit,
},
"input_assets": {
"task_source": task_source,
"dem": dem_payload,
"orbits": orbits_summary,
},
}
def build_pyint_input_preview(root_dir: str, num_to_process: int = 0) -> Dict[str, Any]:
validation = validate_pyint_root_dir(root_dir, num_to_process)
dem_summary = get_pyint_dem_summary()
orbit_context = get_pyint_orbit_context()
warnings: List[str] = list(dem_summary.get("warnings") or [])
blockers: List[str] = list(dem_summary.get("blockers") or [])
task_summaries: List[Dict[str, Any]] = []
resolved_task_count = 0
missing_task_count = 0
for task_dir in validation.get("task_dirs", []) or []:
task_summary = resolve_pyint_task_input_assets(
task_dir,
dem_summary=dem_summary,
orbit_context=orbit_context,
)
task_summaries.append(task_summary)
if task_summary.get("warnings"):
warnings.extend(
f"{task_summary['task_alias']}: {item}"
for item in task_summary["warnings"]
)
if task_summary.get("blockers"):
blockers.extend(
f"{task_summary['task_alias']}: {item}"
for item in task_summary["blockers"]
)
if task_summary["input_assets"]["orbits"]["missing_count"] == 0:
resolved_task_count += 1
else:
missing_task_count += 1
allow_submit = not blockers
precise_orbit_bridge = get_pyint_precise_orbit_bridge_summary()
return {
"root_dir": validation["root_dir"],
"mode": validation["mode"],
"task_count": len(task_summaries),
"selected_task_count": len(task_summaries),
"allow_submit": allow_submit,
"warnings": warnings,
"blockers": blockers,
"invalid_candidates": validation.get("invalid_candidates", []),
"dem": dem_summary,
"orbits": {
"policy": _get_orbit_policy(),
"pool_root": orbit_context.get("pool_root", ""),
"pool_exists": bool(orbit_context.get("pool_exists")),
"resolved_task_count": resolved_task_count,
"missing_task_count": missing_task_count,
"duplicate_count": int(orbit_context.get("duplicate_count", 0) or 0),
"warnings": list(orbit_context.get("warnings") or []),
},
"precise_orbit_bridge": precise_orbit_bridge,
"tasks": task_summaries,
}
def summarize_preview_blockers(preview: Dict[str, Any], limit: int = 8) -> str:
blockers = [str(item).strip() for item in (preview.get("blockers") or []) if str(item).strip()]
if not blockers:
return ""
if len(blockers) <= limit:
return "; ".join(blockers)
return "; ".join(blockers[:limit]) + f"; 其余 {len(blockers) - limit} 项已省略"
def materialize_pyint_input_assets(
*,
task_summary: Dict[str, Any],
input_assets_dir: str,
project_name: str = "",
) -> Dict[str, Any]:
input_assets_dir = _normalize_path(input_assets_dir)
os.makedirs(input_assets_dir, exist_ok=True)
record_enabled = bool(getattr(settings, "PYINT_RECORD_INPUT_ASSETS", True))
orbits_dir = os.path.join(input_assets_dir, "orbits")
dem_dir = os.path.join(input_assets_dir, "dem")
if record_enabled:
os.makedirs(orbits_dir, exist_ok=True)
os.makedirs(dem_dir, exist_ok=True)
manifest = _copy_json_safe(task_summary.get("input_assets") or {})
manifest["generated_at"] = _utc_now_text()
manifest["task_name"] = task_summary.get("task_name")
manifest["task_alias"] = task_summary.get("task_alias")
manifest["pair_key"] = task_summary.get("pair_key")
manifest["task_dir"] = task_summary.get("task_dir")
manifest["allow_submit"] = bool(task_summary.get("allow_submit"))
manifest["warnings"] = list(task_summary.get("warnings") or [])
manifest["blockers"] = list(task_summary.get("blockers") or [])
dem_summary = manifest.get("dem") or {}
if project_name:
dem_summary["resolved_output_dir"] = os.path.join(_normalize_path(settings.PYINT_DEM_ROOT), project_name)
manifest["dem"] = dem_summary
orbits_summary = manifest.get("orbits") or {}
staged_count = 0
precise_orbit_bridge = get_pyint_precise_orbit_bridge_summary()
should_stage_orbits = record_enabled and (
str(orbits_summary.get("policy") or "").strip().lower() == "stage_txt"
or precise_orbit_bridge.get("enabled")
)
if should_stage_orbits:
for role in ("master", "slave"):
orbit_item = orbits_summary.get(role) or {}
orbit_path = _normalize_path(orbit_item.get("path"))
expected_name = str(orbit_item.get("expected_name") or "").strip()
if not orbit_item.get("resolved") or not orbit_path or not expected_name:
continue
target_path = os.path.join(orbits_dir, expected_name)
if not os.path.exists(target_path):
shutil.copy2(orbit_path, target_path)
orbit_item["staged_path"] = target_path
orbit_item["stage_operation"] = "copied"
orbit_item["stage_reason"] = "precise_orbit_bridge" if precise_orbit_bridge.get("enabled") else "stage_txt_policy"
staged_count += 1
orbits_summary[role] = orbit_item
manifest["orbits"] = orbits_summary
materialized = {
"input_assets_dir": input_assets_dir,
"record_enabled": record_enabled,
"orbits_dir": orbits_dir if record_enabled else "",
"dem_dir": dem_dir if record_enabled else "",
"orbits_staged_count": staged_count,
"task_manifest_path": "",
"dem_summary_path": "",
"orbit_summary_path": "",
"input_assets": manifest,
}
if not record_enabled:
return materialized
task_manifest_path = os.path.join(input_assets_dir, "task_manifest.json")
dem_summary_path = os.path.join(dem_dir, "dem_summary.json")
orbit_summary_path = os.path.join(orbits_dir, "orbit_summary.json")
with open(task_manifest_path, "w", encoding="utf-8") as fp:
json.dump(manifest, fp, ensure_ascii=False, indent=2)
fp.write("\n")
with open(dem_summary_path, "w", encoding="utf-8") as fp:
json.dump(dem_summary, fp, ensure_ascii=False, indent=2)
fp.write("\n")
with open(orbit_summary_path, "w", encoding="utf-8") as fp:
json.dump(orbits_summary, fp, ensure_ascii=False, indent=2)
fp.write("\n")
materialized.update(
{
"task_manifest_path": task_manifest_path,
"dem_summary_path": dem_summary_path,
"orbit_summary_path": orbit_summary_path,
}
)
return materialized
+409
View File
@@ -0,0 +1,409 @@
"""Helpers for integrating the external PyINT workflow."""
from __future__ import annotations
import os
import re
import shlex
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional
from ..config import get_env_text, read_bool_env, settings
from .dinsar_naming import PAIR_META_FILENAME, build_fallback_pair_key, find_json_sidecar
from .wsl_service import run_wsl_command
LT1_INPUT_GLOBS = ("LT1*.tar.gz", "LT1*.tiff")
DEFAULT_RANGE_LOOKS = 2
DEFAULT_AZIMUTH_LOOKS = 2
DEFAULT_PARALLEL_WORKERS = 1
MAX_LOOKS = 32
MAX_PARALLEL_WORKERS = 16
_DATE_TOKEN_RE = re.compile(r"(20\d{6})")
_SAFE_TEXT_RE = re.compile(r"[^0-9A-Za-z._-]+")
@dataclass
class PyintCheck:
name: str
ok: bool
detail: str = ""
skipped: bool = False
@dataclass
class PyintEnvironmentReport:
overall_ok: bool
checks: List[PyintCheck] = field(default_factory=list)
message: str = ""
def _read_env(name: str, default: str = "") -> str:
return get_env_text(name, default) or default
def _read_bool_env(name: str, default: bool = False) -> bool:
return read_bool_env(name, default)
def normalize_date_text(value: Any) -> str:
text = str(value or "").strip()
if not text:
return ""
match = _DATE_TOKEN_RE.search(re.sub(r"\D", "", text))
if match:
return match.group(1)
match = _DATE_TOKEN_RE.search(text)
if match:
return match.group(1)
return ""
def slugify_text(value: Any, *, default: str = "item", max_len: int = 96) -> str:
text = _SAFE_TEXT_RE.sub("_", str(value or "").strip()).strip("._")
if not text:
text = default
return text[:max_len]
def build_project_name(pair_key: str, run_key: str) -> str:
return slugify_text(f"{pair_key}_{run_key}", default="pyint_project", max_len=120)
def windows_path_to_wsl_mount(path: str) -> str:
text = str(path or "").strip().strip('"').strip("'")
if not text:
return ""
normalized = os.path.normpath(text)
if normalized.startswith("/"):
return normalized.replace("\\", "/")
if normalized.startswith("\\\\"):
return ""
drive, tail = os.path.splitdrive(normalized)
if not drive:
return normalized.replace("\\", "/")
drive_letter = drive.rstrip(":").lower()
normalized_tail = tail.replace("\\", "/")
return f"/mnt/{drive_letter}/{normalized_tail}"
def to_wsl_path(path: str) -> str:
return windows_path_to_wsl_mount(path)
def quote_shell(value: str) -> str:
return shlex.quote(str(value or ""))
def discover_lt1_archives(task_dir: str) -> Dict[str, List[str]]:
task_path = Path(os.path.normpath(os.path.abspath(str(task_dir or "").strip())))
result: Dict[str, List[str]] = {"master": [], "slave": []}
for role in ("master", "slave"):
role_dir = task_path / role
if not role_dir.is_dir():
continue
inputs = []
for pattern in LT1_INPUT_GLOBS:
inputs.extend(
str(path.resolve())
for path in role_dir.rglob(pattern)
if path.is_file()
)
result[role] = sorted(set(inputs))
return result
def infer_scene_date_from_archives(paths: Iterable[str]) -> str:
dates = {
date_text
for path in paths
for date_text in [normalize_date_text(os.path.basename(path))]
if date_text
}
if len(dates) == 1:
return next(iter(dates))
return ""
def infer_task_identity(task_dir: str) -> Dict[str, Any]:
task_name = os.path.basename(os.path.normpath(task_dir))
pair_meta = find_json_sidecar(task_dir, PAIR_META_FILENAME, max_levels=0) or {}
task_alias = str(pair_meta.get("task_alias") or task_name).strip() or task_name
pair_key = str(pair_meta.get("pair_key") or "").strip() or build_fallback_pair_key(task_alias, task_dir)
master_date = normalize_date_text(pair_meta.get("master_imaging_date"))
slave_date = normalize_date_text(pair_meta.get("slave_imaging_date"))
return {
"task_name": task_name,
"task_alias": task_alias,
"pair_key": pair_key,
"pair_meta": pair_meta,
"master_date": master_date,
"slave_date": slave_date,
}
def build_template_text(
*,
project_name: str,
master_date: str,
range_looks: int,
azimuth_looks: int,
parallel_workers: int,
unwrap: bool,
geocode: bool,
) -> str:
lines = [
f"# Auto-generated for {project_name}",
"satelite=LT",
f"masterDate={master_date}",
f"range_looks={int(range_looks)}",
f"azimuth_looks={int(azimuth_looks)}",
"download_data=0",
"raw2slc_all=1",
f"raw2slc_all_parallel={int(parallel_workers)}",
"coreg_all=1",
f"coreg_all_parallel={int(parallel_workers)}",
"select_pairs=0",
"diff_all=1",
f"diff_all_parallel={int(parallel_workers)}",
f"unwrap_all={1 if unwrap else 0}",
f"unwrap_all_parallel={int(parallel_workers)}",
f"geocode_all={1 if geocode else 0}",
f"geocode_all_parallel={int(parallel_workers)}",
"geocode_products=hyp3,licsbas",
]
return "\n".join(lines) + "\n"
def validate_pyint_root_dir(root_dir: str, num_to_process: int = 0) -> Dict[str, Any]:
normalized_root = os.path.normpath(os.path.abspath(str(root_dir or "").strip()))
if not root_dir or not os.path.isdir(normalized_root):
raise ValueError(f"PyINT root_dir does not exist or is not a directory: {root_dir}")
def _missing_task_subdirs(task_dir: str) -> List[str]:
missing: List[str] = []
for subdir in ("master", "slave"):
if not os.path.isdir(os.path.join(task_dir, subdir)):
missing.append(subdir)
return missing
def _iter_child_dirs(directory: str):
with os.scandir(directory) as entries:
child_dirs = [entry for entry in entries if entry.is_dir()]
child_dirs.sort(key=lambda entry: entry.name.lower())
return child_dirs
if not _missing_task_subdirs(normalized_root):
task_dirs = [normalized_root]
invalid_candidates: List[Dict[str, Any]] = []
mode = "single_task_dir"
else:
task_dirs = []
invalid_candidates = []
for entry in _iter_child_dirs(normalized_root):
if not entry.name.lower().startswith("task_"):
continue
missing = _missing_task_subdirs(entry.path)
if missing:
invalid_candidates.append(
{"name": entry.name, "path": entry.path, "missing_subdirs": missing}
)
continue
task_dirs.append(os.path.normpath(entry.path))
mode = "task_root_dir"
if not task_dirs:
detail = ""
if invalid_candidates:
formatted = ", ".join(
f"{item['name']} missing {','.join(item['missing_subdirs'])}"
for item in invalid_candidates[:5]
)
detail = f" Invalid candidates: {formatted}."
raise ValueError(
"PyINT root_dir must be either a single task directory containing "
"'master' and 'slave', or a parent directory containing valid Task_* subdirectories."
f"{detail}"
)
selected_count = int(num_to_process or 0)
if selected_count > 0:
task_dirs = task_dirs[:selected_count]
return {
"root_dir": normalized_root,
"mode": mode,
"task_dirs": task_dirs,
"task_count": len(task_dirs),
"invalid_candidates": invalid_candidates,
}
def resolve_time_baseline_days(master_date: str, slave_date: str, pair_meta: Dict[str, Any]) -> int:
raw_days = pair_meta.get("time_baseline_days")
try:
if raw_days not in (None, ""):
return int(raw_days)
except (TypeError, ValueError):
pass
if not master_date or not slave_date:
return 0
try:
master_dt = datetime.strptime(master_date, "%Y%m%d")
slave_dt = datetime.strptime(slave_date, "%Y%m%d")
except ValueError:
return 0
return (slave_dt - master_dt).days
def _gamma_prefix(gamma_env_script_wsl: str) -> str:
script = str(gamma_env_script_wsl or "").strip()
if not script:
return ""
return f". {quote_shell(script)} >/dev/null 2>&1 && "
def check_pyint_environment(
*,
enabled: Optional[bool] = None,
distro: Optional[str] = None,
python_cmd: Optional[str] = None,
pyint_home: Optional[str] = None,
pyint_app_script: Optional[str] = None,
template_root: Optional[str] = None,
work_root: Optional[str] = None,
output_root: Optional[str] = None,
dem_root: Optional[str] = None,
gamma_env_script: Optional[str] = None,
smoke_test: Optional[bool] = None,
) -> PyintEnvironmentReport:
enabled_value = _read_bool_env("PYINT_ENABLED", False) if enabled is None else bool(enabled)
if not enabled_value:
return PyintEnvironmentReport(
overall_ok=False,
checks=[PyintCheck(name="PYINT_ENABLED", ok=False, detail="PYINT_ENABLED=false")],
message="PyINT is disabled. Set PYINT_ENABLED=true to enable it.",
)
distro_value = str(distro or _read_env("PYINT_WSL_DISTRO", settings.ISCE2_WSL_DISTRO)).strip()
python_value = str(python_cmd or _read_env("PYINT_WSL_PYTHON", settings.ISCE2_PYTHON)).strip()
pyint_home_wsl = to_wsl_path(str(pyint_home or _read_env("PYINT_HOME", "")))
pyint_app_wsl = to_wsl_path(str(pyint_app_script or _read_env("PYINT_APP_SCRIPT", "")))
template_root_wsl = to_wsl_path(str(template_root or _read_env("PYINT_TEMPLATE_ROOT", "")))
work_root_wsl = to_wsl_path(str(work_root or _read_env("PYINT_WORK_ROOT", "")))
output_root_wsl = to_wsl_path(str(output_root or _read_env("PYINT_OUTPUT_ROOT", "")))
dem_root_wsl = to_wsl_path(str(dem_root or _read_env("PYINT_DEM_ROOT", "")))
gamma_env_wsl = to_wsl_path(str(gamma_env_script or _read_env("PYINT_GAMMA_ENV_SCRIPT", "")))
smoke_enabled = _read_bool_env("PYINT_SMOKE_TEST_ENABLED", False) if smoke_test is None else bool(smoke_test)
precise_orbit_enabled = _read_bool_env("PYINT_LT1_PRECISE_ORBIT_ENABLED", True)
checks: List[PyintCheck] = []
def add(name: str, ok: bool, detail: str = "", skipped: bool = False) -> None:
checks.append(PyintCheck(name=name, ok=ok, detail=detail, skipped=skipped))
rc, out, err = run_wsl_command("echo pyint_alive", distro=distro_value, timeout=15)
wsl_ok = rc == 0 and "pyint_alive" in out
add("WSL distro", wsl_ok, out or err or distro_value)
if not wsl_ok:
return PyintEnvironmentReport(
overall_ok=False,
checks=checks,
message=f"WSL distro is unavailable: {distro_value}",
)
rc, out, err = run_wsl_command(
f"{quote_shell(python_value)} --version",
distro=distro_value,
timeout=15,
)
add("WSL Python", rc == 0, out or err or python_value)
if pyint_home_wsl:
rc, out, err = run_wsl_command(
f"test -d {quote_shell(pyint_home_wsl)} && echo ok",
distro=distro_value,
timeout=10,
)
add("PYINT_HOME", rc == 0 and "ok" in out, pyint_home_wsl or err)
else:
add("PYINT_HOME", False, "PYINT_HOME is empty")
if pyint_app_wsl:
rc, out, err = run_wsl_command(
f"test -f {quote_shell(pyint_app_wsl)} && echo ok",
distro=distro_value,
timeout=10,
)
add("pyintApp.py", rc == 0 and "ok" in out, pyint_app_wsl or err)
else:
add("pyintApp.py", False, "PYINT_APP_SCRIPT is empty")
for name, path_text in (
("PYINT_TEMPLATE_ROOT", template_root_wsl),
("PYINT_WORK_ROOT", work_root_wsl),
("PYINT_OUTPUT_ROOT", output_root_wsl),
("PYINT_DEM_ROOT", dem_root_wsl),
):
if not path_text:
add(name, False, f"{name} is empty")
continue
rc, out, err = run_wsl_command(
f"test -d {quote_shell(path_text)} && test -w {quote_shell(path_text)} && echo ok",
distro=distro_value,
timeout=10,
)
add(name, rc == 0 and "ok" in out, path_text or err)
if gamma_env_wsl:
rc, out, err = run_wsl_command(
f"test -f {quote_shell(gamma_env_wsl)} && echo ok",
distro=distro_value,
timeout=10,
)
add("GAMMA env script", rc == 0 and "ok" in out, gamma_env_wsl or err)
else:
add("GAMMA env script", True, "Not configured; using current PATH", skipped=True)
gamma_prefix = _gamma_prefix(gamma_env_wsl)
for name, command_name in (
("GAMMA LT1 import", "LT1_import_SLC_from_zipfiles1"),
("GAMMA geocode_back", "geocode_back"),
):
rc, out, err = run_wsl_command(
gamma_prefix + f"command -v {quote_shell(command_name)}",
distro=distro_value,
timeout=10,
)
add(name, rc == 0 and bool(out.strip()), out or err or command_name)
helper_path = (
Path(__file__).resolve().parent.parent
/ "pyint_pipeline"
/ "apply_lt1_precise_orbit.py"
)
if precise_orbit_enabled:
add("LT1 precise orbit bridge helper", helper_path.is_file(), str(helper_path))
else:
add("LT1 precise orbit bridge helper", True, "Skipped", skipped=True)
if smoke_enabled:
smoke_cmd = (
f"export PYTHONPATH={quote_shell(pyint_home_wsl)}:$PYTHONPATH && "
+ gamma_prefix
+ f"{quote_shell(python_value)} {quote_shell(pyint_app_wsl)} -h >/dev/null"
)
rc, out, err = run_wsl_command(smoke_cmd, distro=distro_value, timeout=60)
add("PyINT smoke test", rc == 0, out or err or "pyintApp.py -h")
else:
add("PyINT smoke test", True, "Skipped", skipped=True)
required_checks = [check for check in checks if not check.skipped]
overall_ok = all(check.ok for check in required_checks)
failed_names = [check.name for check in required_checks if not check.ok]
message = "All PyINT checks passed." if overall_ok else f"Failed checks: {', '.join(failed_names)}"
return PyintEnvironmentReport(overall_ok=overall_ok, checks=checks, message=message)
+53 -5
View File
@@ -54,12 +54,55 @@ def get_unpack_config() -> Dict[str, Any]:
minimum=1,
maximum=32,
),
"max_files_per_run": module.parse_int(
env.get("UNPACK_MAX_FILES_PER_RUN"),
default=0,
minimum=0,
),
"max_runtime_minutes": module.parse_int(
env.get("UNPACK_MAX_RUNTIME_MINUTES"),
default=0,
minimum=0,
),
}
async def run_unpack_task(task_id: str):
def _normalize_unpack_run_limits(raw_config: Optional[Dict[str, Any]]) -> Dict[str, int]:
if not isinstance(raw_config, dict):
return {}
module = _load_unpack_module()
normalized: Dict[str, int] = {}
if raw_config.get("max_files_per_run") is not None:
normalized["max_files_per_run"] = module.parse_int(
raw_config.get("max_files_per_run"),
default=0,
minimum=0,
)
if raw_config.get("max_runtime_minutes") is not None:
normalized["max_runtime_minutes"] = module.parse_int(
raw_config.get("max_runtime_minutes"),
default=0,
minimum=0,
)
return normalized
def build_unpack_run_config(overrides: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
config = get_unpack_config()
config.update(_normalize_unpack_run_limits(overrides))
return config
async def run_unpack_task(task_id: str, task_config: Optional[Dict[str, Any]] = None):
module = _load_unpack_module()
loop = asyncio.get_running_loop()
config_overrides = _normalize_unpack_run_limits(task_config)
if not config_overrides:
task_record = await task_service.get_task(task_id)
config_overrides = _normalize_unpack_run_limits(getattr(task_record, "params", None))
def _submit(coro):
try:
@@ -88,14 +131,19 @@ async def run_unpack_task(task_id: str):
module.run_unpack_job,
log_callback=log_cb,
progress_callback=progress_cb,
config_overrides=config_overrides or None,
)
if not result:
result = {"processed": 0, "failed": 0, "skipped": 0, "total": 0}
result = {"processed": 0, "failed": 0, "skipped": 0, "total": 0, "remaining": 0, "message": "completed"}
summary = (
"Unpack complete: processed {processed}, failed {failed}, skipped {skipped}"
).format(**result)
summary = "Unpack complete: processed {processed}, failed {failed}, skipped {skipped}".format(**result)
remaining = int(result.get("remaining") or 0)
if remaining > 0:
summary = f"{summary}, remaining {remaining}"
message_text = str(result.get("message") or "").strip()
if message_text and message_text != "completed":
summary = f"{summary} ({message_text})"
await task_service.update_task(task_id, status="COMPLETED", progress=100, message=summary)
except Exception as exc:
await task_service.update_task(task_id, status="FAILED", message=f"Unpack failed: {exc}")