chore: import upstream snapshot with attribution
Validate YAML Workflows / Validate YAML Configuration Files (push) Has been cancelled
Validate YAML Workflows / Validate YAML Configuration Files (push) Has been cancelled
This commit is contained in:
Executable
+1
@@ -0,0 +1 @@
|
||||
"""Utility scripts for developer tooling."""
|
||||
Executable
+241
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user