Files
pbf/tools/provider-pipeline/run_provider_pipeline.py

416 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""从 Provider manifest 运行正式 PBF Pipeline,终点为 NavSea Provider Package。"""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import shutil
import subprocess
import sys
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools" / "provider-policy-compiler"))
from provider_policy import build_package, build_policy, create_delivery_zip, fingerprint, validate_package, validate_policy, export_package
def read_resource(location: str, timeout: int = 60) -> bytes:
if location.startswith(("http://", "https://")):
request = urllib.request.Request(location, headers={"User-Agent": "PBF-Provider-Pipeline/1.0"})
with urllib.request.urlopen(request, timeout=timeout) as response:
return response.read()
return Path(location).expanduser().read_bytes()
def resolved(base: str, relative: str) -> str:
if relative.startswith(("http://", "https://")):
return relative
if base.startswith(("http://", "https://")):
return urllib.parse.urljoin(base, relative)
return str((Path(base).expanduser().parent / relative).resolve())
def sha(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def snapshot(location: str, target: Path) -> dict[str, Any]:
data = read_resource(location)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
return {"uri": location, "snapshot": str(target), "sha256": sha(data), "bytes": len(data), "available": True}
def source_tile_template(style: dict[str, Any], configured: str | None, source_id: str | None) -> str:
if configured:
return configured
sources = style.get("sources", {})
ordered = [sources[source_id]] if source_id in sources else list(sources.values())
for source in ordered:
if source.get("type") == "vector" and source.get("tiles"):
return source["tiles"][0]
raise ValueError("Style 中没有可用的 vector tile template,且 manifest 未提供 tileSource.template")
def sample_one(template: str, samples: list[str], destination: Path) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
successes: list[dict[str, Any]] = []
failures: list[dict[str, Any]] = []
for sample in samples:
tile = sample.strip().strip("/")
url = template.replace("{z}", tile.split("/")[0]).replace("{x}", tile.split("/")[1]).replace("{y}", tile.split("/")[2])
target = destination / (tile + ".pbf")
try:
item = snapshot(url, target)
item.update({"tile": tile})
if not item["bytes"]:
raise ValueError("空 PBF 响应")
successes.append(item)
except Exception as exc: # each configured candidate is independently auditable
failures.append({"tile": tile, "uri": url, "error": f"{type(exc).__name__}: {exc}"})
return successes, failures
def standard_assignment(actual: dict[str, Any], standard: dict[str, Any]) -> list[dict[str, Any]]:
by_visual: dict[tuple[str, str], set[str]] = {}
ref_by_visual: dict[tuple[str, str], set[str]] = {}
for obj in standard.get("visual_objects", []):
layer = obj.get("standard_layer_name")
vhash = obj.get("visual_hash")
role = obj.get("component_role")
if layer and vhash and role:
by_visual.setdefault((role, vhash), set()).add(layer)
ref_by_visual.setdefault((role, vhash), set()).add(obj.get("visual_object_id"))
rows = []
for obj in actual.get("visual_objects", []):
candidates: set[str] = set()
refs: set[str] = set()
for component in obj.get("components", []):
role = {"icon": "icon", "fill": "fill-pattern", "line": "line-pattern"}.get(component.get("role"), component.get("role"))
vhash = component.get("resource_visual_hash")
if role and vhash:
candidates.update(by_visual.get((role, vhash), set()))
refs.update(ref_by_visual.get((role, vhash), set()))
exact_ref = next(iter(refs)) if len(candidates) == 1 and len(refs) == 1 else None
rows.append(_assignment(obj, sorted(candidates), "FROZEN_STANDARD_VISUAL_HASH", exact_ref))
return rows
def _assignment(obj: dict[str, Any], candidates: list[str], basis: str, standard_visual_object_ref: str | None = None) -> dict[str, Any]:
if len(candidates) == 1:
status, layer = ("VISUAL_OBJECT_MATCH", candidates[0]) if standard_visual_object_ref else ("LAYER_ONLY", candidates[0])
elif len(candidates) > 1:
status, layer = "AMBIGUOUS", None
else:
status, layer = "UNKNOWN", None
return {"visualObjectId": obj["visual_object_id"], "occurrenceIds": [], "status": status, "standardLayer": layer, "standardVisualObjectRef": standard_visual_object_ref, "candidateStandardLayers": candidates if status == "AMBIGUOUS" else [], "basis": basis, "provenance": obj.get("provenance", {})}
def manual_assignment(actual: dict[str, Any], assignment_doc: dict[str, Any]) -> list[dict[str, Any]]:
resource_lookup = {}
for row in assignment_doc.get("resource_summary", []):
resource_lookup[row.get("cm95_resource_key")] = row
object_by_id = {x["visual_object_id"]: x for x in actual.get("visual_objects", [])}
components_by_vo: dict[str, set[str]] = {}
for obj in actual.get("visual_objects", []):
components_by_vo[obj["visual_object_id"]] = {c.get("resource_key") for c in obj.get("components", []) if c.get("resource_key")}
rows = []
for vo_id, obj in object_by_id.items():
evidence = [resource_lookup[key] for key in components_by_vo.get(vo_id, set()) if key in resource_lookup]
layer_candidates = sorted({layer for e in evidence for layer in ([e.get("assigned_standard_layer")] + (e.get("candidate_standard_layers") or [])) if layer})
statuses = {e.get("assignment_status") for e in evidence}
if not evidence or not layer_candidates:
status, layer, candidates = "UNKNOWN", None, []
elif "AMBIGUOUS" in statuses or len(layer_candidates) > 1:
status, layer, candidates = "AMBIGUOUS", None, layer_candidates
elif statuses == {"COMPONENT"}:
status, layer, candidates = "COMPONENT_ONLY", None, []
else:
status, layer, candidates = "LAYER_ONLY", layer_candidates[0], []
exact_resources = {x.get("reviewed_standard_resource_key") for x in evidence if x.get("assignment_status") == "MATCH" and x.get("reviewed_standard_resource_key")}
exact_ref = next(iter(exact_resources)) if len(evidence) == 1 and len(exact_resources) == 1 else None
if status == "LAYER_ONLY" and exact_ref:
status = "VISUAL_OBJECT_MATCH"
rows.append({"visualObjectId": vo_id, "occurrenceIds": [], "status": status, "standardLayer": layer, "standardVisualObjectRef": exact_ref, "candidateStandardLayers": candidates, "basis": "HUMAN_REVIEWED_ASSIGNMENT_RESOURCE_KEY", "manualStatuses": sorted(x for x in statuses if x), "provenance": obj.get("provenance", {})})
return rows
def load_execution_catalog(manifest_path: Path, relative: str, style_hash: str) -> tuple[dict[str, Any], dict[str, Any]]:
catalog_path = (manifest_path.parent / relative).resolve()
catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
if catalog.get("schema") != "actual-visual-object-catalog/v1" or not isinstance(catalog.get("visual_objects"), list):
raise ValueError("executionEvidenceCatalog schema 无效")
style_path = Path(catalog.get("inputs", {}).get("style", ""))
if not style_path.is_file():
raise ValueError("executionEvidenceCatalog Style 输入不存在")
catalog_style_hash = sha(style_path.read_bytes())
if catalog_style_hash != style_hash:
raise ValueError("executionEvidenceCatalog Style SHA-256 与 intake Style 不一致")
summary = catalog.get("summary", {})
source = {
"schema": catalog["schema"], "catalog": relative,
"styleSha256": catalog_style_hash,
"tileCount": catalog.get("inputs", {}).get("tile_count"),
"matchedOccurrenceCount": summary.get("matched_feature_occurrence_count"),
"distinctVisualObjectCount": summary.get("distinct_visual_object_count"),
}
return catalog, source
def select_mapping_catalog(
sample_catalog: dict[str, Any], execution_catalog: dict[str, Any] | None
) -> dict[str, Any]:
"""Use the declared, Style-verified execution catalog for complete mapping.
The sample catalog remains the sole source of sample occurrenceIds. If the
full catalog does not contain every sampled Visual Object, fail closed.
"""
if execution_catalog is None:
return sample_catalog
full_objects = execution_catalog.get("visual_objects")
sample_objects = sample_catalog.get("visual_objects")
if not isinstance(full_objects, list) or not isinstance(sample_objects, list):
raise ValueError("映射 Catalog 缺少 visual_objects")
full_ids = [item.get("visual_object_id") for item in full_objects if isinstance(item, dict)]
sample_ids = [item.get("visual_object_id") for item in sample_objects if isinstance(item, dict)]
if (len(full_ids) != len(full_objects) or
len(sample_ids) != len(sample_objects) or
any(not isinstance(item, str) or not item for item in full_ids + sample_ids) or
len(set(full_ids)) != len(full_ids) or
len(set(sample_ids)) != len(sample_ids)):
raise ValueError("映射 Catalog Visual Object ID 无效或重复")
missing = set(sample_ids) - set(full_ids)
if missing:
raise ValueError(f"全量映射 Catalog 未覆盖样本 Visual Object:{len(missing)} 条")
return execution_catalog
def attach_render_evidence(assignments: list[dict[str, Any]], catalog: dict[str, Any]) -> list[str]:
objects = {obj["visual_object_id"]: obj for obj in catalog["visual_objects"]}
missing = []
for row in assignments:
obj = objects.get(row["visualObjectId"])
if obj is None:
row["renderEvidence"] = None
missing.append(row["visualObjectId"])
continue
row["renderEvidence"] = {
"sourceLayer": obj.get("provenance", {}).get("source_layer"),
"geometryRole": obj.get("geometry_role"),
"occurrenceCount": obj.get("occurrence_count"),
"components": [{
"styleLayerId": component.get("style_layer_id"),
"styleIndex": component.get("style_index"),
"role": component.get("role"),
"resourceKey": component.get("resource_key"),
"resourceVisualHash": component.get("resource_visual_hash"),
} for component in obj.get("components", [])],
}
return missing
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--manifest", required=True, help="Provider manifest JSON")
ap.add_argument("--work-dir", help="Defaults to .tmp/provider-pipeline/<provider>/<dataset>")
ap.add_argument("--output-dir", help="Defaults to dist/<provider>/<dataset>/<version>")
ap.add_argument("--sample-tile", action="append", help="XYZ tile z/x/y; repeat to supply candidates")
ap.add_argument("--no-export", action="store_true", help="Run validation without exporting a package")
args = ap.parse_args()
manifest_path = Path(args.manifest).resolve()
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
provider = manifest["provider"]
dataset = manifest["dataset"]
package = manifest["package"]
provider_id, dataset_id, version = provider["providerId"], dataset["datasetId"], package["version"]
work = Path(args.work_dir).resolve() if args.work_dir else ROOT / ".tmp" / "provider-pipeline" / provider_id / dataset_id
export_dir = Path(args.output_dir).resolve() if args.output_dir else ROOT / "dist" / provider_id / dataset_id / version
record: dict[str, Any] = {"manifest": str(manifest_path), "stages": {}, "provider": provider, "dataset": dataset, "package": package}
intake = work / "intake"
intake.mkdir(parents=True, exist_ok=True)
style_spec = manifest["resources"]["style"]
style_uri = style_spec["uri"]
style_info = snapshot(style_uri, intake / "style.snapshot.json")
style = json.loads((intake / "style.snapshot.json").read_text(encoding="utf-8"))
template = source_tile_template(style, manifest.get("tileSource", {}).get("template"), manifest.get("tileSource", {}).get("sourceId"))
tile_source = {"uriTemplate": template, "sourceId": manifest.get("tileSource", {}).get("sourceId"), "protocol": "XYZ"}
sprite_base = manifest.get("resources", {}).get("sprite", {}).get("uri") or style.get("sprite")
if not sprite_base:
sprite_info = {"available": False, "uri": None, "reason": "Style 未声明 Sprite"}
else:
sprite_base = resolved(style_uri, sprite_base) if not sprite_base.startswith(("http://", "https://")) else sprite_base
sprite_dir = intake / "sprite"
sprite_dir.mkdir(parents=True, exist_ok=True)
sprite_info = snapshot(sprite_base + ".json", sprite_dir / "sprite.json")
png_info = snapshot(sprite_base + ".png", sprite_dir / "sprite.png")
sprite_info["png"] = png_info
glyph_template = manifest.get("resources", {}).get("glyphs", {}).get("uri") or style.get("glyphs")
glyph_info = {"uriTemplate": glyph_template, "available": bool(glyph_template), "snapshot": None}
if not glyph_template:
glyph_info["reason"] = "Style 未声明 Glyph 资源"
if glyph_template and manifest.get("probeGlyph", False):
probe = glyph_template.replace("{fontstack}", urllib.parse.quote(manifest.get("glyphProbe", {}).get("fontstack", "Noto Sans Regular"), safe="")).replace("{range}", manifest.get("glyphProbe", {}).get("range", "0-255"))
try:
glyph_info.update(snapshot(probe, intake / "glyphs" / "probe.pbf"))
except Exception as exc:
glyph_info.update({"available": False, "error": str(exc)})
samples = args.sample_tile or manifest.get("sampleTiles", []) or manifest.get("sampleTileCandidates", [])
if not samples:
raise ValueError("manifest 未提供 sampleTiles/sampleTileCandidates;请用 --sample-tile 明确指定")
successful_tiles, failed_tiles = sample_one(template, samples, intake / "samples")
source_manifest = {
"schemaVersion": "pbf-source-manifest/v1", "provider": provider, "dataset": dataset, "package": package,
"resources": {"style": style_info, "tileSource": tile_source, "sprite": sprite_info, "glyphs": glyph_info},
"bounds": manifest.get("bounds"), "zoom": manifest.get("zoom", {}),
"sampleTiles": successful_tiles,
"failedSampleTiles": failed_tiles, "semanticSources": manifest.get("semanticSources", []),
}
source_manifest["sourceFingerprint"] = fingerprint({k: v for k, v in source_manifest.items() if k != "sourceFingerprint"})
(intake / "source-manifest.json").write_text(json.dumps(source_manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
record["stages"]["PBF-01"] = {"status": "PASS" if successful_tiles and style_info["available"] and sprite_info.get("available") else "PASS_WITH_WARNINGS", "sourceFingerprint": source_manifest["sourceFingerprint"], "samples": successful_tiles, "failedSamples": failed_tiles}
if not successful_tiles:
raise RuntimeError("PBF-01 未取得任何真实 PBF tile,停止后续阶段")
vo_out = work / "pbf-02-style-visual-object"
if not (vo_out / "anonymous-catalog.json").exists():
spec = importlib.util.spec_from_file_location("style_vo_tool", ROOT / "tools/style-visual-object/style_vo_tool.py")
module = importlib.util.module_from_spec(spec); assert spec and spec.loader; spec.loader.exec_module(module)
catalog = module.extract(str(intake / "style.snapshot.json"), str(intake / "sprite" / "sprite") if sprite_info.get("available") else None, vo_out, f"{provider_id}-anonymous")
else:
catalog = json.loads((vo_out / "anonymous-catalog.json").read_text(encoding="utf-8"))
record["stages"]["PBF-02"] = {"status": "PASS", "visualObjects": catalog["summary"]["visual_object_count"], "unresolved": catalog["summary"]["unresolved_count"]}
if not (work / "pbf-03-actual" / "actual-anonymous-catalog.json").exists():
instance_output = work / "pbf-03-actual"
for item in successful_tiles:
z, x, y = item["tile"].split("/")
dest = instance_output / "tiles" / z / x / f"{y}.pbf"
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item["snapshot"], dest)
command = [sys.executable, str(ROOT / "tools/visual-object-instantiation/instantiate_pbf_visual_objects.py"), "--style", str(intake / "style.snapshot.json"), "--sprite-json", str(intake / "sprite/sprite.json"), "--sprite-png", str(intake / "sprite/sprite.png"), "--pbf-root", str(instance_output / "tiles"), "--output", str(instance_output), "--provider-id", provider_id, "--dataset-id", dataset_id, "--provider-version", str(provider.get("version", "unknown"))]
if not sprite_info.get("available"):
raise RuntimeError("PBF-03 当前要求 Sprite JSON/PNG 可用")
completed = subprocess.run(command, cwd=ROOT, text=True, capture_output=True)
(instance_output / "instantiation.log").write_text(completed.stdout + completed.stderr, encoding="utf-8")
if completed.returncode:
raise RuntimeError(f"PBF-03 实例化失败,详见 {instance_output / 'instantiation.log'}")
actual = json.loads((work / "pbf-03-actual" / "actual-anonymous-catalog.json").read_text(encoding="utf-8"))
for row in actual.get("visual_objects", []):
matching_occurrences = [x for x in actual.get("occurrences", []) if x["visual_object_id"] == row["visual_object_id"]]
row["occurrence_ids"] = [x["occurrence_id"] for x in matching_occurrences]
record["stages"]["PBF-03"] = {"status": "PASS", **actual["summary"], "occurrenceCount": len(actual.get("occurrences", []))}
evidence_path = manifest.get("mapping", {}).get("executionEvidenceCatalog")
execution_catalog = None
evidence_source = None
if evidence_path:
execution_catalog, evidence_source = load_execution_catalog(
manifest_path, evidence_path, style_info["sha256"]
)
# A declared full execution catalog is the mapping universe, not merely
# a lookup sidecar for the sampled subset of Visual Objects.
mapping_actual = select_mapping_catalog(actual, execution_catalog)
mapping_mode = manifest.get("mapping", {}).get("mode", "frozen-standard-visual")
if mapping_mode == "manual-assignment":
assignment_path = (manifest_path.parent / manifest["mapping"]["assignmentFile"]).resolve()
assignment_doc = json.loads(assignment_path.read_text(encoding="utf-8"))
assignments = manual_assignment(mapping_actual, assignment_doc)
mapping_source = str(assignment_path)
else:
standard_path = (manifest_path.parent / manifest.get("mapping", {}).get("standardCatalog", "../../tools/style-visual-object/output/navsea-standard/standard-catalog.json")).resolve()
if not standard_path.exists():
standard_path = ROOT / "tools/style-visual-object/output/navsea-standard/standard-catalog.json"
standard = json.loads(standard_path.read_text(encoding="utf-8"))
assignments = standard_assignment(mapping_actual, standard)
mapping_source = str(standard_path)
occurrence_lookup: dict[str, list[str]] = {}
for occ in actual.get("occurrences", []):
occurrence_lookup.setdefault(occ["visual_object_id"], []).append(occ["occurrence_id"])
for row in assignments:
row["occurrenceIds"] = occurrence_lookup.get(row["visualObjectId"], [])
evidence_missing: list[str] = []
if execution_catalog is not None:
evidence_missing = attach_render_evidence(assignments, execution_catalog)
mapping_doc = {"schemaVersion": "navsea-standard-layer-mapping/v2" if evidence_path else "navsea-standard-layer-mapping/v1", "source": mapping_source, "assignments": assignments}
if evidence_source:
mapping_doc["renderEvidenceSource"] = evidence_source
mapping_doc["diagnostics"] = [{"code": "renderEvidence:catalog_missing_visual_object", "visualObjectId": x} for x in evidence_missing]
mapping_path = work / "pbf-04-mapping" / "provider-layer-assignment.json"
mapping_path.parent.mkdir(parents=True, exist_ok=True)
mapping_path.write_text(json.dumps(mapping_doc, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
unresolved = [{"visualObjectId": x["visualObjectId"], "status": x["status"], "candidateStandardLayers": x["candidateStandardLayers"]} for x in assignments if x["status"] in {"AMBIGUOUS", "UNKNOWN", "COMPONENT_ONLY"}]
unresolved += [{"visualObjectId": x, "status": "MISSING_RENDER_EVIDENCE"} for x in evidence_missing]
record["stages"]["PBF-04"] = {"status": "PASS_WITH_WARNINGS" if unresolved else "PASS", "mappingMode": mapping_mode, "assignments": len(assignments), "unresolved": len(unresolved), "missingRenderEvidence": evidence_missing, "source": mapping_source,
"catalogScope": "DECLARED_EXECUTION_CATALOG" if execution_catalog is not None else "SAMPLED_PBF",
"sampleVisualObjects": len(actual.get("visual_objects", [])),
"mappingVisualObjects": len(mapping_actual.get("visual_objects", []))}
fingerprints = {
"source": source_manifest["sourceFingerprint"], "style": style_info["sha256"],
"sprite": sprite_info.get("sha256"), "mapping": fingerprint(mapping_doc),
}
if mapping_mode != "manual-assignment":
fingerprints["standardCatalog"] = fingerprint(standard)
data = {
"provider": provider, "dataset": dataset, "package": package,
"standardCatalog": {"id": "navsea-frozen-standard", "schemaVersion": "style-visual-object-standard/v1", "fingerprint": fingerprints.get("standardCatalog")},
"resources": {
"style": {"uri": style_uri, "mediaType": "application/vnd.mapbox.style+json", "sha256": style_info["sha256"], "version": style.get("version")},
"pbfTemplate": {"uriTemplate": template, "protocol": "XYZ"},
"pbfSamples": [{"tile": x["tile"], "uri": x["uri"], "sha256": x["sha256"], "bytes": x["bytes"]} for x in successful_tiles],
"sprite": {"uri": sprite_base, "jsonSha256": sprite_info.get("sha256"), "pngSha256": sprite_info.get("png", {}).get("sha256")},
"glyphs": glyph_info,
},
"mapping": assignments, "mappingSchemaVersion": mapping_doc["schemaVersion"], "renderEvidenceSource": evidence_source, "mappingDiagnostics": mapping_doc.get("diagnostics", []),
"display": manifest.get("policy", {}).get("display", {}), "pickup": manifest.get("policy", {}).get("pickup", {}),
"planning": manifest.get("policy", {}).get("planning", {}), "danger": manifest.get("policy", {}).get("danger", {}),
"capabilities": manifest.get("policy", {}).get("capabilities", {}),
"validationRequirements": {"requireStyle": True, "requirePbfSample": True, "requireSprite": True, "minimumMappingCount": 1},
"unresolved": unresolved, "limitations": manifest.get("limitations", []),
"provenance": {"manifest": str(manifest_path), "mappingSource": mapping_source, "pipelineVersion": "1.1.0", "generatedAt": None if evidence_path else datetime.now(timezone.utc).isoformat()},
"fingerprints": fingerprints,
}
record["stages"]["PBF-05"] = {"status": "PASS", "schemaVersion": "navsea-provider-policy/v1"}
policy = build_policy(data)
policy_validation = validate_policy(policy, style)
if policy_validation["result"] == "FAIL":
raise RuntimeError(f"Provider Policy 校验 FAIL: {policy_validation['errors']}")
(work / "pbf-05-policy.json").write_text(json.dumps(policy, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
record["stages"]["PBF-05"]["validation"] = policy_validation
package_doc = build_package(data, policy, policy_validation)
validation = validate_package(package_doc, style)
package_doc["validation"] = validation
record["stages"]["PBF-06"] = {"status": validation["result"], "schemaVersion": "navsea-provider-package/v1", "validation": validation}
record["validationResult"] = validation["result"]
if not args.no_export:
export_package(export_dir, package_doc, policy, validation)
shutil.copy2(intake / "style.snapshot.json", export_dir / "resources-style.json")
shutil.copy2(intake / "sprite/sprite.json", export_dir / "resources-sprite.json")
shutil.copy2(intake / "sprite/sprite.png", export_dir / "resources-sprite.png")
shutil.copy2(mapping_path, export_dir / "mapping-layer-assignment.json")
record["stages"]["PBF-07"] = {"status": "EXPORTED", "path": str(export_dir)}
delivery_zip = create_delivery_zip(export_dir, provider_id, dataset_id, version)
record["stages"]["PBF-08"] = {"status": "PACKAGED", **delivery_zip}
else:
delivery_zip = None
record["stages"]["PBF-07"] = {"status": "SKIPPED_BY_REQUEST"}
record["stages"]["PBF-08"] = {"status": "SKIPPED_BY_REQUEST"}
report_path = work / "pipeline-report.json"
report_path.write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"providerId": provider_id, "datasetId": dataset_id, "validationResult": validation["result"], "sampleTiles": [x["tile"] for x in successful_tiles], "output": None if args.no_export else str(export_dir), "deliveryZip": None if delivery_zip is None else delivery_zip["path"], "deliveryZipSha256": None if delivery_zip is None else delivery_zip["sha256"], "report": str(report_path)}, ensure_ascii=False, indent=2))
return 0 if validation["result"] != "FAIL" else 2
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as error:
print(f"ERROR: {type(error).__name__}: {error}", file=sys.stderr)
raise SystemExit(2)