from __future__ import annotations import argparse import json import sys import tomllib from pathlib import Path from typing import Any import yaml ROOT = Path(__file__).resolve().parents[1] CONNECTOR = ROOT / "plugins" / "1c" / "connector" PARSER = ROOT / "plugins" / "1c" / "parser" def require(condition: bool, message: str, failures: list[str]) -> None: if not condition: failures.append(message) def read_yaml(path: Path) -> dict[str, Any]: with path.open("r", encoding="utf-8") as handle: data = yaml.safe_load(handle) return data if isinstance(data, dict) else {} def read_toml(path: Path) -> dict[str, Any]: with path.open("rb") as handle: data = tomllib.load(handle) return data if isinstance(data, dict) else {} def run_checks() -> dict[str, Any]: failures: list[str] = [] required = [ CONNECTOR / "adapter_1c_server.py", CONNECTOR / "contracts" / "openapi.yaml", CONNECTOR / "policies" / "read-only-query.yaml", CONNECTOR / "policies" / "sql-base-access-policy.yaml", CONNECTOR / "policies" / "change-workflow.yaml", CONNECTOR / "policies" / "config-layer-write-policy.yaml", CONNECTOR / "Dockerfile", CONNECTOR / "docker-compose.yml", CONNECTOR / ".env.example", CONNECTOR / "pyproject.toml", CONNECTOR / "service.yaml", CONNECTOR / "README.md", PARSER / "__init__.py", PARSER / "payload.py", PARSER / "cas_payload.py", ] missing = [str(path.relative_to(ROOT)) for path in required if not path.exists()] require(not missing, f"missing standalone connector files: {missing}", failures) service = read_yaml(CONNECTOR / "service.yaml") if (CONNECTOR / "service.yaml").exists() else {} compose = read_yaml(CONNECTOR / "docker-compose.yml") if (CONNECTOR / "docker-compose.yml").exists() else {} pyproject = read_toml(CONNECTOR / "pyproject.toml") if (CONNECTOR / "pyproject.toml").exists() else {} openapi = read_yaml(CONNECTOR / "contracts" / "openapi.yaml") if (CONNECTOR / "contracts" / "openapi.yaml").exists() else {} access_policy = read_yaml(CONNECTOR / "policies" / "sql-base-access-policy.yaml") if (CONNECTOR / "policies" / "sql-base-access-policy.yaml").exists() else {} require(service.get("id") == "onec-adapter-connector", "service.yaml must identify onec-adapter-connector", failures) require(service.get("status") == "standalone-ready", "service.yaml status must be standalone-ready", failures) require((service.get("runtime") or {}).get("entrypoint") == "adapter_1c_server.py", "service entrypoint must be adapter_1c_server.py", failures) require("contracts/openapi.yaml" == (service.get("contracts") or {}).get("openapi"), "service must point to connector OpenAPI contract", failures) registered_policies = (service.get("contracts") or {}).get("policies") or [] require("policies/sql-base-access-policy.yaml" in registered_policies, "service must register SQL base access policy", failures) base_settings = access_policy.get("base_settings") if isinstance(access_policy.get("base_settings"), dict) else {} read_scope = access_policy.get("read_scope") if isinstance(access_policy.get("read_scope"), dict) else {} write_scope = access_policy.get("write_scope") if isinstance(access_policy.get("write_scope"), dict) else {} identity = access_policy.get("sql_identity_management") if isinstance(access_policy.get("sql_identity_management"), dict) else {} require(access_policy.get("status") == "active", "SQL base access policy must be active", failures) require(base_settings.get("selector") == "base_id", "SQL settings must be selected by base_id", failures) require(set(base_settings.get("required_fields") or []) == {"server", "database", "user"}, "SQL base settings must require server, database, and user", failures) require(read_scope.get("application_data") == "read_only", "application data must be read-only", failures) require(read_scope.get("metadata_structure") == "read_only", "metadata structure must be readable without mutation", failures) require(set((write_scope.get("allowed") or {}).values()) == {"ConfigSave", "ConfigCASSave"}, "only ConfigSave and ConfigCASSave may be write targets", failures) require(identity.get("mode") == "forbidden", "SQL identity management must be forbidden", failures) project = pyproject.get("project") if isinstance(pyproject.get("project"), dict) else {} require(project.get("name") == "onec-adapter-connector", "pyproject project.name must be onec-adapter-connector", failures) scripts = project.get("scripts") if isinstance(project.get("scripts"), dict) else {} require(scripts.get("onec-adapter") == "adapter_1c_server:main", "pyproject must expose onec-adapter script", failures) dependencies = project.get("dependencies") if isinstance(project.get("dependencies"), list) else [] require(any(str(dep).startswith("pymssql") for dep in dependencies), "pyproject must include pymssql dependency", failures) services = compose.get("services") if isinstance(compose.get("services"), dict) else {} adapter_service = services.get("onec-adapter") if isinstance(services.get("onec-adapter"), dict) else {} build = adapter_service.get("build") if isinstance(adapter_service.get("build"), dict) else {} require(build.get("context") == "..", "docker-compose build context must include parser sibling", failures) require(build.get("dockerfile") == "connector/Dockerfile", "docker-compose must use connector/Dockerfile", failures) require(bool(adapter_service.get("healthcheck")), "docker-compose must define a healthcheck", failures) require(openapi.get("openapi") == "3.1.0", "connector OpenAPI must parse as 3.1.0", failures) paths = openapi.get("paths") if isinstance(openapi.get("paths"), dict) else {} require("/health" in paths, "connector OpenAPI must expose /health", failures) require("/metadata/write-plan" in paths, "connector OpenAPI must expose /metadata/write-plan", failures) return { "schema": "onec_connector_standalone_check.v1", "status": "ok" if not failures else "failed", "failures": failures, "checks": { "required_files": not missing, "service_manifest": service.get("id"), "pyproject": project.get("name"), "compose_service": "onec-adapter" in services, "openapi": openapi.get("openapi"), }, } def main() -> int: parser = argparse.ArgumentParser(description="Check 1C connector standalone-ready service packaging.") parser.add_argument("--print", action="store_true") args = parser.parse_args() report = run_checks() if args.print or report["status"] != "ok": print(json.dumps(report, ensure_ascii=False, indent=2)) else: print("1C connector standalone status: ok") return 0 if report["status"] == "ok" else 1 if __name__ == "__main__": raise SystemExit(main())