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