3 Commits
Author SHA1 Message Date
zemion 33deec41ad Release v0.1.17
Module Package Release / publish-packages (push) Successful in 10s
2026-08-05 20:34:07 +02:00
zemion f65cb36324 Release v0.1.16
Module Package Release / publish-packages (push) Successful in 11s
2026-08-05 19:52:12 +02:00
zemion 10ea54f38d feat: honor temporal read context 2026-08-05 00:03:32 +02:00
6 changed files with 60 additions and 14 deletions
+5
View File
@@ -9,6 +9,11 @@ external subject. It belongs to an exact case, workflow, or decision context.
Contact delivery uses frozen snapshot references so later address changes do Contact delivery uses frozen snapshot references so later address changes do
not rewrite evidence. Preferred channels must be permitted by the procedure. not rewrite evidence. Preferred channels must be permitted by the procedure.
Unversioned reads follow the platform temporal-data context. An explicit
procedure-resolution instant takes precedence over the titlebar valid-time
choice. Recorded-time reconstruction remains separate from real-world
validity, while permissions and new delivery actions use current state.
## Representation ## Representation
Representation powers identify representative and represented Parties, an Representation powers identify representative and represented Parties, an
+2 -2
View File
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-parties" name = "govoplan-parties"
version = "0.1.15" version = "0.1.17"
description = "Procedure-local party and representation lifecycle for GovOPlaN." description = "Procedure-local party and representation lifecycle 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 Parties module.""" """GovOPlaN Parties module."""
__version__ = "0.1.15" __version__ = "0.1.17"
+2 -2
View File
@@ -13,7 +13,7 @@ from govoplan_parties.backend.service import SqlPartyResolver
MODULE_ID = "parties" MODULE_ID = "parties"
MODULE_NAME = "Parties" MODULE_NAME = "Parties"
MODULE_VERSION = "0.1.15" MODULE_VERSION = "0.1.17"
READ_SCOPE = "parties:procedure:read" READ_SCOPE = "parties:procedure:read"
WRITE_SCOPE = "parties:procedure:write" WRITE_SCOPE = "parties:procedure:write"
ADMIN_SCOPE = "parties:procedure:admin" ADMIN_SCOPE = "parties:procedure:admin"
@@ -66,7 +66,7 @@ manifest = ModuleManifest(
id="parties.procedure-and-representation", id="parties.procedure-and-representation",
title="Procedure parties and representation", title="Procedure parties and representation",
summary="Keep procedure roles and authority distinct from subject master data.", summary="Keep procedure roles and authority distinct from subject master data.",
body="Parties stores immutable procedure participation and explicit representation powers. Powers cannot disappear silently and delivery uses frozen contact snapshots.", body="Parties stores immutable procedure participation and explicit representation powers. Powers cannot disappear silently and delivery uses frozen contact snapshots. Unversioned reads follow the titlebar valid-time and recorded-time selection; explicit procedure resolution times take precedence and current authorization is unchanged.",
layer="configured", layer="configured",
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
audience=("user", "operator", "module_admin", "auditor"), audience=("user", "operator", "module_admin", "auditor"),
+13 -8
View File
@@ -3,7 +3,6 @@ from __future__ import annotations
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any, Mapping, Sequence from typing import Any, Mapping, Sequence
from sqlalchemy import or_
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from govoplan_core.core.institutional import ( from govoplan_core.core.institutional import (
@@ -15,6 +14,8 @@ from govoplan_core.core.institutional import (
revise_procedure_party, revise_procedure_party,
revoke_party_representation, revoke_party_representation,
) )
from govoplan_core.core.temporal import TemporalDataContext
from govoplan_core.db.temporal import apply_temporal_revision_filter
from govoplan_parties.backend.db.models import ProcedurePartyRevision from govoplan_parties.backend.db.models import ProcedurePartyRevision
@@ -123,7 +124,7 @@ def get_procedure_party(
ProcedurePartyRevision.party_id == party_id, ProcedurePartyRevision.party_id == party_id,
) )
if revision is None: if revision is None:
query = query.filter(ProcedurePartyRevision.superseded_at.is_(None)) query = apply_temporal_revision_filter(query, ProcedurePartyRevision)
else: else:
query = query.filter(ProcedurePartyRevision.revision == revision) query = query.filter(ProcedurePartyRevision.revision == revision)
row = query.order_by(ProcedurePartyRevision.recorded_at.desc()).first() row = query.order_by(ProcedurePartyRevision.recorded_at.desc()).first()
@@ -151,13 +152,17 @@ class SqlPartyResolver:
ProcedurePartyRevision.procedure_owner_module == procedure_ref.owner_module, ProcedurePartyRevision.procedure_owner_module == procedure_ref.owner_module,
ProcedurePartyRevision.procedure_id == procedure_ref.object_id, ProcedurePartyRevision.procedure_id == procedure_ref.object_id,
) )
if effective_at is None: context = None
query = query.filter(ProcedurePartyRevision.superseded_at.is_(None)) if effective_at is not None:
else: context = TemporalDataContext(
query = query.filter( validity_mode="at",
or_(ProcedurePartyRevision.valid_from.is_(None), ProcedurePartyRevision.valid_from <= effective_at), valid_at=effective_at,
or_(ProcedurePartyRevision.valid_to.is_(None), ProcedurePartyRevision.valid_to > effective_at),
) )
query = apply_temporal_revision_filter(
query,
ProcedurePartyRevision,
context=context,
)
rows = query.order_by( rows = query.order_by(
ProcedurePartyRevision.party_id.asc(), ProcedurePartyRevision.party_id.asc(),
ProcedurePartyRevision.recorded_at.desc(), ProcedurePartyRevision.recorded_at.desc(),
+37 -1
View File
@@ -15,8 +15,18 @@ from govoplan_core.core.institutional import (
ProcedureParty, ProcedureParty,
TemporalRevision, TemporalRevision,
) )
from govoplan_core.core.temporal import (
TemporalDataContext,
bind_temporal_data_context,
reset_temporal_data_context,
)
from govoplan_parties.backend.db.models import ProcedurePartyRevision from govoplan_parties.backend.db.models import ProcedurePartyRevision
from govoplan_parties.backend.service import PartyStoreError, SqlPartyResolver, record_procedure_party from govoplan_parties.backend.service import (
PartyStoreError,
SqlPartyResolver,
get_procedure_party,
record_procedure_party,
)
NOW = datetime(2026, 8, 1, 11, 0, tzinfo=UTC) NOW = datetime(2026, 8, 1, 11, 0, tzinfo=UTC)
@@ -87,6 +97,32 @@ class PartyTests(unittest.TestCase):
with self.assertRaisesRegex(Exception, "same-tenant"): with self.assertRaisesRegex(Exception, "same-tenant"):
SqlPartyResolver().list_procedure_parties(self.session, Principal("tenant-2"), procedure_ref=ref("case", "case-1", "cases")) SqlPartyResolver().list_procedure_parties(self.session, Principal("tenant-2"), procedure_ref=ref("case", "case-1", "cases"))
def test_read_context_can_reconstruct_recorded_state(self) -> None:
record_procedure_party(self.session, self.principal, party=party())
revoked = representation(revision="2", revoked_at=NOW + timedelta(hours=1))
record_procedure_party(
self.session,
self.principal,
party=party(revision="2", representations=(revoked,)),
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 = get_procedure_party(
self.session,
self.principal,
party_id="representative",
)
self.assertEqual("1", result.temporal.revision if result else None)
finally:
reset_temporal_data_context(token)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()