Coverage for src/lexigram/notification/config/push.py: 82%

51 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-26 07:17 +0800

1"""Push notification driver configuration.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass 

6from typing import Any 

7 

8from lexigram.domain import DomainModel 

9from lexigram.notification.constants import ( 

10 DEFAULT_APNS_TIMEOUT, 

11 DEFAULT_FCM_TIMEOUT, 

12) 

13from lexigram.validation import Field, SecretStr, field_validator 

14 

15 

16@dataclass(init=False) 

17class FCMDriverConfig(DomainModel): 

18 """Firebase Cloud Messaging (FCM) push notification configuration.""" 

19 

20 server_key: SecretStr | None = Field( 

21 default=None, 

22 description="FCM Server API Key", 

23 ) 

24 

25 @field_validator("server_key") 

26 @classmethod 

27 def _coerce_server_key(cls, value: Any) -> Any: 

28 if value is None or isinstance(value, SecretStr): 

29 return value 

30 return SecretStr(str(value)) 

31 

32 timeout: int = Field( 

33 default=DEFAULT_FCM_TIMEOUT, 

34 ge=1, 

35 description="HTTP timeout (s)", 

36 ) 

37 

38 

39@dataclass(init=False) 

40class APNsDriverConfig(DomainModel): 

41 """Apple Push Notification service (APNs) configuration.""" 

42 

43 team_id: str | None = Field( 

44 default=None, 

45 description="Apple Developer Team ID (10-character string)", 

46 ) 

47 key_id: str | None = Field( 

48 default=None, 

49 description="APNs Auth Key ID (10-character string)", 

50 ) 

51 apns_auth_key: SecretStr | None = Field( 

52 default=None, 

53 description=( 

54 "ECDSA private key — PEM string starting with " 

55 "'-----BEGIN PRIVATE KEY-----' or path to a .p8 key file" 

56 ), 

57 ) 

58 bundle_id: str | None = Field( 

59 default=None, 

60 description="App bundle identifier (e.g. com.example.MyApp)", 

61 ) 

62 sandbox: bool = Field( 

63 default=False, 

64 description="Use the APNs sandbox endpoint instead of production", 

65 ) 

66 timeout: int = Field( 

67 default=DEFAULT_APNS_TIMEOUT, 

68 ge=1, 

69 description="HTTP timeout (s)", 

70 ) 

71 

72 @field_validator("apns_auth_key") 

73 @classmethod 

74 def _coerce_apns_auth_key(cls, value: Any) -> Any: 

75 if value is None or isinstance(value, SecretStr): 

76 return value 

77 return SecretStr(str(value)) 

78 

79 

80@dataclass(init=False) 

81class WebPushDriverConfig(DomainModel): 

82 """Web Push (RFC 8030) driver configuration.""" 

83 

84 vapid_private_key: SecretStr | None = Field( 

85 default=None, 

86 description="VAPID private key (PEM-encoded EC prime256v1)", 

87 ) 

88 vapid_public_key: str | None = Field( 

89 default=None, 

90 description="VAPID public key (base64url-encoded)", 

91 ) 

92 vapid_claims_subject: str | None = Field( 

93 default=None, 

94 description="VAPID claims subject URI (e.g. mailto:ops@example.com)", 

95 ) 

96 timeout: int = Field( 

97 default=30, 

98 ge=1, 

99 description="HTTP timeout (s)", 

100 ) 

101 

102 @field_validator("vapid_private_key") 

103 @classmethod 

104 def _coerce_vapid_private_key(cls, value: Any) -> Any: 

105 if value is None or isinstance(value, SecretStr): 

106 return value 

107 return SecretStr(str(value)) 

108 

109 

110@dataclass(init=False) 

111class NamedPushConfig(DomainModel): 

112 """Configuration for a single named push notification backend. 

113 

114 Used in NotificationConfig.push_backends to declare multiple push backends 

115 that the framework registers as named DI bindings. 

116 

117 Example: 

118 push_backends: 

119 - name: mobile 

120 driver: fcm 

121 primary: true 

122 fcm: 

123 server_key: AAAA... 

124 

125 Args: 

126 name: Unique backend identifier. Used as the Named() DI key. 

127 primary: Whether this is the primary backend. Primary backends 

128 also receive the unnamed PushChannelProtocol binding. 

129 driver: Push driver. One of 'fcm' or other supported drivers. 

130 fcm: FCM-specific config. 

131 """ 

132 

133 name: str = Field(..., description="Unique backend name used as the Named() DI key") 

134 primary: bool = Field( 

135 default=False, 

136 description="Also register under unnamed PushChannelProtocol binding", 

137 ) 

138 driver: str = Field(default="fcm", description="Push driver name") 

139 fcm: FCMDriverConfig | None = Field( 

140 default=None, 

141 description="FCM driver config", 

142 ) 

143 apns: APNsDriverConfig | None = Field( 

144 default=None, 

145 description="APNs driver config", 

146 ) 

147 web_push: WebPushDriverConfig | None = Field( 

148 default=None, 

149 description="Web Push driver config", 

150 ) 

151 

152 

153__all__ = [ 

154 "APNsDriverConfig", 

155 "FCMDriverConfig", 

156 "NamedPushConfig", 

157 "WebPushDriverConfig", 

158]