Initialize GovOPlaN SOAP connector
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.venv/
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
30
README.md
Normal file
30
README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# GovOPlaN SOAP Connector
|
||||
|
||||
<!-- govoplan-repository-type:start -->
|
||||
**Repository type:** connector (protocol).
|
||||
<!-- govoplan-repository-type:end -->
|
||||
|
||||
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
|
||||
```
|
||||
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
@@ -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"]
|
||||
5
src/govoplan_soap/__init__.py
Normal file
5
src/govoplan_soap/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""SOAP connector module for GovOPlaN."""
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.7"
|
||||
1
src/govoplan_soap/backend/__init__.py
Normal file
1
src/govoplan_soap/backend/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Backend integration points for the SOAP connector module."""
|
||||
38
src/govoplan_soap/backend/contracts.py
Normal file
38
src/govoplan_soap/backend/contracts.py
Normal file
@@ -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]:
|
||||
...
|
||||
57
src/govoplan_soap/backend/manifest.py
Normal file
57
src/govoplan_soap/backend/manifest.py
Normal file
@@ -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
|
||||
28
src/govoplan_soap/backend/router.py
Normal file
28
src/govoplan_soap/backend/router.py
Normal file
@@ -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,
|
||||
}
|
||||
1
src/govoplan_soap/py.typed
Normal file
1
src/govoplan_soap/py.typed
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
43
tests/test_soap_module_contract.py
Normal file
43
tests/test_soap_module_contract.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user