feat: consolidate legacy workflows into gyxx-flow

This commit is contained in:
wangyunlong committed 2026-07-28 14:51:15 +08:00
commit c23b62a8c8
374 files changed
+132990

No files matched your search

+139
View File
@@ -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"]