3 Commits
Author SHA1 Message Date
zemion 8c901416b4 Release v0.1.17
Module Package Release / publish-packages (push) Successful in 11s
2026-08-05 20:34:10 +02:00
zemion ed4517f469 Release v0.1.16
Module Package Release / publish-packages (push) Successful in 10s
2026-08-05 19:52:13 +02:00
zemion 25c4267dc6 feat: honor temporal read context 2026-08-05 00:03:32 +02:00
6 changed files with 59 additions and 10 deletions
+6
View File
@@ -18,6 +18,12 @@ instead of silently treating them as available.
Draft and retired definitions are excluded from the general provider catalogue. Draft and retired definitions are excluded from the general provider catalogue.
Administration APIs retain their complete revision history. 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 ## Revision And Recovery
Writes are replay-safe and use optimistic concurrency against the current Writes are replay-safe and use optimistic concurrency against the current
+2 -2
View File
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-services" name = "govoplan-services"
version = "0.1.15" version = "0.1.17"
description = "Versioned institutional service definitions for GovOPlaN." description = "Versioned institutional service definitions for GovOPlaN."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }] authors = [{ name = "GovOPlaN" }]
dependencies = ["govoplan-core>=0.1.15"] dependencies = ["govoplan-core>=0.1.17"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
+1 -1
View File
@@ -1,3 +1,3 @@
"""GovOPlaN Services module.""" """GovOPlaN Services module."""
__version__ = "0.1.15" __version__ = "0.1.17"
+2 -2
View File
@@ -26,7 +26,7 @@ from govoplan_services.backend.service import RegistryServiceAvailabilityEvaluat
MODULE_ID = "services" MODULE_ID = "services"
MODULE_NAME = "Services" MODULE_NAME = "Services"
MODULE_VERSION = "0.1.15" MODULE_VERSION = "0.1.17"
READ_SCOPE = "services:definition:read" READ_SCOPE = "services:definition:read"
WRITE_SCOPE = "services:definition:write" WRITE_SCOPE = "services:definition:write"
ADMIN_SCOPE = "services:definition:admin" ADMIN_SCOPE = "services:definition:admin"
@@ -102,7 +102,7 @@ manifest = ModuleManifest(
id="services.catalogue", id="services.catalogue",
title="Institutional service catalogue", title="Institutional service catalogue",
summary="Manage versioned service promises independently from presentation and case handling.", 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", layer="configured",
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
audience=("user", "operator", "module_admin", "product_owner"), audience=("user", "operator", "module_admin", "product_owner"),
+18 -5
View File
@@ -12,6 +12,8 @@ from govoplan_core.core.institutional import (
ServiceDefinition, ServiceDefinition,
TemporalRevision, 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 from govoplan_services.backend.db.models import ServiceDefinitionRevision
@@ -116,6 +118,7 @@ def get_service_definition(
*, *,
service_id: str, service_id: str,
revision: str | None = None, revision: str | None = None,
temporal_context: TemporalDataContext | None = None,
) -> ServiceDefinition | None: ) -> ServiceDefinition | None:
tenant_id = _principal_tenant(principal) tenant_id = _principal_tenant(principal)
query = session.query(ServiceDefinitionRevision).filter( query = session.query(ServiceDefinitionRevision).filter(
@@ -123,7 +126,11 @@ def get_service_definition(
ServiceDefinitionRevision.service_id == service_id, ServiceDefinitionRevision.service_id == service_id,
) )
if revision is None: if revision is None:
query = query.filter(ServiceDefinitionRevision.superseded_at.is_(None)) query = apply_temporal_revision_filter(
query,
ServiceDefinitionRevision,
context=temporal_context,
)
else: else:
query = query.filter(ServiceDefinitionRevision.revision == revision) query = query.filter(ServiceDefinitionRevision.revision == revision)
row = query.order_by(ServiceDefinitionRevision.recorded_at.desc()).first() 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.") raise ServiceStoreError("Service list limit must be between 1 and 200.")
statement = session.query(ServiceDefinitionRevision).filter( statement = session.query(ServiceDefinitionRevision).filter(
ServiceDefinitionRevision.tenant_id == tenant_id, ServiceDefinitionRevision.tenant_id == tenant_id,
ServiceDefinitionRevision.superseded_at.is_(None), )
statement = apply_temporal_revision_filter(
statement,
ServiceDefinitionRevision,
) )
if publication_states is not None: if publication_states is not None:
statement = statement.filter( statement = statement.filter(
@@ -180,10 +190,13 @@ class SqlServiceDefinitionProvider:
principal, principal,
service_id=reference.object_id, service_id=reference.object_id,
revision=reference.version, 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 ( if item is None or (effective_at is not None and reference.version is not None and not item.temporal.effective_at(effective_at)):
effective_at is not None and not item.temporal.effective_at(effective_at)
):
return None return None
return item return item
+30
View File
@@ -13,6 +13,11 @@ from govoplan_core.core.institutional import (
ServiceDefinition, ServiceDefinition,
TemporalRevision, 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.db.models import ServiceDefinitionRevision
from govoplan_services.backend.service import ( from govoplan_services.backend.service import (
RegistryServiceAvailabilityEvaluator, RegistryServiceAvailabilityEvaluator,
@@ -97,6 +102,31 @@ class ServiceTests(unittest.TestCase):
with self.assertRaisesRegex(Exception, "cross tenants"): with self.assertRaisesRegex(Exception, "cross tenants"):
SqlServiceDefinitionProvider().list_service_definitions(self.session, Principal("tenant-2"), tenant_id="tenant-1") 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__": if __name__ == "__main__":
unittest.main() unittest.main()