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", PARSER / "common_command.py", PARSER / "scheduled_job.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("/methods" in paths, "connector OpenAPI must expose runtime /methods registry", failures) require("/rpc" in paths, "connector OpenAPI must expose universal /rpc", failures) require("/metadata/write-plan" in paths, "connector OpenAPI must expose /metadata/write-plan", failures) schemas = ((openapi.get("components") or {}).get("schemas") or {}) if isinstance(openapi.get("components"), dict) else {} require("AdapterRpcRequest" in schemas, "connector OpenAPI must define AdapterRpcRequest", failures) require("AdapterMethodsResponse" in schemas, "connector OpenAPI must define AdapterMethodsResponse", failures) try: sys.path.insert(0, str(CONNECTOR)) sys.path.insert(0, str(CONNECTOR.parent)) import adapter_1c_server as adapter_server runtime_paths = set(adapter_server.HTTP_GET_METHOD_ROUTES) | set(adapter_server.HTTP_POST_METHOD_ROUTES) | {"/rpc"} require( set(paths) == runtime_paths, f"OpenAPI/runtime HTTP route drift: missing_in_openapi={sorted(runtime_paths - set(paths))}, missing_in_runtime={sorted(set(paths) - runtime_paths)}", failures, ) require( all("get" in (paths.get(path) or {}) for path in adapter_server.HTTP_GET_METHOD_ROUTES), "every runtime GET route must be declared as GET in OpenAPI", failures, ) require( all("post" in (paths.get(path) or {}) for path in adapter_server.HTTP_POST_METHOD_ROUTES), "every runtime POST route must be declared as POST in OpenAPI", failures, ) except Exception as exc: failures.append(f"could not verify runtime HTTP route registry: {exc}") 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())