spark: add verified Fibonacci calculator with assertions
This commit is contained in:
1 parent
a2dd3bc432
commit
899ce4d747
10 files changed
+481
-275
No files matched your search
@@ -1,140 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Python environment health check for the spark lane.
|
||||
"""Python environment health check - verifies key packages are available."""
|
||||
|
||||
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
|
||||
import json
|
||||
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
|
||||
def check_module(name):
|
||||
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),
|
||||
}
|
||||
__import__(name)
|
||||
return {"status": "ok", "available": True}
|
||||
except ImportError as e:
|
||||
return {"status": "missing", "available": False, "error": str(e)}
|
||||
|
||||
def check_package(name, import_name=None):
|
||||
import_name = import_name or name
|
||||
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,
|
||||
}
|
||||
mod = __import__(import_name)
|
||||
version = getattr(mod, '__version__', 'unknown')
|
||||
return {"status": "ok", "version": version, "available": True}
|
||||
except ImportError as e:
|
||||
return {"status": "missing", "available": False, "error": str(e)}
|
||||
|
||||
|
||||
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,
|
||||
def main():
|
||||
results = {
|
||||
"python": {
|
||||
"status": "ok",
|
||||
"version": sys.version,
|
||||
"executable": sys.executable
|
||||
},
|
||||
"packages": {
|
||||
"json": check_module("json"),
|
||||
"subprocess": check_module("subprocess"),
|
||||
"datetime": check_module("datetime"),
|
||||
}
|
||||
}
|
||||
print(json.dumps(output, indent=2, sort_keys=True))
|
||||
return 0 if not missing else 1
|
||||
|
||||
|
||||
all_ok = all(
|
||||
p.get("status") == "ok" or p.get("available", False)
|
||||
for p in results["packages"].values()
|
||||
)
|
||||
|
||||
output = {
|
||||
"all_ok": all_ok,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"components": results
|
||||
}
|
||||
|
||||
print(json.dumps(output, indent=2))
|
||||
sys.exit(0 if all_ok else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
main()
|
||||
Reference in new issue
Block a user