Initialize GovOPlaN REST connector

This commit is contained in:
2026-07-11 17:17:03 +02:00
commit ada9f843eb
10 changed files with 226 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
.venv/
build/
dist/
*.egg-info/

26
README.md Normal file
View File

@@ -0,0 +1,26 @@
# govoplan-rest
<!-- govoplan-repository-type:start -->
**Repository type:** connector (protocol).
<!-- govoplan-repository-type:end -->
`govoplan-rest` provides the GovOPlaN REST connector surface. It is intended to
expose selected module functions through governed HTTP endpoints without forcing
business modules to import each other directly.
The module starts with a small provider contract and discovery endpoint. Domain
modules will later register REST-exposable functions through the connector
contract; the connector owns HTTP binding, policy checks, serialization, and
operational diagnostics for that surface.
## Ownership
This repository owns:
- backend module manifest `rest`
- REST connector route contributions
- REST function publication DTOs and provider protocol
- future REST endpoint policy, throttling, and public/credentialed access rules
It does not own business semantics. Modules that expose functions remain
responsible for validation, authorization facts, and side effects.

24
pyproject.toml Normal file
View File

@@ -0,0 +1,24 @@
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "govoplan-rest"
version = "0.1.7"
description = "GovOPlaN REST connector module."
readme = "README.md"
requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }]
dependencies = [
"govoplan-core>=0.1.7",
"pydantic>=2,<3",
]
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
govoplan_rest = ["py.typed"]
[project.entry-points."govoplan.modules"]
rest = "govoplan_rest.backend.manifest:get_manifest"

View File

@@ -0,0 +1,5 @@
"""GovOPlaN REST connector module."""
__all__ = ["__version__"]
__version__ = "0.1.7"

View File

@@ -0,0 +1 @@
"""Backend package for the GovOPlaN REST connector."""

View File

@@ -0,0 +1,34 @@
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from typing import Any, Literal, Protocol
RestFunctionVisibility = Literal["internal", "authenticated", "public"]
@dataclass(frozen=True, slots=True)
class RestFunctionDescriptor:
id: str
module_id: str
name: str
summary: str
method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = "POST"
path: str | None = None
visibility: RestFunctionVisibility = "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 RestFunctionProvider(Protocol):
def rest_functions(self) -> Sequence[RestFunctionDescriptor]:
...
def invoke_rest_function(self, function_id: str, payload: Mapping[str, Any], context: Mapping[str, Any]) -> Mapping[str, Any]:
...
CAPABILITY_REST_FUNCTION_PROVIDER = "rest.functionProvider"

View 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
REST_READ_SCOPE = "rest:endpoint:read"
REST_READ_SCOPES = (REST_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="REST connector",
level="system",
module_id=module_id,
resource=resource,
action=action,
)
def _route_factory(context: ModuleContext):
del context
from govoplan_rest.backend.router import router
return router
manifest = ModuleManifest(
id="rest",
name="REST Connector",
version="0.1.7",
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
optional_dependencies=("audit", "docs", "policy"),
provides_interfaces=(
ModuleInterfaceProvider(name="rest.functionPublication", version="0.1.0"),
),
permissions=(
_permission(REST_READ_SCOPE, "View REST connector", "Read REST connector status and published function metadata."),
),
role_templates=(
RoleTemplate(
slug="rest_connector_reader",
name="REST connector reader",
description="Read REST connector diagnostics and published endpoint metadata.",
permissions=(REST_READ_SCOPE,),
level="system",
),
),
route_factory=_route_factory,
)
def get_manifest() -> ModuleManifest:
return manifest

View 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_rest.backend.manifest import REST_READ_SCOPES
router = APIRouter(prefix="/rest", tags=["rest"])
@router.get("/status", status_code=status.HTTP_200_OK)
def rest_status(
request: Request,
principal: ApiPrincipal = Depends(require_any_scope(*REST_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": "rest",
"status": "ok",
"module_count": module_count,
"published_function_count": 0,
}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,41 @@
from __future__ import annotations
import unittest
from govoplan_core.core.modules import ModuleContext
from govoplan_rest.backend.contracts import RestFunctionDescriptor
from govoplan_rest.backend.manifest import get_manifest
class RestModuleContractTests(unittest.TestCase):
def test_manifest_declares_rest_module(self) -> None:
manifest = get_manifest()
self.assertEqual("rest", 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("/rest", router.prefix)
self.assertEqual(1, len(router.routes))
def test_function_descriptor_is_stable(self) -> None:
descriptor = RestFunctionDescriptor(
id="demo.ping",
module_id="demo",
name="Ping",
summary="Demo function",
)
self.assertEqual("POST", descriptor.method)
self.assertEqual("authenticated", descriptor.visibility)
self.assertEqual((), descriptor.required_scopes)
if __name__ == "__main__":
unittest.main()