feat: consolidate legacy workflows into gyxx-flow
This commit is contained in:
commit
c23b62a8c8
374 files changed
+132990
No files matched your search
@@ -0,0 +1,139 @@
|
||||
"""Discover manually runnable scripts owned by the four business modules."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from types import MappingProxyType
|
||||
from typing import Iterable, Literal, Mapping
|
||||
|
||||
|
||||
class ScriptCatalogError(LookupError):
|
||||
"""Raised when a script identifier is invalid or unavailable."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ScriptEntry:
|
||||
script_id: str
|
||||
module: str
|
||||
entry: str
|
||||
kind: Literal["python", "batch", "powershell"]
|
||||
path: Path
|
||||
|
||||
|
||||
class ScriptCatalog:
|
||||
"""Immutable, deterministic index of project-owned command-line scripts."""
|
||||
|
||||
def __init__(self, scripts: Iterable[ScriptEntry]) -> None:
|
||||
index: dict[str, ScriptEntry] = {}
|
||||
for script in scripts:
|
||||
if script.script_id in index:
|
||||
raise ScriptCatalogError(f"duplicate script id: {script.script_id}")
|
||||
index[script.script_id] = script
|
||||
self._scripts = MappingProxyType(dict(sorted(index.items())))
|
||||
|
||||
@classmethod
|
||||
def discover(cls, roots: Mapping[str, Path | str]) -> "ScriptCatalog":
|
||||
scripts: list[ScriptEntry] = []
|
||||
for module, configured_root in sorted(roots.items()):
|
||||
root = Path(configured_root).expanduser().resolve(strict=True)
|
||||
if not root.is_dir():
|
||||
raise ScriptCatalogError(f"runtime root is not a directory: {root}")
|
||||
for path in sorted(root.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
relative = path.relative_to(root)
|
||||
if any(
|
||||
part
|
||||
in {"__pycache__", "tests", "runtime_tests", "launchers_reference"}
|
||||
for part in relative.parts
|
||||
):
|
||||
continue
|
||||
suffix = path.suffix.casefold()
|
||||
kind: Literal["python", "batch", "powershell"] | None = None
|
||||
if suffix in {".py", ".pyw"} and _has_main_guard(path):
|
||||
kind = "python"
|
||||
elif suffix in {".bat", ".cmd"}:
|
||||
kind = "batch"
|
||||
elif suffix == ".ps1":
|
||||
kind = "powershell"
|
||||
if kind is None:
|
||||
continue
|
||||
entry = relative.as_posix()
|
||||
scripts.append(
|
||||
ScriptEntry(
|
||||
script_id=f"{module}:{entry}",
|
||||
module=module,
|
||||
entry=entry,
|
||||
kind=kind,
|
||||
path=path.resolve(),
|
||||
)
|
||||
)
|
||||
return cls(scripts)
|
||||
|
||||
@classmethod
|
||||
def discover_default(cls) -> "ScriptCatalog":
|
||||
module_root = Path(__file__).resolve().parent / "modules"
|
||||
modules = (
|
||||
"content_marketing",
|
||||
"product_commerce",
|
||||
"shop_intelligence",
|
||||
"supply_chain",
|
||||
)
|
||||
return cls.discover(
|
||||
{module: module_root / module / "runtime" for module in modules}
|
||||
)
|
||||
|
||||
@property
|
||||
def scripts(self) -> tuple[ScriptEntry, ...]:
|
||||
return tuple(self._scripts.values())
|
||||
|
||||
@property
|
||||
def script_ids(self) -> tuple[str, ...]:
|
||||
return tuple(self._scripts)
|
||||
|
||||
def get(self, script_id: str) -> ScriptEntry:
|
||||
if not isinstance(script_id, str) or ":" not in script_id:
|
||||
raise ScriptCatalogError(f"invalid script id: {script_id!r}")
|
||||
try:
|
||||
return self._scripts[script_id]
|
||||
except KeyError as exc:
|
||||
raise ScriptCatalogError(f"unknown script: {script_id}") from exc
|
||||
|
||||
|
||||
def _has_main_guard(path: Path) -> bool:
|
||||
text: str | None = None
|
||||
for encoding in ("utf-8-sig", "gb18030"):
|
||||
try:
|
||||
text = path.read_text(encoding=encoding)
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if text is None:
|
||||
return False
|
||||
try:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", SyntaxWarning)
|
||||
tree = ast.parse(text, filename=str(path))
|
||||
except SyntaxError:
|
||||
return False
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.If) or not isinstance(node.test, ast.Compare):
|
||||
continue
|
||||
comparison = node.test
|
||||
if (
|
||||
isinstance(comparison.left, ast.Name)
|
||||
and comparison.left.id == "__name__"
|
||||
and len(comparison.ops) == 1
|
||||
and isinstance(comparison.ops[0], ast.Eq)
|
||||
and len(comparison.comparators) == 1
|
||||
and isinstance(comparison.comparators[0], ast.Constant)
|
||||
and comparison.comparators[0].value == "__main__"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
__all__ = ["ScriptCatalog", "ScriptCatalogError", "ScriptEntry"]
|
||||
Reference in new issue
Block a user