#!/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())