chore: import upstream snapshot with attribution
Validate YAML Workflows / Validate YAML Configuration Files (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:51 +08:00
commit d0e4308def
614 changed files with 74458 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Utility scripts for developer tooling."""
+241
View File
@@ -0,0 +1,241 @@
"""CLI for exporting DesignConfig YAML templates from typed schemas."""
import argparse
from collections import OrderedDict
from pathlib import Path
from typing import Any, Dict, Mapping, Sequence, Tuple
import yaml
from runtime.bootstrap.schema import ensure_schema_registry_populated
from entity.configs import BaseConfig, DesignConfig
from entity.configs.base import ChildKey, ConfigFieldSpec
TYPE_ALIASES: Dict[str, str] = {
"str": "string",
"string": "string",
"int": "int",
"float": "float",
"bool": "bool",
"list": "list",
"dict": "dict",
"mapping": "mapping",
"enum": "enum",
}
ensure_schema_registry_populated()
class DesignTemplateEmitter:
"""Builds human-oriented YAML templates from config schemas."""
def __init__(self, root_cls: type[BaseConfig] = DesignConfig):
self.root_cls = root_cls
def build(self, *, version: str | None = None) -> OrderedDict[str, Any]:
document = self._emit_config(self.root_cls, stack=[])
if version:
document["version"] = version
return document
# ------------------------------------------------------------------
# Rendering helpers
# ------------------------------------------------------------------
def _emit_config(self, config_cls: type[BaseConfig], *, stack: list[type[BaseConfig]]) -> OrderedDict[str, Any]:
if config_cls in stack:
return OrderedDict(
{
self._format_recursive_placeholder(config_cls, stack):
"See earlier definition to avoid infinite recursion",
}
)
stack.append(config_cls)
payload: "OrderedDict[str, Any]" = OrderedDict()
field_specs = config_cls.field_specs()
try:
for name, spec in field_specs.items():
payload[name] = self._emit_field(config_cls, spec, stack=stack)
finally:
stack.pop()
return payload
def _emit_field(self, parent_cls: type[BaseConfig], spec: ConfigFieldSpec, *, stack: list[type[BaseConfig]]) -> Any:
routes = self._routes_for_field(parent_cls, spec.name)
if routes:
variant_block: "OrderedDict[str, Any]" = OrderedDict()
for label, child_cls in routes:
variant_block[label] = self._wrap_with_container(
spec,
self._emit_config(child_cls, stack=stack),
)
return variant_block
if spec.child is not None:
return self._wrap_with_container(
spec,
self._emit_config(spec.child, stack=stack),
)
hint = (spec.type_hint or "value").lower()
if self._looks_like_dict(hint):
value_placeholder = self._format_placeholder(type_hint="value", required=True, default=None, enum=None)
return OrderedDict({"<key>": value_placeholder})
if self._looks_like_list(hint):
inner_hint = self._extract_inner_type(spec.type_hint)
entry_placeholder = self._format_placeholder(
type_hint=inner_hint,
required=spec.required,
default=None,
enum=spec.enum,
)
return [entry_placeholder]
return self._format_placeholder_from_spec(spec)
def _routes_for_field(self, parent_cls: type[BaseConfig], field_name: str) -> Sequence[Tuple[str, type[BaseConfig]]]:
routes: list[Tuple[str, type[BaseConfig]]] = []
for key, child in parent_cls.child_routes().items():
if key.field != field_name:
continue
label = self._format_variant_label(key)
routes.append((label, child))
return routes
def _wrap_with_container(self, spec: ConfigFieldSpec, payload: Any) -> Any:
hint = (spec.type_hint or "").lower()
if self._looks_like_list(hint):
return [payload]
return payload
# ------------------------------------------------------------------
# Formatting utilities
# ------------------------------------------------------------------
def _format_placeholder_from_spec(self, spec: ConfigFieldSpec) -> str:
return self._format_placeholder(
type_hint=spec.type_hint,
required=spec.required,
default=spec.default,
enum=spec.enum,
)
def _format_placeholder(
self,
*,
type_hint: str | None,
required: bool,
default: Any,
enum: Sequence[Any] | None,
) -> str:
type_label = self._normalize_type(type_hint)
default_label = self._format_default(enum=enum, default=default, required=required)
return f"<{type_label}> | {default_label}"
def _format_default(
self,
*,
enum: Sequence[Any] | None,
default: Any,
required: bool,
) -> str:
if enum:
return f"[{', '.join(map(str, enum))}]"
if default is None:
return "required" if required else "None"
if isinstance(default, bool):
return "true" if default else "false"
if isinstance(default, (int, float)):
return str(default)
if isinstance(default, str):
return f'"{default}"'
if isinstance(default, Mapping):
return "{}"
if isinstance(default, Sequence) and not isinstance(default, (str, bytes)):
return "[]"
return str(default)
def _normalize_type(self, type_hint: str | None) -> str:
if not type_hint:
return "value"
normalized = type_hint.strip()
base = normalized.split("[", 1)[0].split("|", 1)[0].strip().lower()
alias = TYPE_ALIASES.get(base)
if alias:
normalized = normalized.replace(base, alias, 1)
return normalized
@staticmethod
def _looks_like_list(type_hint: str) -> bool:
return type_hint.startswith("list") or type_hint.endswith("[]")
@staticmethod
def _looks_like_dict(type_hint: str) -> bool:
return type_hint.startswith("dict") or "mapping" in type_hint
@staticmethod
def _extract_inner_type(type_hint: str | None) -> str:
if not type_hint or "[" not in type_hint or "]" not in type_hint:
return "value"
start = type_hint.find("[") + 1
end = type_hint.rfind("]")
inner = type_hint[start:end].strip()
return inner or "value"
@staticmethod
def _format_variant_label(key: ChildKey) -> str:
if key.value is None:
return f"<variant[{key.field}]>"
return f"<variant[{key.field}]={key.value}>"
@staticmethod
def _format_recursive_placeholder(config_cls: type[BaseConfig], stack: list[type[BaseConfig]]) -> str:
cycle = "".join(cls.__name__ for cls in (*stack, config_cls))
return f"<recursive[{config_cls.__name__}] path: {cycle}>"
def dump_yaml(data: Mapping[str, Any], path: Path) -> None:
class _Dumper(yaml.SafeDumper):
pass
def _represent_ordered_dict(dumper: yaml.SafeDumper, value: OrderedDict) -> yaml.nodes.MappingNode: # type: ignore
return dumper.represent_dict(value.items())
_Dumper.add_representer(OrderedDict, _represent_ordered_dict)
with path.open("w", encoding="utf-8") as handle:
yaml.dump(data, handle, Dumper=_Dumper, sort_keys=False, allow_unicode=True)
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Export design_0.4.0 YAML templates from config schemas")
parser.add_argument("--output", type=Path, required=True, help="Primary output YAML path")
parser.add_argument("--version", type=str, default=None, help="Version string to pin in the template")
parser.add_argument(
"--mirror",
type=Path,
nargs="*",
default=(),
help="Optional additional paths that should receive the same generated document",
)
return parser.parse_args(argv)
def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(argv)
emitter = DesignTemplateEmitter(DesignConfig)
document = emitter.build(version=args.version)
targets = [args.output, *args.mirror]
for target in targets:
target.parent.mkdir(parents=True, exist_ok=True)
dump_yaml(document, target)
print("Exported design template to:")
for target in targets:
print(f" - {target.resolve()}")
return 0
if __name__ == "__main__": # pragma: no cover
# uv run -m tools.export_design_template --output yaml_template/design.yaml
raise SystemExit(main())
+65
View File
@@ -0,0 +1,65 @@
"""
Synchronize YAML Configurations to VueGraph Database
This tool uploads local YAML workflow configurations from the yaml_instance/
directory to the VueGraph database via the API endpoint. This is essential for
making workflow configurations available to the frontend visualization system.
Purpose:
- Ensures the database reflects the latest YAML configurations
- Required after modifying workflow YAML files to see changes in the UI
- Useful for development and deployment workflows
Usage:
python tools/sync_vuegraphs.py
# or via Makefile:
make sync
"""
import os
import glob
import requests
import yaml
from pathlib import Path
# Configuration
API_URL = "http://localhost:6400/api/vuegraphs/upload/content"
YAML_DIR = "yaml_instance"
def sync_yaml_to_vuegraphs():
"""Reads all YAML files and uploads them to the VueGraph database."""
print(f"Syncing YAML files from {YAML_DIR} to {API_URL}...")
yaml_files = glob.glob(os.path.join(YAML_DIR, "*.yaml"))
for file_path in yaml_files:
try:
filename = Path(file_path).stem # simulation_hospital_lmstudio
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Basic validation to ensure it's a valid YAML
try:
yaml.safe_load(content)
except yaml.YAMLError as e:
print(f"Skipping {filename}: Invalid YAML - {e}")
continue
# Upload to VueGraph API
payload = {"filename": filename, "content": content}
response = requests.post(API_URL, json=payload)
if response.status_code == 200:
print(f"Synced: {filename}")
else:
print(f"Failed: {filename} - {response.status_code} {response.text}")
except Exception as e:
print(f"Error processing {file_path}: {e}")
if __name__ == "__main__":
sync_yaml_to_vuegraphs()
+104
View File
@@ -0,0 +1,104 @@
"""
Validate All YAML Workflow Configurations
This tool performs strict validation on all YAML workflow configuration files
in the yaml_instance/ directory. It ensures configuration integrity and prevents
runtime errors by catching issues early in the development process.
Purpose:
- Validates YAML syntax and schema compliance for all workflow configurations
- Prevents invalid configurations from causing runtime failures
- Essential for CI/CD pipelines to ensure code quality
- Provides detailed error reporting for debugging
Usage:
python tools/validate_all_yamls.py
# or via Makefile:
make validate-yamls
"""
import sys
import subprocess
from pathlib import Path
def validate_all():
base_dir = Path("yaml_instance")
if not base_dir.exists():
print(f"Directory {base_dir} not found.")
sys.exit(1)
# Recursive search for all .yaml files
files = sorted(list(base_dir.rglob("*.yaml")))
if not files:
print("No YAML files found.")
return
print(
f"Found {len(files)} YAML files. Running FULL validation via check.check...\n"
)
passed = 0
failed = 0
failed_files = []
for yaml_file in files:
# Use relative path for cleaner output
try:
rel_path = yaml_file.relative_to(Path.cwd())
except ValueError:
rel_path = yaml_file
# NOW we run check.check, which we just patched to have a main()
# This performs the stricter load_config() validation
cmd = [sys.executable, "-m", "check.check", "--path", str(yaml_file)]
try:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"{rel_path}")
passed += 1
else:
print(f"{rel_path}")
# Indent error output
if result.stdout:
print(" stdout:", result.stdout.strip().replace("\n", "\n "))
# Validation errors usually print to stdout/stderr depending on impl
# Our new main prints to stdout for success/failure message
failed += 1
failed_files.append(str(rel_path))
except Exception as e:
print(f"{rel_path} (Execution Failed)")
print(f" Error: {e}")
failed += 1
failed_files.append(str(rel_path))
print("\n" + "=" * 40)
print(f"YAML Validation Summary")
print("=" * 40)
print(f"Total Files: {len(files)}")
print(f"Passed: {passed}")
print(f"Failed: {failed}")
if failed > 0:
print("\nFailed Files:")
for f in failed_files:
print(f"- {f}")
# Overall validation status
print("\n" + "=" * 40)
print("Overall Validation Status")
print("=" * 40)
if failed > 0:
print("YAML validation: FAILED")
sys.exit(1)
else:
print("All validations passed successfully.")
sys.exit(0)
if __name__ == "__main__":
validate_all()