Add SARscape SBAS prepared stack workflow
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
"""Extract SARscape SBAS task template metadata from installed .task files.
|
||||
|
||||
This script is intentionally file-based. It does not start ENVI, taskengine, or
|
||||
envipyengine, so it is safe to use on workstations where live
|
||||
task.parameters inspection can hang.
|
||||
|
||||
Examples:
|
||||
python scripts/extract_sarscape_sbas_task_templates.py --json
|
||||
python scripts/extract_sarscape_sbas_task_templates.py --template --output tmp_sarscape_sbas_template.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
|
||||
|
||||
DEFAULT_ENVI_ROOT = Path(os.environ.get("SARSCAPE_ENVI_ROOT", r"C:\Program Files\Harris\ENVI56"))
|
||||
|
||||
NATIVE_WORKFLOW_TASKS = ["wf_sbas", "wf_esbas"]
|
||||
SUPPORT_TASKS = [
|
||||
"SARscape_setting_output_folders",
|
||||
"SARsLoadPreferences",
|
||||
"SARsImportSarSelector",
|
||||
"SARscapeSuggestLooks",
|
||||
"SARscapeEnviuriToShape",
|
||||
]
|
||||
STACK_TASKS = [
|
||||
"SARsInSARStackSBASGenerateConnectionGraph",
|
||||
"SARsInSARStackSBASInterferogramGeneration",
|
||||
"SARsInSARStackSBASInversionStep1",
|
||||
"SARsInSARStackSBASInversionStep2",
|
||||
"SARsInSARStackSBASGeocode",
|
||||
"SARsInSARStackSBASVariogram",
|
||||
]
|
||||
ESBAS_TASKS = [
|
||||
"SARsInSARConnectionGraphESBAS",
|
||||
"SARsInSARStackESBASInterferogramGeneration",
|
||||
"SARsInSARStackESBASInversion",
|
||||
"SARsInSARStackESBASGeocode",
|
||||
]
|
||||
DEFAULT_TASKS = [
|
||||
*NATIVE_WORKFLOW_TASKS,
|
||||
*SUPPORT_TASKS,
|
||||
*STACK_TASKS,
|
||||
*ESBAS_TASKS,
|
||||
]
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Read SARscape SBAS .task files and emit a static parameter report."
|
||||
)
|
||||
parser.add_argument("--envi-root", default=str(DEFAULT_ENVI_ROOT), help="ENVI install root.")
|
||||
parser.add_argument("--task", action="append", default=[], help="Task name to extract. May be repeated.")
|
||||
parser.add_argument("--json", action="store_true", help="Print the extraction report as JSON.")
|
||||
parser.add_argument("--template", action="store_true", help="Print a backend template skeleton.")
|
||||
parser.add_argument("--output", default="", help="Optional output file for JSON/template output.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _candidate_paths(envi_root: Path, task_name: str) -> Iterable[Path]:
|
||||
if task_name in NATIVE_WORKFLOW_TASKS:
|
||||
yield envi_root / "user_custom_code" / f"{task_name}.task"
|
||||
yield envi_root / "resource" / "templates" / "tasks" / "SARscape" / f"{task_name}.task"
|
||||
yield envi_root / "resource" / "templates" / "tasks" / f"{task_name}.task"
|
||||
yield envi_root / "user_custom_code" / f"{task_name}.task"
|
||||
|
||||
|
||||
def _read_task(envi_root: Path, task_name: str) -> Dict[str, Any]:
|
||||
for path in _candidate_paths(envi_root, task_name):
|
||||
if path.is_file():
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except UnicodeDecodeError:
|
||||
payload = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
return {"ok": True, "path": str(path), "payload": payload}
|
||||
return {"ok": False, "path": None, "payload": None, "error": "task file not found"}
|
||||
|
||||
|
||||
def _choice_list(value: Any) -> Optional[Dict[str, Any]]:
|
||||
if isinstance(value, dict):
|
||||
return {str(key): item for key, item in value.items()}
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_parameter(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
parameter_type = str(item.get("parameterType") or "").strip()
|
||||
required = bool(item.get("required")) or parameter_type.lower() == "required"
|
||||
default = item.get("defaultValue", item.get("default", item.get("value")))
|
||||
normalized: Dict[str, Any] = {
|
||||
"name": str(item.get("name") or "").strip(),
|
||||
"keyword": str(item.get("keyword") or item.get("name") or "").strip(),
|
||||
"display_name": str(item.get("displayName") or item.get("display_name") or "").strip(),
|
||||
"data_type": str(item.get("dataType") or item.get("type") or "").strip(),
|
||||
"direction": str(item.get("direction") or "").strip().lower(),
|
||||
"required": required,
|
||||
}
|
||||
if parameter_type:
|
||||
normalized["parameter_type"] = parameter_type
|
||||
if default is not None:
|
||||
normalized["default"] = default
|
||||
choices = _choice_list(item.get("choiceList") or item.get("choice_list"))
|
||||
if choices:
|
||||
normalized["choice_list"] = choices
|
||||
description = str(item.get("description") or "").strip()
|
||||
if description:
|
||||
normalized["description"] = description
|
||||
return normalized
|
||||
|
||||
|
||||
def _dag_summary(payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
parameters = payload.get("parameters") if isinstance(payload.get("parameters"), list) else []
|
||||
dag_param = next((item for item in parameters if item.get("name") == "DAG"), None)
|
||||
dag = dag_param.get("default") if isinstance(dag_param, dict) else None
|
||||
if not isinstance(dag, dict):
|
||||
return []
|
||||
|
||||
summary: List[Dict[str, Any]] = []
|
||||
for node_id, node in dag.items():
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
task_name = node.get("name")
|
||||
if isinstance(task_name, dict):
|
||||
task_name = task_name.get("base_class") or "<inline_task>"
|
||||
summary.append(
|
||||
{
|
||||
"node_id": str(node_id),
|
||||
"task_name": str(task_name or ""),
|
||||
"external_input": node.get("external_input") or {},
|
||||
"internal_input": node.get("internal_input") or {},
|
||||
"static_input": node.get("static_input") or {},
|
||||
"output": node.get("output") or {},
|
||||
}
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
def build_report(envi_root: Path, task_names: List[str]) -> Dict[str, Any]:
|
||||
tasks: List[Dict[str, Any]] = []
|
||||
missing: List[str] = []
|
||||
for task_name in task_names:
|
||||
raw = _read_task(envi_root, task_name)
|
||||
if not raw["ok"]:
|
||||
missing.append(task_name)
|
||||
tasks.append({"name": task_name, "available": False, "error": raw.get("error")})
|
||||
continue
|
||||
payload = raw["payload"] if isinstance(raw["payload"], dict) else {}
|
||||
parameters = payload.get("parameters") if isinstance(payload.get("parameters"), list) else []
|
||||
normalized_params = [
|
||||
_normalize_parameter(item)
|
||||
for item in parameters
|
||||
if isinstance(item, dict) and str(item.get("name") or "").strip()
|
||||
]
|
||||
tasks.append(
|
||||
{
|
||||
"name": str(payload.get("name") or task_name),
|
||||
"available": True,
|
||||
"path": raw["path"],
|
||||
"version": payload.get("version") or payload.get("revision"),
|
||||
"display_name": payload.get("displayName") or payload.get("display_name"),
|
||||
"base_class": payload.get("baseClass") or payload.get("base_class"),
|
||||
"parameter_count": len(normalized_params),
|
||||
"required_inputs": [
|
||||
item["name"]
|
||||
for item in normalized_params
|
||||
if item.get("required") and item.get("direction") == "input"
|
||||
],
|
||||
"outputs": [
|
||||
item["name"]
|
||||
for item in normalized_params
|
||||
if item.get("direction") == "output"
|
||||
],
|
||||
"parameters": normalized_params,
|
||||
"dag": _dag_summary(payload),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"schema": "insar.sarscape-task-template-extract/v1",
|
||||
"generated_at_utc": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
|
||||
"envi_root": str(envi_root),
|
||||
"task_count": len(tasks),
|
||||
"available_count": sum(1 for item in tasks if item.get("available")),
|
||||
"missing": missing,
|
||||
"tasks": tasks,
|
||||
}
|
||||
|
||||
|
||||
def build_template(report: Dict[str, Any]) -> Dict[str, Any]:
|
||||
by_name = {str(item.get("name") or ""): item for item in report.get("tasks") or []}
|
||||
wf_sbas = by_name.get("wf_sbas") or {}
|
||||
stack_tasks = [by_name.get(name) or {"name": name, "available": False} for name in STACK_TASKS]
|
||||
return {
|
||||
"schema": "insar.sarscape-sbas-template/v1",
|
||||
"template_name": "SARscape SBAS native wf_sbas template skeleton",
|
||||
"sarscape_version_hint": "Extracted from installed ENVI/SARscape .task files",
|
||||
"validated": False,
|
||||
"execution_strategy": "native_workflow_metatask",
|
||||
"source_report_schema": report.get("schema"),
|
||||
"source_envi_root": report.get("envi_root"),
|
||||
"native_workflow": {
|
||||
"phase_id": "native_wf_sbas",
|
||||
"task_name": "wf_sbas",
|
||||
"source_task_file": wf_sbas.get("path"),
|
||||
"parameters": {
|
||||
"INPUT_FILE_LIST": "${scene_input_uris}",
|
||||
"SARSCAPE_PREFERENCE": "Use actual preferences",
|
||||
"DEM_SARSCAPEDATA": "${dem_sarscapedata}",
|
||||
"OUTPUT_FOLDER": "${output_root}",
|
||||
"GEOCODE_RG_GRID_SIZE": 10.0,
|
||||
"ESTIMATE_RESIDUAL_HEIGHT": True,
|
||||
"DISPLACEMENT_MODEL_TYPE": "linear",
|
||||
},
|
||||
"parameter_schema": wf_sbas.get("parameters") or [],
|
||||
"dag": wf_sbas.get("dag") or [],
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"phase_id": str(item.get("name") or ""),
|
||||
"task_name": str(item.get("name") or ""),
|
||||
"enabled": False,
|
||||
"source_task_file": item.get("path"),
|
||||
"required_inputs": item.get("required_inputs") or [],
|
||||
"outputs": item.get("outputs") or [],
|
||||
"parameter_schema": item.get("parameters") or [],
|
||||
"parameters": {},
|
||||
}
|
||||
for item in stack_tasks
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _parse_args()
|
||||
envi_root = Path(args.envi_root)
|
||||
task_names = args.task or DEFAULT_TASKS
|
||||
report = build_report(envi_root, task_names)
|
||||
payload = build_template(report) if args.template else report
|
||||
text = json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
|
||||
if args.output:
|
||||
output_path = Path(args.output)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(text + "\n", encoding="utf-8")
|
||||
if args.json or args.template or not args.output:
|
||||
print(text)
|
||||
return 0 if report.get("available_count") else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,328 @@
|
||||
"""Validate the SARscape SBAS parameter template without executing SBAS.
|
||||
|
||||
The validation scope is intentionally limited to the template contract:
|
||||
|
||||
- load the checked-in SARscape SBAS template
|
||||
- inspect the native wf_sbas task parameters through taskengine
|
||||
- resolve template macros against a selected stack manifest
|
||||
- verify required inputs, parameter names, basic types, and source paths
|
||||
|
||||
It does not call task.execute() and does not run SARscape processing.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate SARscape SBAS template parameters without executing SARscape."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stack-manifest",
|
||||
required=True,
|
||||
help="Path to selected_stack_manifest.json.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--template",
|
||||
default="",
|
||||
help="Optional SARscape SBAS template path. Defaults to configured template.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default="",
|
||||
help="Optional output JSON report path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=120,
|
||||
help="Task inspection timeout in seconds.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-live",
|
||||
action="store_true",
|
||||
help="Skip live wf_sbas parameter inspection.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> Dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as fp:
|
||||
payload = json.load(fp)
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"JSON root must be an object: {path}")
|
||||
return payload
|
||||
|
||||
|
||||
def _path_exists(path_text: str) -> bool:
|
||||
text = str(path_text or "").strip()
|
||||
return bool(text) and os.path.exists(os.path.normpath(text))
|
||||
|
||||
|
||||
def _dem_exists(dem: Dict[str, Any]) -> Dict[str, Any]:
|
||||
url = str(dem.get("url") or "").replace("/", os.sep)
|
||||
aux = [str(item or "").replace("/", os.sep) for item in dem.get("auxiliary_url") or []]
|
||||
return {
|
||||
"url": dem.get("url"),
|
||||
"url_exists": _path_exists(url),
|
||||
"auxiliary_url": dem.get("auxiliary_url") or [],
|
||||
"auxiliary_exists": [_path_exists(item) for item in aux],
|
||||
}
|
||||
|
||||
|
||||
def _scene_path_report(scenes: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
rows = []
|
||||
for index, scene in enumerate(scenes):
|
||||
meta_path = str(scene.get("meta_path") or "").strip()
|
||||
tiff_path = str(scene.get("tiff_path") or "").strip()
|
||||
folder_path = str(scene.get("folder_path") or "").strip()
|
||||
rows.append(
|
||||
{
|
||||
"index": index,
|
||||
"imaging_date": scene.get("imaging_date"),
|
||||
"meta_path": meta_path,
|
||||
"meta_exists": _path_exists(meta_path),
|
||||
"tiff_path": tiff_path,
|
||||
"tiff_exists": _path_exists(tiff_path),
|
||||
"folder_path": folder_path,
|
||||
"folder_exists": _path_exists(folder_path),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"scene_count": len(scenes),
|
||||
"missing_meta_count": sum(1 for item in rows if not item["meta_exists"]),
|
||||
"missing_tiff_count": sum(1 for item in rows if not item["tiff_exists"]),
|
||||
"missing_folder_count": sum(1 for item in rows if not item["folder_exists"]),
|
||||
"scenes": rows,
|
||||
}
|
||||
|
||||
|
||||
def _validate_resolved_parameters(
|
||||
parameters: Dict[str, Any],
|
||||
live_input_names: List[str],
|
||||
live_required_inputs: List[str],
|
||||
live_choice_lists: Dict[str, List[Any]],
|
||||
) -> List[str]:
|
||||
issues: List[str] = []
|
||||
live_input_set = set(live_input_names)
|
||||
for key in parameters:
|
||||
if live_input_set and key not in live_input_set:
|
||||
issues.append(f"Template parameter is not a live wf_sbas input: {key}")
|
||||
|
||||
for key in live_required_inputs:
|
||||
value = parameters.get(key)
|
||||
if value is None or value == "" or value == []:
|
||||
issues.append(f"Required live wf_sbas input is missing or empty: {key}")
|
||||
|
||||
input_files = parameters.get("INPUT_FILE_LIST")
|
||||
if not isinstance(input_files, list) or not input_files:
|
||||
issues.append("INPUT_FILE_LIST must resolve to a non-empty list.")
|
||||
elif any(not isinstance(item, str) or not item.strip() for item in input_files):
|
||||
issues.append("INPUT_FILE_LIST contains an empty or non-string item.")
|
||||
|
||||
output_folder = parameters.get("OUTPUT_FOLDER")
|
||||
if output_folder is not None and not isinstance(output_folder, str):
|
||||
issues.append("OUTPUT_FOLDER must resolve to a string path.")
|
||||
|
||||
dem = parameters.get("DEM_SARSCAPEDATA")
|
||||
if dem is not None:
|
||||
if not isinstance(dem, dict):
|
||||
issues.append("DEM_SARSCAPEDATA must resolve to a SARSCAPEDATA object.")
|
||||
elif dem.get("factory") != "ENVISARscapedata":
|
||||
issues.append("DEM_SARSCAPEDATA.factory must be ENVISARscapedata.")
|
||||
|
||||
if "GEOCODE_RG_GRID_SIZE" in parameters and not isinstance(
|
||||
parameters.get("GEOCODE_RG_GRID_SIZE"),
|
||||
(int, float),
|
||||
):
|
||||
issues.append("GEOCODE_RG_GRID_SIZE must be numeric.")
|
||||
|
||||
if "ESTIMATE_RESIDUAL_HEIGHT" in parameters and not isinstance(
|
||||
parameters.get("ESTIMATE_RESIDUAL_HEIGHT"),
|
||||
bool,
|
||||
):
|
||||
issues.append("ESTIMATE_RESIDUAL_HEIGHT must be boolean.")
|
||||
|
||||
for key, choices in live_choice_lists.items():
|
||||
if key in parameters and choices and parameters[key] not in choices:
|
||||
issues.append(f"{key} is not in live choice list: {parameters[key]}")
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = _repo_root()
|
||||
if str(repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(repo_root))
|
||||
|
||||
from backend.app.config import ensure_project_env_loaded
|
||||
from backend.app.services import envi_service
|
||||
from backend.app.services.sarscape_sbas_service import (
|
||||
NATIVE_WORKFLOW_TASK,
|
||||
_resolve_template_value,
|
||||
_scene_input_uris,
|
||||
default_parameter_template_path,
|
||||
load_parameter_template,
|
||||
)
|
||||
|
||||
ensure_project_env_loaded()
|
||||
args = _parse_args()
|
||||
|
||||
manifest_path = Path(args.stack_manifest).resolve()
|
||||
template_path = Path(args.template).resolve() if args.template else Path(default_parameter_template_path()).resolve()
|
||||
stack_manifest = _read_json(manifest_path)
|
||||
template_status = load_parameter_template(str(template_path))
|
||||
template = template_status.get("template") if isinstance(template_status.get("template"), dict) else {}
|
||||
native_workflow = template.get("native_workflow") if isinstance(template.get("native_workflow"), dict) else {}
|
||||
task_name = str(native_workflow.get("task_name") or NATIVE_WORKFLOW_TASK).strip()
|
||||
|
||||
live_report: Dict[str, Any] = {"skipped": True}
|
||||
live_task: Dict[str, Any] = {}
|
||||
if not args.skip_live:
|
||||
live_report = envi_service.inspect_sarscape_sbas_tasks_subprocess(
|
||||
[task_name],
|
||||
include_parameters=True,
|
||||
timeout_seconds=max(10, int(args.timeout or 120)),
|
||||
)
|
||||
live_task = next(
|
||||
(
|
||||
item
|
||||
for item in live_report.get("tasks") or []
|
||||
if str(item.get("name") or "") == task_name
|
||||
),
|
||||
{},
|
||||
)
|
||||
|
||||
scenes = stack_manifest.get("scenes") if isinstance(stack_manifest.get("scenes"), list) else []
|
||||
work_root = Path(stack_manifest.get("proposed_scratch_windows") or manifest_path.parents[1]).resolve()
|
||||
output_root = work_root / "sarscape_sbas_template_validation_output"
|
||||
network_edges_path = output_root / "selected_network_edges.json"
|
||||
context = {
|
||||
"${work_root}": str(work_root),
|
||||
"${output_root}": str(output_root),
|
||||
"${selected_stack_manifest}": str(manifest_path),
|
||||
"${selected_network_edges}": str(network_edges_path),
|
||||
"${scene_meta_paths}": [
|
||||
str(item.get("meta_path"))
|
||||
for item in scenes
|
||||
if str(item.get("meta_path") or "").strip()
|
||||
],
|
||||
"${scene_input_uris}": _scene_input_uris(scenes),
|
||||
"${scene_folder_paths}": [
|
||||
str(item.get("folder_path"))
|
||||
for item in scenes
|
||||
if str(item.get("folder_path") or "").strip()
|
||||
],
|
||||
"${selection_params}": stack_manifest.get("selection_params") or {},
|
||||
"${dem_sarscapedata}": envi_service._build_sarscapedata(envi_service.DEM_BASE_FILE), # noqa: SLF001
|
||||
}
|
||||
resolved_parameters = _resolve_template_value(native_workflow.get("parameters") or {}, context)
|
||||
|
||||
live_parameters = live_task.get("parameters") if isinstance(live_task.get("parameters"), list) else []
|
||||
live_choice_lists = {
|
||||
str(item.get("name")): list(item.get("choice_list") or [])
|
||||
for item in live_parameters
|
||||
if isinstance(item, dict) and isinstance(item.get("choice_list"), list)
|
||||
}
|
||||
issues: List[str] = []
|
||||
execution_gate_issues: List[str] = []
|
||||
for item in template_status.get("errors") or []:
|
||||
text = str(item)
|
||||
if text == "Template is not marked validated=true.":
|
||||
execution_gate_issues.append(text)
|
||||
else:
|
||||
issues.append(text)
|
||||
if not bool(template_status.get("readable")):
|
||||
issues.append("Template is not readable.")
|
||||
if not args.skip_live:
|
||||
if not bool(live_report.get("ok")):
|
||||
issues.append("Live wf_sbas parameter inspection failed.")
|
||||
if not bool(live_task.get("available")):
|
||||
issues.append("Live wf_sbas task is not available to taskengine.")
|
||||
issues.extend(
|
||||
_validate_resolved_parameters(
|
||||
resolved_parameters,
|
||||
list(live_task.get("input_names") or []),
|
||||
list(live_task.get("required_input_names") or []),
|
||||
live_choice_lists,
|
||||
)
|
||||
)
|
||||
|
||||
scene_report = _scene_path_report(scenes)
|
||||
if scene_report["missing_meta_count"]:
|
||||
issues.append("One or more scene meta_path files are missing.")
|
||||
dem_report = _dem_exists(resolved_parameters.get("DEM_SARSCAPEDATA") or {})
|
||||
if not dem_report["url_exists"] and not all(dem_report["auxiliary_exists"]):
|
||||
issues.append("DEM SARSCAPEDATA path or auxiliary files are missing.")
|
||||
|
||||
report = {
|
||||
"schema": "insar.sarscape-sbas-template-validation/v1",
|
||||
"created_at_utc": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
|
||||
"ok": not issues,
|
||||
"validation_scope": "template_contract_only_no_task_execute",
|
||||
"issues": issues,
|
||||
"execution_gate_issues": execution_gate_issues,
|
||||
"template": {
|
||||
"path": str(template_path),
|
||||
"validated_flag": bool(template_status.get("validated")),
|
||||
"execution_strategy": template_status.get("execution_strategy"),
|
||||
"native_workflow_task": task_name,
|
||||
"errors": template_status.get("errors") or [],
|
||||
},
|
||||
"live_task": {
|
||||
"skipped": bool(args.skip_live),
|
||||
"ok": bool(live_report.get("ok")) if not args.skip_live else None,
|
||||
"available": bool(live_task.get("available")) if live_task else None,
|
||||
"parameter_count": live_task.get("parameter_count"),
|
||||
"input_names": live_task.get("input_names") or [],
|
||||
"required_input_names": live_task.get("required_input_names") or [],
|
||||
"output_names": live_task.get("output_names") or [],
|
||||
"error": live_task.get("error"),
|
||||
},
|
||||
"environment": {
|
||||
"runner_cwd": envi_service.get_envi_runner_cwd(),
|
||||
"envi_custom_code": envi_service.get_envi_runner_env().get("ENVI_CUSTOM_CODE"),
|
||||
"dem_base_file": envi_service.DEM_BASE_FILE,
|
||||
},
|
||||
"stack_manifest": {
|
||||
"path": str(manifest_path),
|
||||
"scene_count": len(scenes),
|
||||
"network_edge_count": len(stack_manifest.get("network_edges") or []),
|
||||
"reference_date": stack_manifest.get("reference_date"),
|
||||
"processor_code": stack_manifest.get("processor_code"),
|
||||
},
|
||||
"resolved_parameters": {
|
||||
"keys": sorted(resolved_parameters.keys()),
|
||||
"INPUT_FILE_LIST_count": len(resolved_parameters.get("INPUT_FILE_LIST") or []),
|
||||
"OUTPUT_FOLDER": resolved_parameters.get("OUTPUT_FOLDER"),
|
||||
"GEOCODE_RG_GRID_SIZE": resolved_parameters.get("GEOCODE_RG_GRID_SIZE"),
|
||||
"ESTIMATE_RESIDUAL_HEIGHT": resolved_parameters.get("ESTIMATE_RESIDUAL_HEIGHT"),
|
||||
"DISPLACEMENT_MODEL_TYPE": resolved_parameters.get("DISPLACEMENT_MODEL_TYPE"),
|
||||
"DEM_SARSCAPEDATA": dem_report,
|
||||
},
|
||||
"scene_paths": scene_report,
|
||||
}
|
||||
|
||||
if args.output:
|
||||
output_path = Path(args.output).resolve()
|
||||
else:
|
||||
output_path = repo_root / "backend" / "runtime" / "sarscape_sbas_template_validation_latest.json"
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
return 0 if report["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Inspect likely SARscape SBAS/E-SBAS ENVI task names.
|
||||
|
||||
This script does not execute processing tasks. It only asks envipyengine to
|
||||
instantiate task definitions and read their parameters.
|
||||
|
||||
Examples:
|
||||
python scripts/verify_sarscape_sbas_tasks.py
|
||||
python scripts/verify_sarscape_sbas_tasks.py --task SARsInSARStackSBASGenerateConnectionGraph --parameters
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Inspect SARscape SBAS/E-SBAS ENVI task availability."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--task",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Task name to inspect. May be repeated. Defaults to built-in candidates.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Print the full JSON report.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--parameters",
|
||||
action="store_true",
|
||||
help="Also inspect task parameters. This can be slow for some SARscape tasks.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=120,
|
||||
help="Timeout in seconds when --parameters is used.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = _repo_root()
|
||||
if str(repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(repo_root))
|
||||
|
||||
from backend.app.config import ensure_project_env_loaded
|
||||
from backend.app.services.envi_service import (
|
||||
inspect_sarscape_sbas_tasks,
|
||||
inspect_sarscape_sbas_tasks_subprocess,
|
||||
)
|
||||
|
||||
ensure_project_env_loaded()
|
||||
args = _parse_args()
|
||||
if args.parameters:
|
||||
report = inspect_sarscape_sbas_tasks_subprocess(
|
||||
args.task or None,
|
||||
include_parameters=True,
|
||||
timeout_seconds=max(10, int(args.timeout or 120)),
|
||||
)
|
||||
else:
|
||||
report = inspect_sarscape_sbas_tasks(args.task or None)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print("SARscape SBAS/E-SBAS task inspection")
|
||||
print(f"ok: {report.get('ok')}")
|
||||
print(f"candidate_source: {report.get('candidate_source')}")
|
||||
print(f"include_parameters: {report.get('include_parameters')}")
|
||||
print(f"available: {report.get('available_count')} / {report.get('task_count')}")
|
||||
error = str(report.get("error") or "").strip()
|
||||
if error:
|
||||
print(f"error: {error}")
|
||||
print()
|
||||
for item in report.get("tasks") or []:
|
||||
status = "OK" if item.get("available") else "MISS"
|
||||
print(f"[{status}] {item.get('name')}")
|
||||
if item.get("available"):
|
||||
required = item.get("required_input_names") or []
|
||||
outputs = item.get("output_names") or []
|
||||
print(f" required inputs: {required}")
|
||||
print(f" outputs: {outputs}")
|
||||
else:
|
||||
print(f" error: {item.get('error')}")
|
||||
|
||||
return 0 if report.get("ok") else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user