commit 0503638e4d6774ea64b0966755b436cc4827c889 Author: Albrecht Degering Date: Sat Jul 11 17:17:06 2026 +0200 Initialize GovOPlaN SOAP connector diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7bf152 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +build/ +dist/ +*.egg-info/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d204d5 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# GovOPlaN SOAP Connector + + +**Repository type:** connector (protocol). + + +This repository contains the SOAP connector module for GovOPlaN. It owns SOAP +endpoint publication contracts, provider protocols, connector permissions, and +the module manifest. + +The SOAP connector is intentionally transport-oriented. Domain modules expose +capabilities through core/platform contracts, and the SOAP connector adapts +those capabilities into SOAP operations without importing module-internal +business code. + +## Development + +Install this repository next to `govoplan-core`, then include it through the +normal GovOPlaN module discovery flow. + +```sh +python -m pip install -e . +``` + +For whole-product development, use the meta repository: + +```sh +cd /mnt/DATA/git/govoplan +./tools/launch/launch-dev.sh +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e36e5f9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "govoplan-soap" +version = "0.1.7" +description = "SOAP connector module for GovOPlaN" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "govoplan-core>=0.1.7", + "pydantic>=2,<3", +] + +[project.entry-points."govoplan.modules"] +soap = "govoplan_soap.backend.manifest:get_manifest" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +govoplan_soap = ["py.typed"] diff --git a/src/govoplan_soap/__init__.py b/src/govoplan_soap/__init__.py new file mode 100644 index 0000000..fedcd6e --- /dev/null +++ b/src/govoplan_soap/__init__.py @@ -0,0 +1,5 @@ +"""SOAP connector module for GovOPlaN.""" + +__all__ = ["__version__"] + +__version__ = "0.1.7" diff --git a/src/govoplan_soap/backend/__init__.py b/src/govoplan_soap/backend/__init__.py new file mode 100644 index 0000000..3127c04 --- /dev/null +++ b/src/govoplan_soap/backend/__init__.py @@ -0,0 +1 @@ +"""Backend integration points for the SOAP connector module.""" diff --git a/src/govoplan_soap/backend/contracts.py b/src/govoplan_soap/backend/contracts.py new file mode 100644 index 0000000..156118b --- /dev/null +++ b/src/govoplan_soap/backend/contracts.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from typing import Any, Literal, Protocol + + +SoapOperationVisibility = Literal["internal", "authenticated", "public"] + +CAPABILITY_SOAP_OPERATION_PROVIDER = "soap.operationProvider" + + +@dataclass(frozen=True, slots=True) +class SoapOperationDescriptor: + id: str + module_id: str + name: str + summary: str + service_name: str + operation_name: str + visibility: SoapOperationVisibility = "authenticated" + required_scopes: tuple[str, ...] = () + request_schema: Mapping[str, Any] = field(default_factory=dict) + response_schema: Mapping[str, Any] = field(default_factory=dict) + tags: tuple[str, ...] = () + + +class SoapOperationProvider(Protocol): + def soap_operations(self) -> Sequence[SoapOperationDescriptor]: + ... + + def invoke_soap_operation( + self, + operation_id: str, + payload: Mapping[str, Any], + context: Mapping[str, Any], + ) -> Mapping[str, Any]: + ... diff --git a/src/govoplan_soap/backend/manifest.py b/src/govoplan_soap/backend/manifest.py new file mode 100644 index 0000000..940c98b --- /dev/null +++ b/src/govoplan_soap/backend/manifest.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER +from govoplan_core.core.modules import ModuleContext, ModuleInterfaceProvider, ModuleManifest, PermissionDefinition, RoleTemplate + +SOAP_READ_SCOPE = "soap:endpoint:read" +SOAP_READ_SCOPES = (SOAP_READ_SCOPE, "system:settings:read", "admin:settings:read") + + +def _permission(scope: str, label: str, description: str) -> PermissionDefinition: + module_id, resource, action = scope.split(":", 2) + return PermissionDefinition( + scope=scope, + label=label, + description=description, + category="SOAP connector", + level="system", + module_id=module_id, + resource=resource, + action=action, + ) + + +def _route_factory(context: ModuleContext): + del context + from govoplan_soap.backend.router import router + + return router + + +manifest = ModuleManifest( + id="soap", + name="SOAP Connector", + version="0.1.7", + required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), + optional_dependencies=("audit", "docs", "policy"), + provides_interfaces=( + ModuleInterfaceProvider(name="soap.operationPublication", version="0.1.0"), + ), + permissions=( + _permission(SOAP_READ_SCOPE, "View SOAP connector", "Read SOAP connector status and published operation metadata."), + ), + role_templates=( + RoleTemplate( + slug="soap_connector_reader", + name="SOAP connector reader", + description="Read SOAP connector diagnostics and published operation metadata.", + permissions=(SOAP_READ_SCOPE,), + level="system", + ), + ), + route_factory=_route_factory, +) + + +def get_manifest() -> ModuleManifest: + return manifest diff --git a/src/govoplan_soap/backend/router.py b/src/govoplan_soap/backend/router.py new file mode 100644 index 0000000..2977178 --- /dev/null +++ b/src/govoplan_soap/backend/router.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, Depends, Request, status + +from govoplan_core.auth import ApiPrincipal, require_any_scope +from govoplan_core.core.registry import PlatformRegistry + +from govoplan_soap.backend.manifest import SOAP_READ_SCOPES + +router = APIRouter(prefix="/soap", tags=["soap"]) + + +@router.get("/status", status_code=status.HTTP_200_OK) +def soap_status( + request: Request, + principal: ApiPrincipal = Depends(require_any_scope(*SOAP_READ_SCOPES)), +) -> dict[str, Any]: + del principal + registry = getattr(request.app.state, "govoplan_registry", None) + module_count = len(registry.manifests()) if isinstance(registry, PlatformRegistry) else 0 + return { + "module": "soap", + "status": "ok", + "module_count": module_count, + "published_operation_count": 0, + } diff --git a/src/govoplan_soap/py.typed b/src/govoplan_soap/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/govoplan_soap/py.typed @@ -0,0 +1 @@ + diff --git a/tests/test_soap_module_contract.py b/tests/test_soap_module_contract.py new file mode 100644 index 0000000..75028af --- /dev/null +++ b/tests/test_soap_module_contract.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import unittest + +from govoplan_core.core.modules import ModuleContext + +from govoplan_soap.backend.contracts import SoapOperationDescriptor +from govoplan_soap.backend.manifest import get_manifest + + +class SoapModuleContractTests(unittest.TestCase): + def test_manifest_declares_soap_module(self) -> None: + manifest = get_manifest() + + self.assertEqual("soap", manifest.id) + self.assertTrue(manifest.route_factory) + self.assertEqual(("auth.principalResolver", "auth.permissionEvaluator"), manifest.required_capabilities) + + def test_route_factory_exports_status_router(self) -> None: + manifest = get_manifest() + + router = manifest.route_factory(ModuleContext(registry=object(), settings=object())) + + self.assertEqual("/soap", router.prefix) + self.assertEqual(1, len(router.routes)) + + def test_operation_descriptor_is_stable(self) -> None: + descriptor = SoapOperationDescriptor( + id="demo.ping", + module_id="demo", + name="Ping", + summary="Demo operation", + service_name="DemoService", + operation_name="Ping", + ) + + self.assertEqual("DemoService", descriptor.service_name) + self.assertEqual("authenticated", descriptor.visibility) + self.assertEqual((), descriptor.required_scopes) + + +if __name__ == "__main__": + unittest.main()