Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed4517f469 | ||
|
|
25c4267dc6 |
@@ -18,6 +18,12 @@ instead of silently treating them as available.
|
||||
Draft and retired definitions are excluded from the general provider catalogue.
|
||||
Administration APIs retain their complete revision history.
|
||||
|
||||
Catalogue and unversioned detail reads follow the platform temporal-data
|
||||
context. Valid time answers when a definition applied; the independent
|
||||
recorded cutoff reconstructs what was known then. Exact service references
|
||||
remain exact, and current availability and authorization are revalidated for
|
||||
new launches.
|
||||
|
||||
## Revision And Recovery
|
||||
|
||||
Writes are replay-safe and use optimistic concurrency against the current
|
||||
|
||||
+2
-2
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-services"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
description = "Versioned institutional service definitions for GovOPlaN."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
authors = [{ name = "GovOPlaN" }]
|
||||
dependencies = ["govoplan-core>=0.1.15"]
|
||||
dependencies = ["govoplan-core>=0.1.16"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""GovOPlaN Services module."""
|
||||
|
||||
__version__ = "0.1.15"
|
||||
__version__ = "0.1.16"
|
||||
|
||||
@@ -26,7 +26,7 @@ from govoplan_services.backend.service import RegistryServiceAvailabilityEvaluat
|
||||
|
||||
MODULE_ID = "services"
|
||||
MODULE_NAME = "Services"
|
||||
MODULE_VERSION = "0.1.15"
|
||||
MODULE_VERSION = "0.1.16"
|
||||
READ_SCOPE = "services:definition:read"
|
||||
WRITE_SCOPE = "services:definition:write"
|
||||
ADMIN_SCOPE = "services:definition:admin"
|
||||
@@ -102,7 +102,7 @@ manifest = ModuleManifest(
|
||||
id="services.catalogue",
|
||||
title="Institutional service catalogue",
|
||||
summary="Manage versioned service promises independently from presentation and case handling.",
|
||||
body="Definitions retain audiences, prerequisites, evidence, responsibility, bindings, publication, and explainable availability requirements. Portal and Cases consume exact revisions through capabilities.",
|
||||
body="Definitions retain audiences, prerequisites, evidence, responsibility, bindings, publication, and explainable availability requirements. Catalogue and unversioned detail reads follow the titlebar valid-time and recorded-time selection. Portal and Cases consume exact revisions through capabilities, and current authorization is unchanged.",
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("user", "operator", "module_admin", "product_owner"),
|
||||
|
||||
@@ -12,6 +12,8 @@ from govoplan_core.core.institutional import (
|
||||
ServiceDefinition,
|
||||
TemporalRevision,
|
||||
)
|
||||
from govoplan_core.core.temporal import TemporalDataContext
|
||||
from govoplan_core.db.temporal import apply_temporal_revision_filter
|
||||
from govoplan_services.backend.db.models import ServiceDefinitionRevision
|
||||
|
||||
|
||||
@@ -116,6 +118,7 @@ def get_service_definition(
|
||||
*,
|
||||
service_id: str,
|
||||
revision: str | None = None,
|
||||
temporal_context: TemporalDataContext | None = None,
|
||||
) -> ServiceDefinition | None:
|
||||
tenant_id = _principal_tenant(principal)
|
||||
query = session.query(ServiceDefinitionRevision).filter(
|
||||
@@ -123,7 +126,11 @@ def get_service_definition(
|
||||
ServiceDefinitionRevision.service_id == service_id,
|
||||
)
|
||||
if revision is None:
|
||||
query = query.filter(ServiceDefinitionRevision.superseded_at.is_(None))
|
||||
query = apply_temporal_revision_filter(
|
||||
query,
|
||||
ServiceDefinitionRevision,
|
||||
context=temporal_context,
|
||||
)
|
||||
else:
|
||||
query = query.filter(ServiceDefinitionRevision.revision == revision)
|
||||
row = query.order_by(ServiceDefinitionRevision.recorded_at.desc()).first()
|
||||
@@ -143,7 +150,10 @@ def list_service_definitions(
|
||||
raise ServiceStoreError("Service list limit must be between 1 and 200.")
|
||||
statement = session.query(ServiceDefinitionRevision).filter(
|
||||
ServiceDefinitionRevision.tenant_id == tenant_id,
|
||||
ServiceDefinitionRevision.superseded_at.is_(None),
|
||||
)
|
||||
statement = apply_temporal_revision_filter(
|
||||
statement,
|
||||
ServiceDefinitionRevision,
|
||||
)
|
||||
if publication_states is not None:
|
||||
statement = statement.filter(
|
||||
@@ -180,10 +190,13 @@ class SqlServiceDefinitionProvider:
|
||||
principal,
|
||||
service_id=reference.object_id,
|
||||
revision=reference.version,
|
||||
temporal_context=(
|
||||
TemporalDataContext(validity_mode="at", valid_at=effective_at)
|
||||
if effective_at is not None and reference.version is None
|
||||
else None
|
||||
),
|
||||
)
|
||||
if item is None or (
|
||||
effective_at is not None and not item.temporal.effective_at(effective_at)
|
||||
):
|
||||
if item is None or (effective_at is not None and reference.version is not None and not item.temporal.effective_at(effective_at)):
|
||||
return None
|
||||
return item
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@ from govoplan_core.core.institutional import (
|
||||
ServiceDefinition,
|
||||
TemporalRevision,
|
||||
)
|
||||
from govoplan_core.core.temporal import (
|
||||
TemporalDataContext,
|
||||
bind_temporal_data_context,
|
||||
reset_temporal_data_context,
|
||||
)
|
||||
from govoplan_services.backend.db.models import ServiceDefinitionRevision
|
||||
from govoplan_services.backend.service import (
|
||||
RegistryServiceAvailabilityEvaluator,
|
||||
@@ -97,6 +102,31 @@ class ServiceTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(Exception, "cross tenants"):
|
||||
SqlServiceDefinitionProvider().list_service_definitions(self.session, Principal("tenant-2"), tenant_id="tenant-1")
|
||||
|
||||
def test_read_context_can_reconstruct_recorded_state(self) -> None:
|
||||
record_service_definition(self.session, self.principal, definition=service())
|
||||
record_service_definition(
|
||||
self.session,
|
||||
self.principal,
|
||||
definition=service(revision="2", state="suspended"),
|
||||
expected_revision="1",
|
||||
)
|
||||
token = bind_temporal_data_context(
|
||||
TemporalDataContext(
|
||||
validity_mode="at",
|
||||
valid_at=NOW + timedelta(hours=1),
|
||||
recorded_at=NOW + timedelta(seconds=30),
|
||||
)
|
||||
)
|
||||
try:
|
||||
result = SqlServiceDefinitionProvider().list_service_definitions(
|
||||
self.session,
|
||||
self.principal,
|
||||
tenant_id="tenant-1",
|
||||
)
|
||||
self.assertEqual("1", result[0].temporal.revision if result else None)
|
||||
finally:
|
||||
reset_temporal_data_context(token)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user