Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
468c7bb910 | ||
|
|
8c901416b4 | ||
|
|
ed4517f469 | ||
|
|
25c4267dc6 |
@@ -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
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-services"
|
name = "govoplan-services"
|
||||||
version = "0.1.15"
|
version = "0.1.18"
|
||||||
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.18"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""GovOPlaN Services module."""
|
"""GovOPlaN Services module."""
|
||||||
|
|
||||||
__version__ = "0.1.15"
|
__version__ = "0.1.18"
|
||||||
|
|||||||
@@ -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.18"
|
||||||
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"),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user