Files
troupe-standup/wizard-work/spark/python-env-check.py
T
volt 1cb001a350 volt: Q127 -- restore Q109/Q120 repairs and pin them by digest
899ce4d put both pre-repair blobs back; both cells read red at HEAD (CHECK-env-verifiers 9 failures, test_python_env_check 5 failures + 2 errors). Restored byte-identically from ebba640 and bda365c. New closed_artifact_guard.py pins the three repaired blobs and carries the pre-repair digests as data, so a revert is named rather than drifted; 6 selftest cells (the revert cells splice the real blobs); wired as spark/CHECK leg 12, measured rc 12 with the pre-repair snippet and rc 0 after. Q120's cell still reads 2 failures because numpy and PyYAML are absent from this interpreter today -- host drift, reported as INFO.
2026-10-30 10:54:34 +00:00

141 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Python environment health check for the spark lane.
The dependency list is the lane's own third-party imports, measured from
`wizard-work/spark/*.py` (`grep -rhoE '^(import|from) +[A-Za-z0-9_.]+'`):
PyYAML -> yaml debug_yaml.py:2
numpy -> numpy qc_analyze.py:7
Pillow -> PIL ascii_tool.py:12, qc_analyze.py:6
jsonschema -> jsonschema validate_json_schema.py:14
`email_validator` is deliberately NOT declared: the lane's
`wizard-work/spark/email_validator.py` is a local, stdlib-only module, so
declaring the PyPI distribution would test a package the lane never uses.
The check can fail. A dependency that will not import is reported as
`"status": "missing"` and the process exits 1; a package that imports but
raises something other than ImportError is reported as `"status": "broken"`.
`--require` appends a dependency for one run, which is how the committed
cell in `test_python_env_check.py` proves the red path with a package that
is genuinely absent on the host.
Exit codes (tools/ contract):
0 every declared dependency imports and reports
1 at least one declared or --require'd dependency is missing/broken
2 usage error (argparse)
"""
import argparse
import importlib
import importlib.metadata
import json
import sys
from datetime import datetime, timezone
# (distribution name, import name) for the lane's own third-party imports
DEPENDENCIES = (
("PyYAML", "yaml"),
("numpy", "numpy"),
("Pillow", "PIL"),
("jsonschema", "jsonschema"),
)
def check_package(distribution, import_name=None):
"""Import one dependency by name and report its version.
The version is read from the distribution metadata first; `__version__`
is the fallback for packages that ship no metadata record.
"""
import_name = import_name or distribution
try:
mod = importlib.import_module(import_name)
except ImportError as exc:
return {
"status": "missing",
"available": False,
"import_name": import_name,
"error": "%s: %s" % (type(exc).__name__, exc),
}
except Exception as exc: # a package that raises on import is not healthy
return {
"status": "broken",
"available": False,
"import_name": import_name,
"error": "%s: %s" % (type(exc).__name__, exc),
}
try:
version = importlib.metadata.version(distribution)
except importlib.metadata.PackageNotFoundError:
version = getattr(mod, "__version__", "unknown")
return {
"status": "ok",
"available": True,
"import_name": import_name,
"version": version,
}
def parse_requirement(text):
"""`NAME` or `DISTRIBUTION:IMPORT` -> (distribution, import name)."""
if ":" in text:
distribution, import_name = text.split(":", 1)
if not distribution or not import_name:
raise argparse.ArgumentTypeError(
"--require needs NAME or DISTRIBUTION:IMPORT, got %r" % text
)
return distribution, import_name
return text, text
def build_parser():
parser = argparse.ArgumentParser(
prog="python-env-check.py",
description="Verify the spark lane's third-party dependencies import, "
"and report their versions.",
)
parser.add_argument(
"--require",
metavar="NAME[:IMPORT]",
action="append",
default=[],
type=parse_requirement,
help="also require this package for this run (repeatable); "
"useful for proving the red path",
)
return parser
def main(argv=None):
args = build_parser().parse_args(argv)
declared = list(DEPENDENCIES) + list(args.require)
packages = {}
for distribution, import_name in declared:
result = check_package(distribution, import_name)
result["distribution"] = distribution
packages[import_name] = result
missing = sorted(
name for name, result in packages.items() if result["status"] != "ok"
)
output = {
"all_ok": not missing,
"timestamp": datetime.now(timezone.utc).isoformat(),
"dependencies_declared": len(declared),
"missing": missing,
"components": {
"python": {
"status": "ok",
"version": sys.version,
"executable": sys.executable,
},
"packages": packages,
},
}
print(json.dumps(output, indent=2, sort_keys=True))
return 0 if not missing else 1
if __name__ == "__main__":
sys.exit(main())