232 lines
8.0 KiB
Python
232 lines
8.0 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from datetime import datetime, timezone
|
|
from math import fsum
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def resolve_python_interpreter() -> list[str]:
|
|
"""Prefer the project Python 3.13 interpreter on Windows.
|
|
|
|
The repo's working `python` command may point at an older interpreter, but
|
|
the build and validation scripts depend on the package set installed under
|
|
Python 3.13. Fall back to sys.executable only if the launcher is unavailable.
|
|
"""
|
|
configured = os.environ.get("CODEX_PYTHON")
|
|
if configured:
|
|
return [configured]
|
|
if os.name == "nt":
|
|
for candidate in (
|
|
r"C:\Users\kjh20\AppData\Local\Programs\Python\Python313\python.exe",
|
|
r"C:\Users\kjh20\AppData\Local\Python\pythoncore-3.13-64\python.exe",
|
|
):
|
|
if Path(candidate).exists():
|
|
return [candidate]
|
|
return ["py", "-3.13"]
|
|
return [sys.executable]
|
|
|
|
|
|
def utf8_env() -> dict[str, str]:
|
|
env = os.environ.copy()
|
|
env.setdefault("PYTHONIOENCODING", "utf-8")
|
|
env.setdefault("PYTHONUTF8", "1")
|
|
return env
|
|
|
|
|
|
def _run_command(command: list[str], cwd: Path | None = None) -> dict[str, Any]:
|
|
started = datetime.now(timezone.utc)
|
|
resolved = list(command)
|
|
if os.name == "nt" and resolved and resolved[0].lower() == "npm":
|
|
resolved[0] = "npm.cmd"
|
|
if resolved and resolved[0].endswith(".py"):
|
|
resolved = [*resolve_python_interpreter(), *resolved]
|
|
subprocess.run(resolved, cwd=cwd or ROOT, check=True, env=utf8_env())
|
|
finished = datetime.now(timezone.utc)
|
|
return {
|
|
"kind": "command",
|
|
"command": command,
|
|
"started_at": started.isoformat(),
|
|
"finished_at": finished.isoformat(),
|
|
"elapsed_sec": round((finished - started).total_seconds(), 3),
|
|
"status": "OK",
|
|
}
|
|
|
|
|
|
def _run_callable(func: Callable[[], Any]) -> dict[str, Any]:
|
|
started = datetime.now(timezone.utc)
|
|
payload = func()
|
|
finished = datetime.now(timezone.utc)
|
|
return {
|
|
"kind": "callable",
|
|
"started_at": started.isoformat(),
|
|
"finished_at": finished.isoformat(),
|
|
"elapsed_sec": round((finished - started).total_seconds(), 3),
|
|
"status": "OK",
|
|
"payload": payload,
|
|
}
|
|
|
|
|
|
def _parse_iso8601(value: str) -> datetime:
|
|
text = str(value or "")
|
|
if text.endswith("Z"):
|
|
text = text[:-1] + "+00:00"
|
|
return datetime.fromisoformat(text)
|
|
|
|
|
|
def summarize_plan(results: list[dict[str, Any]]) -> dict[str, Any]:
|
|
if not results:
|
|
return {
|
|
"step_count": 0,
|
|
"command_step_count": 0,
|
|
"callable_step_count": 0,
|
|
"critical_path_sec": 0.0,
|
|
"wall_clock_sec": 0.0,
|
|
"parallel_width_max": 0,
|
|
"step_names": [],
|
|
}
|
|
|
|
by_name = {str(item.get("name")): item for item in results if item.get("name")}
|
|
memo: dict[str, float] = {}
|
|
|
|
def critical_path_for(name: str) -> float:
|
|
if name in memo:
|
|
return memo[name]
|
|
item = by_name[name]
|
|
own = float(item.get("elapsed_sec") or 0.0)
|
|
deps = [str(dep) for dep in (item.get("depends_on") or []) if str(dep) in by_name]
|
|
if deps:
|
|
own += max(critical_path_for(dep) for dep in deps)
|
|
memo[name] = own
|
|
return own
|
|
|
|
starts = []
|
|
ends = []
|
|
points: list[tuple[datetime, int]] = []
|
|
command_step_count = 0
|
|
callable_step_count = 0
|
|
for item in results:
|
|
started = _parse_iso8601(str(item.get("started_at")))
|
|
finished = _parse_iso8601(str(item.get("finished_at")))
|
|
starts.append(started)
|
|
ends.append(finished)
|
|
points.append((started, 1))
|
|
points.append((finished, -1))
|
|
if item.get("kind") == "command":
|
|
command_step_count += 1
|
|
elif item.get("kind") == "callable":
|
|
callable_step_count += 1
|
|
|
|
points.sort(key=lambda pair: (pair[0], -pair[1]))
|
|
active = 0
|
|
parallel_width_max = 0
|
|
for _, delta in points:
|
|
active += delta
|
|
if active > parallel_width_max:
|
|
parallel_width_max = active
|
|
|
|
step_elapsed_sum = fsum(float(item.get("elapsed_sec") or 0.0) for item in results)
|
|
wall_clock_sec = round((max(ends) - min(starts)).total_seconds(), 3)
|
|
critical_path_sec = round(max(critical_path_for(name) for name in by_name), 3)
|
|
return {
|
|
"step_count": len(results),
|
|
"command_step_count": command_step_count,
|
|
"callable_step_count": callable_step_count,
|
|
"step_elapsed_sum_sec": round(step_elapsed_sum, 3),
|
|
"critical_path_sec": critical_path_sec,
|
|
"wall_clock_sec": wall_clock_sec,
|
|
"parallel_width_max": parallel_width_max,
|
|
"step_names": [str(item.get("name")) for item in results if item.get("name")],
|
|
}
|
|
|
|
|
|
def run_plan(
|
|
steps: list[dict[str, Any]],
|
|
*,
|
|
label: str = "orchestration",
|
|
cwd: Path | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""
|
|
Execute a dependency plan.
|
|
|
|
Each step accepts:
|
|
- name: unique string
|
|
- depends_on: list[str]
|
|
- command: list[str] for subprocess execution, or
|
|
- callable: zero-arg callable for in-process execution
|
|
"""
|
|
if not steps:
|
|
return []
|
|
|
|
step_map: dict[str, dict[str, Any]] = {}
|
|
dependents: dict[str, set[str]] = {}
|
|
remaining: set[str] = set()
|
|
for index, step in enumerate(steps):
|
|
name = str(step.get("name") or "").strip()
|
|
if not name:
|
|
raise ValueError(f"{label}: step missing name at index {index}")
|
|
if name in step_map:
|
|
raise ValueError(f"{label}: duplicate step name: {name}")
|
|
step_map[name] = dict(step)
|
|
step_map[name]["_index"] = index
|
|
remaining.add(name)
|
|
dependents.setdefault(name, set())
|
|
|
|
for name, step in step_map.items():
|
|
deps = step.get("depends_on") or []
|
|
if not isinstance(deps, list):
|
|
raise ValueError(f"{label}: depends_on must be a list for {name}")
|
|
step["_depends_on"] = {str(dep) for dep in deps}
|
|
for dep in step["_depends_on"]:
|
|
if dep not in step_map:
|
|
raise ValueError(f"{label}: unknown dependency {dep} for {name}")
|
|
dependents.setdefault(dep, set()).add(name)
|
|
|
|
completed: set[str] = set()
|
|
results: dict[str, dict[str, Any]] = {}
|
|
while remaining:
|
|
ready = sorted(
|
|
[name for name in remaining if step_map[name]["_depends_on"].issubset(completed)],
|
|
key=lambda n: step_map[n]["_index"],
|
|
)
|
|
if not ready:
|
|
blocked = sorted(remaining)
|
|
raise RuntimeError(f"{label}: cyclic or blocked dependencies: {blocked}")
|
|
|
|
with ThreadPoolExecutor(max_workers=len(ready)) as executor:
|
|
future_map = {}
|
|
for name in ready:
|
|
step = step_map[name]
|
|
if "command" in step and step["command"] is not None:
|
|
future = executor.submit(_run_command, list(step["command"]), cwd)
|
|
elif "callable" in step and step["callable"] is not None:
|
|
future = executor.submit(_run_callable, step["callable"])
|
|
else:
|
|
raise ValueError(f"{label}: step {name} missing command/callable")
|
|
future_map[future] = name
|
|
|
|
for future in as_completed(future_map):
|
|
name = future_map[future]
|
|
result = future.result()
|
|
result["name"] = name
|
|
result["depends_on"] = list(step_map[name]["_depends_on"])
|
|
result["index"] = step_map[name]["_index"]
|
|
results[name] = result
|
|
completed.add(name)
|
|
remaining.remove(name)
|
|
for child in dependents.get(name, set()):
|
|
if child in step_map:
|
|
step_map[child]["_depends_on"].discard(name)
|
|
|
|
ordered = sorted(results.values(), key=lambda item: item["index"])
|
|
for item in ordered:
|
|
item.pop("index", None)
|
|
return ordered
|