Coverage for src / lexigram / contracts / notification / inbox.py: 97%
30 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
1"""Inbox notification contracts — model and store protocol.
3Defines the canonical ``InboxMessage`` value object and the
4``InboxStoreProtocol`` that all inbox backends must satisfy.
5"""
7from __future__ import annotations
9from dataclasses import dataclass, field
10from datetime import UTC, datetime
11from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
13if TYPE_CHECKING:
14 from lexigram.contracts.core import HealthCheckResult
15import uuid
17INBOX_SENT_HOOK = "notification.inbox.sent"
20@dataclass(frozen=True)
21class InboxMessage:
22 """A persisted inbox notification for a single user.
24 Attributes:
25 id: Unique message identifier (UUID4 string).
26 user_id: Recipient user ID.
27 title: Short display title shown in notification lists.
28 body: Full notification body text.
29 read: Whether the recipient has read this message.
30 created_at: UTC timestamp of creation.
31 metadata: Opaque key-value payload for application-level use.
32 """
34 id: str
35 user_id: str
36 title: str
37 body: str
38 read: bool = False
39 created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
40 metadata: dict[str, Any] = field(default_factory=dict)
42 @classmethod
43 def create(
44 cls,
45 user_id: str,
46 title: str,
47 body: str,
48 *,
49 metadata: dict[str, Any] | None = None,
50 ) -> InboxMessage:
51 """Factory that auto-generates ``id`` and ``created_at``.
53 Args:
54 user_id: Recipient user ID.
55 title: Short notification title.
56 body: Full notification body.
57 metadata: Optional key-value payload.
59 Returns:
60 A new unsaved :class:`InboxMessage` instance.
61 """
62 return cls(
63 id=str(uuid.uuid4()),
64 user_id=user_id,
65 title=title,
66 body=body,
67 metadata=metadata or {},
68 )
71@runtime_checkable
72class InboxStoreProtocol(Protocol):
73 """Structural protocol for inbox message persistence backends.
75 All async methods are I/O-bound; implementations must not block the
76 event loop. Callers may use either :class:`InMemoryInboxStore` (for
77 tests or simple deployments) or :class:`DatabaseInboxStore` (for
78 production SQL-backed storage).
79 """
81 async def save(self, message: InboxMessage) -> None:
82 """Persist *message*.
84 Args:
85 message: The inbox message to store.
86 """
87 ...
89 async def get(self, message_id: str) -> InboxMessage | None:
90 """Return the message with *message_id*, or ``None`` if not found.
92 Args:
93 message_id: The unique message ID to look up.
94 """
95 ...
97 async def list_for_user(
98 self,
99 user_id: str,
100 *,
101 unread_only: bool = False,
102 ) -> list[InboxMessage]:
103 """Return messages for *user_id* in reverse-chronological order.
105 Args:
106 user_id: Filter to this user.
107 unread_only: When ``True`` only unread messages are returned.
108 """
109 ...
111 async def mark_read(self, message_id: str, user_id: str) -> None:
112 """Mark *message_id* as read, guarded by *user_id* ownership.
114 Args:
115 message_id: ID of the message to mark read.
116 user_id: Owner of the message (for authorisation).
117 """
118 ...
120 async def mark_all_read(self, user_id: str) -> None:
121 """Mark every unread message for *user_id* as read.
123 Args:
124 user_id: Target user.
125 """
126 ...
128 async def delete(self, message_id: str, user_id: str) -> None:
129 """Delete *message_id*, guarded by *user_id* ownership.
131 Args:
132 message_id: ID of the message to remove.
133 user_id: Owner of the message (for authorisation).
134 """
135 ...
137 async def count_unread(self, user_id: str) -> int:
138 """Return the number of unread messages for *user_id*.
140 Args:
141 user_id: Target user.
143 Returns:
144 Unread message count.
145 """
146 ...
148 async def clear_all(self, user_id: str) -> int:
149 """Delete all messages for *user_id*.
151 Args:
152 user_id: Target user.
154 Returns:
155 Number of messages deleted.
156 """
157 ...
159 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
160 """Return backend health for inbox storage.
162 Args:
163 timeout: Maximum seconds to wait for the health probe.
164 """
165 ...
168__all__ = ["INBOX_SENT_HOOK", "InboxMessage", "InboxStoreProtocol"]