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

1"""Inbox notification contracts — model and store protocol. 

2 

3Defines the canonical ``InboxMessage`` value object and the 

4``InboxStoreProtocol`` that all inbox backends must satisfy. 

5""" 

6 

7from __future__ import annotations 

8 

9from dataclasses import dataclass, field 

10from datetime import UTC, datetime 

11from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable 

12 

13if TYPE_CHECKING: 

14 from lexigram.contracts.core import HealthCheckResult 

15import uuid 

16 

17INBOX_SENT_HOOK = "notification.inbox.sent" 

18 

19 

20@dataclass(frozen=True) 

21class InboxMessage: 

22 """A persisted inbox notification for a single user. 

23 

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 """ 

33 

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) 

41 

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``. 

52 

53 Args: 

54 user_id: Recipient user ID. 

55 title: Short notification title. 

56 body: Full notification body. 

57 metadata: Optional key-value payload. 

58 

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 ) 

69 

70 

71@runtime_checkable 

72class InboxStoreProtocol(Protocol): 

73 """Structural protocol for inbox message persistence backends. 

74 

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 """ 

80 

81 async def save(self, message: InboxMessage) -> None: 

82 """Persist *message*. 

83 

84 Args: 

85 message: The inbox message to store. 

86 """ 

87 ... 

88 

89 async def get(self, message_id: str) -> InboxMessage | None: 

90 """Return the message with *message_id*, or ``None`` if not found. 

91 

92 Args: 

93 message_id: The unique message ID to look up. 

94 """ 

95 ... 

96 

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. 

104 

105 Args: 

106 user_id: Filter to this user. 

107 unread_only: When ``True`` only unread messages are returned. 

108 """ 

109 ... 

110 

111 async def mark_read(self, message_id: str, user_id: str) -> None: 

112 """Mark *message_id* as read, guarded by *user_id* ownership. 

113 

114 Args: 

115 message_id: ID of the message to mark read. 

116 user_id: Owner of the message (for authorisation). 

117 """ 

118 ... 

119 

120 async def mark_all_read(self, user_id: str) -> None: 

121 """Mark every unread message for *user_id* as read. 

122 

123 Args: 

124 user_id: Target user. 

125 """ 

126 ... 

127 

128 async def delete(self, message_id: str, user_id: str) -> None: 

129 """Delete *message_id*, guarded by *user_id* ownership. 

130 

131 Args: 

132 message_id: ID of the message to remove. 

133 user_id: Owner of the message (for authorisation). 

134 """ 

135 ... 

136 

137 async def count_unread(self, user_id: str) -> int: 

138 """Return the number of unread messages for *user_id*. 

139 

140 Args: 

141 user_id: Target user. 

142 

143 Returns: 

144 Unread message count. 

145 """ 

146 ... 

147 

148 async def clear_all(self, user_id: str) -> int: 

149 """Delete all messages for *user_id*. 

150 

151 Args: 

152 user_id: Target user. 

153 

154 Returns: 

155 Number of messages deleted. 

156 """ 

157 ... 

158 

159 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult: 

160 """Return backend health for inbox storage. 

161 

162 Args: 

163 timeout: Maximum seconds to wait for the health probe. 

164 """ 

165 ... 

166 

167 

168__all__ = ["INBOX_SENT_HOOK", "InboxMessage", "InboxStoreProtocol"]