Add contact point resolution contract
This commit is contained in:
@@ -286,7 +286,8 @@ such as `calendar:sync-source:<id>`.
|
|||||||
Current named interfaces, generated from the source manifests by the workspace
|
Current named interfaces, generated from the source manifests by the workspace
|
||||||
contract checks, are:
|
contract checks, are:
|
||||||
|
|
||||||
- `addresses.contact_writer`, `addresses.lookup`, `addresses.recipient_source`
|
- `addresses.contact_point_resolution`, `addresses.contact_writer`,
|
||||||
|
`addresses.lookup`, `addresses.recipient_source`
|
||||||
- `calendar.external_profiles`, `calendar.invitations`, `calendar.outbox`,
|
- `calendar.external_profiles`, `calendar.invitations`, `calendar.outbox`,
|
||||||
`calendar.scheduling`
|
`calendar.scheduling`
|
||||||
- `campaigns.access`, `campaigns.delivery_tasks`,
|
- `campaigns.access`, `campaigns.delivery_tasks`,
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
from govoplan_core.core.distribution_lists import (
|
||||||
|
DistributionChannel,
|
||||||
|
DistributionExplanation,
|
||||||
|
DistributionOutcome,
|
||||||
|
DistributionSourceReference,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION = "addresses.contact_point_resolution"
|
||||||
|
CONTACT_POINT_CONTRACT_VERSION = "1.0"
|
||||||
|
|
||||||
|
ContactPointFallbackRule = Literal["none", "primary", "any"]
|
||||||
|
PostalAddressFormat = Literal["domestic", "international"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ContactPointResolutionRequest:
|
||||||
|
tenant_id: str
|
||||||
|
subject: DistributionSourceReference
|
||||||
|
effective_at: datetime
|
||||||
|
purpose: str | None = None
|
||||||
|
requested_channels: tuple[DistributionChannel, ...] = ()
|
||||||
|
address_purpose: str | None = None
|
||||||
|
fallback_rule: ContactPointFallbackRule = "primary"
|
||||||
|
locale: str | None = None
|
||||||
|
postal_format: PostalAddressFormat = "domestic"
|
||||||
|
context: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ContactPointCandidate:
|
||||||
|
channel: DistributionChannel
|
||||||
|
target: str
|
||||||
|
target_key: str
|
||||||
|
status: DistributionOutcome
|
||||||
|
contact_point_id: str | None = None
|
||||||
|
address_purpose: str | None = None
|
||||||
|
locale: str | None = None
|
||||||
|
preferred: bool = False
|
||||||
|
preference_rank: int | None = None
|
||||||
|
reason_code: str | None = None
|
||||||
|
explanation: str | None = None
|
||||||
|
source: DistributionSourceReference | None = None
|
||||||
|
source_revision: str | None = None
|
||||||
|
preference_revision: str | None = None
|
||||||
|
consent_revision: str | None = None
|
||||||
|
value: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ContactPointResolution:
|
||||||
|
contract_version: str
|
||||||
|
subject: DistributionSourceReference
|
||||||
|
status: DistributionOutcome
|
||||||
|
contact_id: str | None = None
|
||||||
|
display_name: str | None = None
|
||||||
|
candidates: tuple[ContactPointCandidate, ...] = ()
|
||||||
|
excluded: tuple[ContactPointCandidate, ...] = ()
|
||||||
|
explanations: tuple[DistributionExplanation, ...] = ()
|
||||||
|
source_revision: str | None = None
|
||||||
|
source_fingerprint: str | None = None
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ContactPointSourceRequest:
|
||||||
|
tenant_id: str
|
||||||
|
source_id: str
|
||||||
|
effective_at: datetime
|
||||||
|
purpose: str | None = None
|
||||||
|
requested_channels: tuple[DistributionChannel, ...] = ()
|
||||||
|
address_purpose: str | None = None
|
||||||
|
fallback_rule: ContactPointFallbackRule = "primary"
|
||||||
|
locale: str | None = None
|
||||||
|
postal_format: PostalAddressFormat = "domestic"
|
||||||
|
max_items: int = 5_000
|
||||||
|
context: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ContactPointSourcePreview:
|
||||||
|
contract_version: str
|
||||||
|
source: DistributionSourceReference
|
||||||
|
request: ContactPointSourceRequest
|
||||||
|
resolutions: tuple[ContactPointResolution, ...]
|
||||||
|
total_count: int
|
||||||
|
usable_count: int
|
||||||
|
excluded_count: int
|
||||||
|
offset: int
|
||||||
|
limit: int
|
||||||
|
has_more: bool
|
||||||
|
source_revision: str
|
||||||
|
source_fingerprint: str
|
||||||
|
generated_at: datetime
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ContactPointSnapshotRef:
|
||||||
|
id: str
|
||||||
|
tenant_id: str
|
||||||
|
contract_version: str
|
||||||
|
source: DistributionSourceReference
|
||||||
|
request: ContactPointSourceRequest
|
||||||
|
resolutions: tuple[ContactPointResolution, ...]
|
||||||
|
recipient_count: int
|
||||||
|
excluded_count: int
|
||||||
|
source_revision: str
|
||||||
|
source_fingerprint: str
|
||||||
|
snapshot_hash: str
|
||||||
|
generated_at: datetime
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class ContactPointResolutionProvider(Protocol):
|
||||||
|
def resolve_contact_points(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: ContactPointResolutionRequest,
|
||||||
|
) -> ContactPointResolution:
|
||||||
|
...
|
||||||
|
|
||||||
|
def preview_source(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: ContactPointSourceRequest,
|
||||||
|
offset: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
) -> ContactPointSourcePreview:
|
||||||
|
...
|
||||||
|
|
||||||
|
def freeze_source(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: ContactPointSourceRequest,
|
||||||
|
) -> ContactPointSnapshotRef:
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_snapshot(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
snapshot_id: str,
|
||||||
|
) -> ContactPointSnapshotRef | None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def contact_point_resolution_provider(
|
||||||
|
registry: object | None,
|
||||||
|
) -> ContactPointResolutionProvider | None:
|
||||||
|
if (
|
||||||
|
registry is None
|
||||||
|
or not hasattr(registry, "has_capability")
|
||||||
|
or not hasattr(registry, "capability")
|
||||||
|
or not registry.has_capability(CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION)
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
capability = registry.capability(CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION)
|
||||||
|
return capability if isinstance(capability, ContactPointResolutionProvider) else None
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION",
|
||||||
|
"CONTACT_POINT_CONTRACT_VERSION",
|
||||||
|
"ContactPointCandidate",
|
||||||
|
"ContactPointFallbackRule",
|
||||||
|
"ContactPointResolution",
|
||||||
|
"ContactPointResolutionProvider",
|
||||||
|
"ContactPointResolutionRequest",
|
||||||
|
"ContactPointSnapshotRef",
|
||||||
|
"ContactPointSourcePreview",
|
||||||
|
"ContactPointSourceRequest",
|
||||||
|
"PostalAddressFormat",
|
||||||
|
"contact_point_resolution_provider",
|
||||||
|
]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from govoplan_core.core.contact_points import (
|
||||||
|
CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION,
|
||||||
|
CONTACT_POINT_CONTRACT_VERSION,
|
||||||
|
ContactPointResolutionRequest,
|
||||||
|
contact_point_resolution_provider,
|
||||||
|
)
|
||||||
|
from govoplan_core.core.distribution_lists import DistributionSourceReference
|
||||||
|
|
||||||
|
|
||||||
|
class _Registry:
|
||||||
|
def __init__(self, capability: object | None = None) -> None:
|
||||||
|
self._capability = capability
|
||||||
|
|
||||||
|
def has_capability(self, name: str) -> bool:
|
||||||
|
return (
|
||||||
|
name == CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION
|
||||||
|
and self._capability is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
def capability(self, _name: str) -> object:
|
||||||
|
return self._capability
|
||||||
|
|
||||||
|
|
||||||
|
class ContactPointContractTests(unittest.TestCase):
|
||||||
|
def test_request_is_provider_neutral_and_versioned(self) -> None:
|
||||||
|
request = ContactPointResolutionRequest(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
subject=DistributionSourceReference(
|
||||||
|
provider="idm",
|
||||||
|
resource_type="identity",
|
||||||
|
resource_id="identity-1",
|
||||||
|
),
|
||||||
|
effective_at=datetime(2026, 8, 2, tzinfo=UTC),
|
||||||
|
requested_channels=("email", "postal"),
|
||||||
|
address_purpose="official",
|
||||||
|
fallback_rule="primary",
|
||||||
|
postal_format="international",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("1.0", CONTACT_POINT_CONTRACT_VERSION)
|
||||||
|
self.assertEqual("idm", request.subject.provider)
|
||||||
|
self.assertEqual(("email", "postal"), request.requested_channels)
|
||||||
|
|
||||||
|
def test_accessor_rejects_objects_that_do_not_implement_the_protocol(self) -> None:
|
||||||
|
self.assertIsNone(contact_point_resolution_provider(None))
|
||||||
|
self.assertIsNone(contact_point_resolution_provider(_Registry(object())))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user