openmapstack 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openmapstack/__init__.py +6 -0
- openmapstack/__main__.py +3 -0
- openmapstack/checks/__init__.py +112 -0
- openmapstack/checks/geodata.py +318 -0
- openmapstack/checks/overrides.py +263 -0
- openmapstack/checks/presentation.py +168 -0
- openmapstack/checks/project.py +261 -0
- openmapstack/checks/provenance.py +133 -0
- openmapstack/checks/qgis.py +832 -0
- openmapstack/checks/rerun.py +340 -0
- openmapstack/checks/spatial.py +65 -0
- openmapstack/checks/validation.py +288 -0
- openmapstack/checks/visual.py +642 -0
- openmapstack/cli.py +431 -0
- openmapstack/expectations.py +284 -0
- openmapstack/integrity.py +137 -0
- openmapstack/project.py +79 -0
- openmapstack/rerun.py +332 -0
- openmapstack/schema.py +39 -0
- openmapstack/schemas/__init__.py +1 -0
- openmapstack/schemas/project-v1.schema.json +264 -0
- openmapstack/validation.py +1019 -0
- openmapstack/verify.py +386 -0
- openmapstack-0.2.0.dist-info/METADATA +268 -0
- openmapstack-0.2.0.dist-info/RECORD +29 -0
- openmapstack-0.2.0.dist-info/WHEEL +5 -0
- openmapstack-0.2.0.dist-info/entry_points.txt +2 -0
- openmapstack-0.2.0.dist-info/licenses/LICENSE +21 -0
- openmapstack-0.2.0.dist-info/top_level.txt +1 -0
openmapstack/cli.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""Console entry point for OpenMapStack project operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .project import ProjectError, get_in, load_project, project_path, step_outputs
|
|
16
|
+
from .validation import ValidationResult, validate_project
|
|
17
|
+
from .verify import VerifyResult, verify_project
|
|
18
|
+
|
|
19
|
+
STATUS_MARKS = {
|
|
20
|
+
"passed": "PASS",
|
|
21
|
+
"warning": "WARN",
|
|
22
|
+
"not_testable": "N/A ",
|
|
23
|
+
"failed": "FAIL",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
28
|
+
parser = argparse.ArgumentParser(
|
|
29
|
+
prog="openmapstack",
|
|
30
|
+
description="Validate, run, and inspect reproducible OpenMapStack projects.",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument("--version", action="version", version=f"openmapstack {__version__}")
|
|
33
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
34
|
+
|
|
35
|
+
validate_parser = subparsers.add_parser("validate", help="validate a project manifest and its artifacts")
|
|
36
|
+
validate_parser.add_argument("project", nargs="?", default="project.yaml", help="project.yaml or its directory")
|
|
37
|
+
validate_parser.add_argument("--preflight", action="store_true", help="skip generated output, report, and run-record checks")
|
|
38
|
+
validate_parser.add_argument("--strict", action="store_true", help="return failure when warnings or not-testable checks exist")
|
|
39
|
+
validate_parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
40
|
+
validate_parser.add_argument("--output", type=Path, help="also write the JSON result to this path")
|
|
41
|
+
validate_parser.add_argument("--verbose", action="store_true", help="show passed checks in text output")
|
|
42
|
+
validate_parser.set_defaults(handler=_cmd_validate)
|
|
43
|
+
|
|
44
|
+
run_parser = subparsers.add_parser("run", help="run the canonical pipeline and validate its artifacts")
|
|
45
|
+
run_parser.add_argument("project", nargs="?", default="project.yaml", help="project.yaml or its directory")
|
|
46
|
+
run_parser.add_argument("--dry-run", action="store_true", help="validate preflight and print the command without executing it")
|
|
47
|
+
run_parser.add_argument("--strict", action="store_true", help="return failure when post-run validation has warnings")
|
|
48
|
+
run_parser.add_argument("--json", action="store_true", help="capture pipeline output and emit machine-readable JSON")
|
|
49
|
+
run_parser.add_argument(
|
|
50
|
+
"--pipeline-arg",
|
|
51
|
+
action="append",
|
|
52
|
+
default=[],
|
|
53
|
+
dest="pipeline_args",
|
|
54
|
+
metavar="ARG",
|
|
55
|
+
help="pass one argument to the pipeline; repeat as needed (use --pipeline-arg=--flag for flags)",
|
|
56
|
+
)
|
|
57
|
+
run_parser.set_defaults(handler=_cmd_run)
|
|
58
|
+
|
|
59
|
+
verify_parser = subparsers.add_parser(
|
|
60
|
+
"verify",
|
|
61
|
+
help="check produced artifacts without requiring a golden answer",
|
|
62
|
+
)
|
|
63
|
+
verify_parser.add_argument("project", nargs="?", default="project.yaml", help="project.yaml or its directory")
|
|
64
|
+
verify_parser.add_argument(
|
|
65
|
+
"--rerun",
|
|
66
|
+
action="store_true",
|
|
67
|
+
help="also rebuild the project from source in a clean workspace and compare",
|
|
68
|
+
)
|
|
69
|
+
verify_parser.add_argument(
|
|
70
|
+
"--rerun-timeout",
|
|
71
|
+
type=float,
|
|
72
|
+
default=1800.0,
|
|
73
|
+
help="seconds allowed for the clean rerun's canonical entrypoint (default: 1800)",
|
|
74
|
+
)
|
|
75
|
+
verify_parser.add_argument("--strict", action="store_true", help="return failure when warnings or not-testable checks exist")
|
|
76
|
+
verify_parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
77
|
+
verify_parser.add_argument("--output", type=Path, help="also write the JSON result to this path")
|
|
78
|
+
verify_parser.add_argument("--verbose", action="store_true", help="show passed checks in text output")
|
|
79
|
+
verify_parser.set_defaults(handler=_cmd_verify)
|
|
80
|
+
|
|
81
|
+
inspect_parser = subparsers.add_parser("inspect", help="summarize a project for audit and review")
|
|
82
|
+
inspect_parser.add_argument("project", nargs="?", default="project.yaml", help="project.yaml or its directory")
|
|
83
|
+
inspect_parser.add_argument("--json", action="store_true", help="emit the full summary as JSON")
|
|
84
|
+
inspect_parser.add_argument("--checks", action="store_true", help="include passed validation checks in text output")
|
|
85
|
+
inspect_parser.set_defaults(handler=_cmd_inspect)
|
|
86
|
+
return parser
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
90
|
+
parser = build_parser()
|
|
91
|
+
args = parser.parse_args(argv)
|
|
92
|
+
try:
|
|
93
|
+
return int(args.handler(args))
|
|
94
|
+
except KeyboardInterrupt:
|
|
95
|
+
print("openmapstack: interrupted", file=sys.stderr)
|
|
96
|
+
return 130
|
|
97
|
+
except BrokenPipeError:
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _cmd_validate(args: argparse.Namespace) -> int:
|
|
102
|
+
result = validate_project(args.project, artifacts=not args.preflight)
|
|
103
|
+
payload = result.to_dict()
|
|
104
|
+
if args.json:
|
|
105
|
+
print(_json(payload))
|
|
106
|
+
else:
|
|
107
|
+
_print_validation(result, verbose=args.verbose)
|
|
108
|
+
if args.output:
|
|
109
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
args.output.write_text(_json(payload) + "\n", encoding="utf-8")
|
|
111
|
+
if not args.json:
|
|
112
|
+
print(f"Wrote {args.output}")
|
|
113
|
+
return 0 if result.ok(strict=args.strict) else 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _cmd_verify(args: argparse.Namespace) -> int:
|
|
117
|
+
try:
|
|
118
|
+
result = verify_project(
|
|
119
|
+
args.project,
|
|
120
|
+
rerun=args.rerun,
|
|
121
|
+
rerun_timeout_s=args.rerun_timeout,
|
|
122
|
+
)
|
|
123
|
+
except ProjectError as exc:
|
|
124
|
+
print(f"openmapstack: {exc}", file=sys.stderr)
|
|
125
|
+
return 2
|
|
126
|
+
payload = result.to_dict()
|
|
127
|
+
if args.json:
|
|
128
|
+
print(_json(payload))
|
|
129
|
+
else:
|
|
130
|
+
_print_verify(result, verbose=args.verbose)
|
|
131
|
+
if args.output:
|
|
132
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
args.output.write_text(_json(payload) + "\n", encoding="utf-8")
|
|
134
|
+
if not args.json:
|
|
135
|
+
print(f"Wrote {args.output}")
|
|
136
|
+
return 0 if result.ok(strict=args.strict) else 1
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _print_verify(result: VerifyResult, *, verbose: bool, stream: Any = None) -> None:
|
|
140
|
+
out = stream or sys.stdout
|
|
141
|
+
for run in result.checks:
|
|
142
|
+
if run.result.status == "passed" and not verbose:
|
|
143
|
+
continue
|
|
144
|
+
mark = STATUS_MARKS.get(run.result.status, run.result.status.upper())
|
|
145
|
+
target = run.args.get("path")
|
|
146
|
+
label = f"{run.name} [{target}]" if target else run.name
|
|
147
|
+
print(f"{mark} {label}: {run.result.detail}", file=out)
|
|
148
|
+
counts = result.counts
|
|
149
|
+
coverage = result.coverage
|
|
150
|
+
print(
|
|
151
|
+
f"{result.status.upper()}: {result.project_file} "
|
|
152
|
+
f"({counts['passed']} passed, {counts['warning']} warnings, "
|
|
153
|
+
f"{counts['not_testable']} not testable, {counts['failed']} failed; "
|
|
154
|
+
f"{coverage['executed']}/{coverage['applicable']} applicable checks executed)",
|
|
155
|
+
file=out,
|
|
156
|
+
)
|
|
157
|
+
if counts["not_testable"]:
|
|
158
|
+
print(
|
|
159
|
+
" NOTE some checks could not run here; install openmapstack[geo] "
|
|
160
|
+
"for geodata checks, QGIS for the .qgz checks",
|
|
161
|
+
file=out,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
166
|
+
preflight = validate_project(args.project, artifacts=False)
|
|
167
|
+
if not preflight.ok():
|
|
168
|
+
if args.json:
|
|
169
|
+
print(_json({"schema": "openmapstack-run-result/v1", "status": "failed", "phase": "preflight", "validation": preflight.to_dict()}))
|
|
170
|
+
else:
|
|
171
|
+
print("Preflight validation failed.", file=sys.stderr)
|
|
172
|
+
_print_validation(preflight, verbose=False, stream=sys.stderr)
|
|
173
|
+
return 1
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
project_file, project = load_project(args.project)
|
|
177
|
+
command = _pipeline_command(project_file, project, args.pipeline_args)
|
|
178
|
+
except ProjectError as exc:
|
|
179
|
+
if args.json:
|
|
180
|
+
print(_json({"schema": "openmapstack-run-result/v1", "status": "failed", "phase": "preflight", "error": str(exc)}))
|
|
181
|
+
else:
|
|
182
|
+
print(f"openmapstack run: {exc}", file=sys.stderr)
|
|
183
|
+
return 2
|
|
184
|
+
|
|
185
|
+
display_command = shlex.join(command)
|
|
186
|
+
if args.dry_run:
|
|
187
|
+
payload = {
|
|
188
|
+
"schema": "openmapstack-run-result/v1",
|
|
189
|
+
"status": preflight.status,
|
|
190
|
+
"phase": "dry_run",
|
|
191
|
+
"project_file": str(project_file),
|
|
192
|
+
"cwd": str(project_file.parent),
|
|
193
|
+
"command": command,
|
|
194
|
+
"validation": preflight.to_dict(),
|
|
195
|
+
}
|
|
196
|
+
if args.json:
|
|
197
|
+
print(_json(payload))
|
|
198
|
+
else:
|
|
199
|
+
print(f"Preflight: {preflight.status}")
|
|
200
|
+
print(f"Would run: {display_command}")
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if not args.json:
|
|
205
|
+
print(f"Preflight: {preflight.status}")
|
|
206
|
+
print(f"Running: {display_command}")
|
|
207
|
+
try:
|
|
208
|
+
completed = subprocess.run(
|
|
209
|
+
command,
|
|
210
|
+
cwd=project_file.parent,
|
|
211
|
+
check=False,
|
|
212
|
+
text=True,
|
|
213
|
+
capture_output=args.json,
|
|
214
|
+
)
|
|
215
|
+
except OSError as exc:
|
|
216
|
+
if args.json:
|
|
217
|
+
print(_json({"schema": "openmapstack-run-result/v1", "status": "failed", "phase": "execute", "command": command, "error": str(exc)}))
|
|
218
|
+
else:
|
|
219
|
+
print(f"openmapstack run: could not start pipeline: {exc}", file=sys.stderr)
|
|
220
|
+
return 2
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
if completed.returncode != 0:
|
|
224
|
+
payload = {
|
|
225
|
+
"schema": "openmapstack-run-result/v1",
|
|
226
|
+
"status": "failed",
|
|
227
|
+
"phase": "execute",
|
|
228
|
+
"project_file": str(project_file),
|
|
229
|
+
"command": command,
|
|
230
|
+
"returncode": completed.returncode,
|
|
231
|
+
}
|
|
232
|
+
if args.json:
|
|
233
|
+
payload["stdout"] = completed.stdout
|
|
234
|
+
payload["stderr"] = completed.stderr
|
|
235
|
+
print(_json(payload))
|
|
236
|
+
else:
|
|
237
|
+
print(f"Pipeline failed with exit code {completed.returncode}.", file=sys.stderr)
|
|
238
|
+
return 1
|
|
239
|
+
|
|
240
|
+
validation = validate_project(project_file, artifacts=True)
|
|
241
|
+
payload = {
|
|
242
|
+
"schema": "openmapstack-run-result/v1",
|
|
243
|
+
"status": validation.status,
|
|
244
|
+
"phase": "complete",
|
|
245
|
+
"project_file": str(project_file),
|
|
246
|
+
"command": command,
|
|
247
|
+
"returncode": completed.returncode,
|
|
248
|
+
"validation": validation.to_dict(),
|
|
249
|
+
}
|
|
250
|
+
if args.json:
|
|
251
|
+
payload["stdout"] = completed.stdout
|
|
252
|
+
payload["stderr"] = completed.stderr
|
|
253
|
+
print(_json(payload))
|
|
254
|
+
else:
|
|
255
|
+
_print_validation(validation, verbose=False)
|
|
256
|
+
return 0 if validation.ok(strict=args.strict) else 1
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _cmd_inspect(args: argparse.Namespace) -> int:
|
|
260
|
+
try:
|
|
261
|
+
project_file, project = load_project(args.project)
|
|
262
|
+
except ProjectError as exc:
|
|
263
|
+
if args.json:
|
|
264
|
+
print(_json({"schema": "openmapstack-inspection/v1", "status": "failed", "error": str(exc)}))
|
|
265
|
+
else:
|
|
266
|
+
print(f"openmapstack inspect: {exc}", file=sys.stderr)
|
|
267
|
+
return 2
|
|
268
|
+
|
|
269
|
+
validation = validate_project(project_file, artifacts=True)
|
|
270
|
+
summary = _inspection(project_file, project, validation)
|
|
271
|
+
if args.json:
|
|
272
|
+
print(_json(summary))
|
|
273
|
+
else:
|
|
274
|
+
_print_inspection(summary, validation, show_checks=args.checks)
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _pipeline_command(project_file: Path, project: dict[str, Any], pipeline_args: Sequence[str]) -> list[str]:
|
|
279
|
+
implementation = get_in(project, "runtime", "implementation")
|
|
280
|
+
if not isinstance(implementation, dict):
|
|
281
|
+
raise ProjectError("runtime.implementation is missing")
|
|
282
|
+
declared_command = implementation.get("command")
|
|
283
|
+
if declared_command is not None:
|
|
284
|
+
if isinstance(declared_command, str):
|
|
285
|
+
command = shlex.split(declared_command)
|
|
286
|
+
elif isinstance(declared_command, list) and all(isinstance(item, str) and item for item in declared_command):
|
|
287
|
+
command = list(declared_command)
|
|
288
|
+
else:
|
|
289
|
+
raise ProjectError("runtime.implementation.command must be a string or list of strings")
|
|
290
|
+
if not command:
|
|
291
|
+
raise ProjectError("runtime.implementation.command is empty")
|
|
292
|
+
else:
|
|
293
|
+
pipeline = implementation.get("pipeline")
|
|
294
|
+
target = project_path(project_file.parent, pipeline)
|
|
295
|
+
if target is None:
|
|
296
|
+
raise ProjectError("runtime.implementation.pipeline must be a safe project-relative path")
|
|
297
|
+
if not target.is_file():
|
|
298
|
+
raise ProjectError(f"pipeline does not exist: {pipeline}")
|
|
299
|
+
relative = str(target.relative_to(project_file.parent))
|
|
300
|
+
if target.suffix.lower() == ".py":
|
|
301
|
+
command = [sys.executable, relative]
|
|
302
|
+
elif target.stat().st_mode & 0o111:
|
|
303
|
+
command = [relative if relative.startswith("./") else f"./{relative}"]
|
|
304
|
+
else:
|
|
305
|
+
raise ProjectError("non-Python pipelines need an executable file or runtime.implementation.command")
|
|
306
|
+
return command + list(pipeline_args)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _inspection(project_file: Path, project: dict[str, Any], validation: ValidationResult) -> dict[str, Any]:
|
|
310
|
+
root = project_file.parent
|
|
311
|
+
metadata = project.get("project") or {}
|
|
312
|
+
sources = project.get("sources") or {}
|
|
313
|
+
overrides = project.get("overrides") or []
|
|
314
|
+
steps = get_in(project, "processing", "steps", default=[]) or []
|
|
315
|
+
outputs = project.get("outputs") or {}
|
|
316
|
+
source_items = []
|
|
317
|
+
for key, source in sources.items() if isinstance(sources, dict) else []:
|
|
318
|
+
source = source if isinstance(source, dict) else {}
|
|
319
|
+
source_items.append(
|
|
320
|
+
{
|
|
321
|
+
"key": key,
|
|
322
|
+
"provider": source.get("provider"),
|
|
323
|
+
"dataset": source.get("dataset"),
|
|
324
|
+
"retrieved_at": get_in(source, "access", "retrieved_at") or get_in(source, "access", "downloaded_at"),
|
|
325
|
+
"version": get_in(source, "version", "identifier") or get_in(source, "version", "published_at"),
|
|
326
|
+
"license": get_in(source, "license", "name"),
|
|
327
|
+
"source_url": source.get("source_url"),
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
override_items = []
|
|
331
|
+
for override in overrides if isinstance(overrides, list) else []:
|
|
332
|
+
if isinstance(override, dict):
|
|
333
|
+
override_items.append(
|
|
334
|
+
{
|
|
335
|
+
"id": override.get("id"),
|
|
336
|
+
"action": override.get("action"),
|
|
337
|
+
"created_by": override.get("created_by"),
|
|
338
|
+
"geometry_file": get_in(override, "geometry_file", "path"),
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
step_items = []
|
|
342
|
+
for index, step in enumerate(steps if isinstance(steps, list) else []):
|
|
343
|
+
if isinstance(step, dict):
|
|
344
|
+
step_items.append(
|
|
345
|
+
{
|
|
346
|
+
"order": index + 1,
|
|
347
|
+
"id": step.get("id"),
|
|
348
|
+
"operation": step.get("operation"),
|
|
349
|
+
"outputs": step_outputs(step),
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
output_items = []
|
|
353
|
+
for key, output in outputs.items() if isinstance(outputs, dict) else []:
|
|
354
|
+
output = output if isinstance(output, dict) else {}
|
|
355
|
+
target = project_path(root, output.get("path"))
|
|
356
|
+
output_items.append(
|
|
357
|
+
{
|
|
358
|
+
"key": key,
|
|
359
|
+
"path": output.get("path"),
|
|
360
|
+
"format": output.get("format"),
|
|
361
|
+
"generated_by": output.get("generated_by"),
|
|
362
|
+
"exists": bool(target and target.exists()),
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
warnings = project.get("warnings") if isinstance(project.get("warnings"), list) else []
|
|
366
|
+
return {
|
|
367
|
+
"schema": "openmapstack-inspection/v1",
|
|
368
|
+
"project_file": str(project_file),
|
|
369
|
+
"project": {
|
|
370
|
+
"schema": project.get("schema"),
|
|
371
|
+
"id": metadata.get("id"),
|
|
372
|
+
"title": metadata.get("title"),
|
|
373
|
+
"question": metadata.get("question"),
|
|
374
|
+
"status": metadata.get("status"),
|
|
375
|
+
"updated_at": metadata.get("updated_at"),
|
|
376
|
+
},
|
|
377
|
+
"crs": {
|
|
378
|
+
"analysis": get_in(project, "processing", "analysis_crs"),
|
|
379
|
+
"storage": get_in(project, "processing", "storage_crs"),
|
|
380
|
+
},
|
|
381
|
+
"sources": source_items,
|
|
382
|
+
"overrides": override_items,
|
|
383
|
+
"steps": step_items,
|
|
384
|
+
"outputs": output_items,
|
|
385
|
+
"warnings": warnings,
|
|
386
|
+
"latest_run": get_in(project, "runs", "latest"),
|
|
387
|
+
"validation": validation.to_dict(),
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _print_validation(result: ValidationResult, *, verbose: bool, stream: Any = None) -> None:
|
|
392
|
+
stream = stream or sys.stdout
|
|
393
|
+
for check in result.checks:
|
|
394
|
+
if not verbose and check.status == "passed":
|
|
395
|
+
continue
|
|
396
|
+
location = f" [{check.path}]" if check.path else ""
|
|
397
|
+
print(f"{STATUS_MARKS.get(check.status, check.status.upper()):4s} {check.id}{location}: {check.message}", file=stream)
|
|
398
|
+
counts = result.counts
|
|
399
|
+
print(
|
|
400
|
+
f"{result.status.upper()}: {result.project_file} "
|
|
401
|
+
f"({counts['passed']} passed, {counts['warning']} warnings, "
|
|
402
|
+
f"{counts['not_testable']} not testable, {counts['failed']} failed)",
|
|
403
|
+
file=stream,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _print_inspection(summary: dict[str, Any], validation: ValidationResult, *, show_checks: bool) -> None:
|
|
408
|
+
project = summary["project"]
|
|
409
|
+
print(f"{project.get('title') or '(untitled project)'}")
|
|
410
|
+
print(f" id: {project.get('id')} schema: {project.get('schema')} declared status: {project.get('status')}")
|
|
411
|
+
print(f" manifest: {summary['project_file']}")
|
|
412
|
+
print(f" CRS: analysis={summary['crs']['analysis']} storage={summary['crs']['storage']}")
|
|
413
|
+
print(f" sources: {len(summary['sources'])} overrides: {len(summary['overrides'])} steps: {len(summary['steps'])} outputs: {len(summary['outputs'])}")
|
|
414
|
+
for source in summary["sources"]:
|
|
415
|
+
print(f" source {source['key']}: {source.get('provider')} / {source.get('dataset')} ({source.get('version')})")
|
|
416
|
+
for override in summary["overrides"]:
|
|
417
|
+
print(f" override {override.get('id')}: {override.get('action')} by {override.get('created_by')}")
|
|
418
|
+
missing_outputs = [item["path"] for item in summary["outputs"] if not item["exists"]]
|
|
419
|
+
if missing_outputs:
|
|
420
|
+
print(f" missing outputs: {', '.join(str(item) for item in missing_outputs)}")
|
|
421
|
+
latest = summary.get("latest_run") or {}
|
|
422
|
+
print(f" latest run: {latest.get('id')} ({latest.get('status')})")
|
|
423
|
+
_print_validation(validation, verbose=show_checks)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _json(value: object) -> str:
|
|
427
|
+
return json.dumps(value, indent=2, ensure_ascii=False, default=str)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
if __name__ == "__main__":
|
|
431
|
+
raise SystemExit(main())
|