Coverage for src / lexigram / contracts / webhook / protocols.py: 0%

21 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-15 18:57 +0800

1"""Webhook protocols: subscription store, delivery store, delivery service.""" 

2 

3from __future__ import annotations 

4 

5from datetime import datetime 

6from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable 

7 

8if TYPE_CHECKING: 

9 from lexigram.contracts.webhook.types import ( 

10 DeliveryAttempt, 

11 DeliveryStatus, 

12 WebhookEvent, 

13 WebhookSubscription, 

14 ) 

15 

16 

17@runtime_checkable 

18class WebhookSubscriptionStoreProtocol(Protocol): 

19 """Storage-agnostic CRUD for webhook subscriptions.""" 

20 

21 async def create(self, subscription: WebhookSubscription) -> None: 

22 """Persist a new subscription. 

23 

24 Args: 

25 subscription: Subscription to store. 

26 """ 

27 ... 

28 

29 async def get(self, subscription_id: str) -> WebhookSubscription | None: 

30 """Return subscription by ID, or None. 

31 

32 Args: 

33 subscription_id: UUID to look up. 

34 

35 Returns: 

36 The subscription if found, else None. 

37 """ 

38 ... 

39 

40 async def list( 

41 self, 

42 *, 

43 active_only: bool = True, 

44 event_type: str | None = None, 

45 tenant_id: str | None = None, 

46 limit: int = 100, 

47 offset: int = 0, 

48 ) -> list[WebhookSubscription]: 

49 """List subscriptions matching filters. 

50 

51 Args: 

52 active_only: Only return active subscriptions. 

53 event_type: Filter to subscriptions that handle this event type. 

54 tenant_id: Filter by tenant. 

55 limit: Maximum results. 

56 offset: Results to skip. 

57 

58 Returns: 

59 Filtered and paginated subscriptions. 

60 """ 

61 ... 

62 

63 async def update(self, subscription: WebhookSubscription) -> None: 

64 """Update an existing subscription. 

65 

66 Args: 

67 subscription: Updated subscription data. 

68 """ 

69 ... 

70 

71 async def delete(self, subscription_id: str) -> None: 

72 """Remove a subscription. 

73 

74 Args: 

75 subscription_id: UUID to remove. 

76 """ 

77 ... 

78 

79 

80@runtime_checkable 

81class WebhookDeliveryStoreProtocol(Protocol): 

82 """Storage for webhook delivery attempts and dead-letter queue.""" 

83 

84 async def record_attempt(self, attempt: DeliveryAttempt) -> None: 

85 """Persist a delivery attempt record. 

86 

87 Args: 

88 attempt: Attempt to record. 

89 """ 

90 ... 

91 

92 async def get_attempts( 

93 self, 

94 *, 

95 subscription_id: str | None = None, 

96 event_id: str | None = None, 

97 status: DeliveryStatus | None = None, 

98 limit: int = 100, 

99 offset: int = 0, 

100 ) -> list[DeliveryAttempt]: 

101 """Query delivery attempts matching filters, newest-first. 

102 

103 Args: 

104 subscription_id: Filter by subscription. 

105 event_id: Filter by event. 

106 status: Filter by delivery status. 

107 limit: Maximum results. 

108 offset: Results to skip. 

109 

110 Returns: 

111 Matching attempts in newest-first order. 

112 """ 

113 ... 

114 

115 async def get_dead_letters( 

116 self, 

117 *, 

118 subscription_id: str | None = None, 

119 limit: int = 100, 

120 offset: int = 0, 

121 ) -> list[DeliveryAttempt]: 

122 """Return attempts in dead-letter status. 

123 

124 Args: 

125 subscription_id: Optional filter by subscription. 

126 limit: Maximum results. 

127 offset: Results to skip. 

128 

129 Returns: 

130 Dead-lettered attempts. 

131 """ 

132 ... 

133 

134 async def count_recent_failures( 

135 self, 

136 subscription_id: str, 

137 since: datetime, 

138 ) -> int: 

139 """Count failed attempts for a subscription since a given time. 

140 

141 Args: 

142 subscription_id: Subscription to check. 

143 since: Count attempts after this timestamp. 

144 

145 Returns: 

146 Count of failed attempts. 

147 """ 

148 ... 

149 

150 

151@runtime_checkable 

152class WebhookDeliveryServiceProtocol(Protocol): 

153 """High-level protocol for dispatching webhook events.""" 

154 

155 async def dispatch(self, event: WebhookEvent) -> None: 

156 """Deliver an event to all matching active subscriptions. 

157 

158 Args: 

159 event: Event to deliver. 

160 """ 

161 ... 

162 

163 async def redeliver(self, attempt_id: str) -> Any: 

164 """Redeliver a failed or dead-lettered attempt. 

165 

166 Returns ``Result[None, WebhookError]``. Typed as ``Any`` because 

167 ``Result`` lives in ``lexigram`` (not ``lexigram-contracts``). 

168 

169 Args: 

170 attempt_id: ID of the attempt to redeliver. 

171 

172 Returns: 

173 Result[None, WebhookError] 

174 """ 

175 ... 

176 

177 

178__all__ = [ 

179 "WebhookDeliveryServiceProtocol", 

180 "WebhookDeliveryStoreProtocol", 

181 "WebhookSubscriptionStoreProtocol", 

182]