Initialize GovOPlaN REST 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/
|
||||||
26
README.md
Normal file
26
README.md
Normal 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
24
pyproject.toml
Normal 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"
|
||||||
5
src/govoplan_rest/__init__.py
Normal file
5
src/govoplan_rest/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""GovOPlaN REST connector module."""
|
||||||
|
|
||||||
|
__all__ = ["__version__"]
|
||||||
|
|
||||||
|
__version__ = "0.1.7"
|
||||||
1
src/govoplan_rest/backend/__init__.py
Normal file
1
src/govoplan_rest/backend/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Backend package for the GovOPlaN REST connector."""
|
||||||
34
src/govoplan_rest/backend/contracts.py
Normal file
34
src/govoplan_rest/backend/contracts.py
Normal 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"
|
||||||
57
src/govoplan_rest/backend/manifest.py
Normal file
57
src/govoplan_rest/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
|
||||||
|
|
||||||
|
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
|
||||||
28
src/govoplan_rest/backend/router.py
Normal file
28
src/govoplan_rest/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_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,
|
||||||
|
}
|
||||||
1
src/govoplan_rest/py.typed
Normal file
1
src/govoplan_rest/py.typed
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
41
tests/test_rest_module_contract.py
Normal file
41
tests/test_rest_module_contract.py
Normal 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()
|
||||||
Reference in New Issue
Block a user