Coverage for functions \ flipdare \ firestore \ context \ _model_context.py: 50%

50 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-05-08 12:22 +1000

1#!/usr/bin/env python 

2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved. 

3# 

4# This file is part of Flipdare's proprietary software and contains 

5# confidential and copyrighted material. Unauthorised copying, 

6# modification, distribution, or use of this file is strictly 

7# prohibited without prior written permission from Flipdare Pty Ltd. 

8# 

9# This software includes third-party components licensed under MIT, 

10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details. 

11# 

12 

13""" 

14Base classes for model contexts and their factories. 

15 

16Provides common validation, error handling, and factory patterns for 

17context objects that wrap multiple related models (e.g., Friend + Users, 

18Dare + Users, Group + Members). 

19 

20All models are wrapped in PersistedWrapper to guarantee doc_id exists, 

21eliminating the need for doc_id validation checks. 

22""" 

23 

24from __future__ import annotations 

25 

26from abc import ABC, abstractmethod 

27from typing import Any 

28 

29from flipdare.wrapper import PersistedWrapper 

30 

31__all__ = ["ModelContext"] 

32 

33 

34class ModelContext(ABC): 

35 """ 

36 Abstract base class for model contexts. 

37 

38 A context wraps one or more related models and provides: 

39 - Validation that all required models exist and have doc_ids 

40 - Safe property access that raises meaningful errors on invalid state 

41 - Consistent error messaging 

42 

43 Subclasses should: 

44 1. Store model references in __init__ 

45 2. Implement validate() to check all models are valid 

46 3. Implement _error_messages property to return list of validation errors 

47 4. Use _require_valid() before accessing model properties 

48 """ 

49 

50 def __init__(self) -> None: 

51 """ 

52 Initialize context and cache validation result. 

53 

54 Note: Subclasses should call super().__init__() AFTER setting 

55 all model attributes, since validate() is called during init. 

56 """ 

57 self._cached_valid = self.validate() 

58 

59 @property 

60 @abstractmethod 

61 def doc_id(self) -> str: ... 

62 

63 @property 

64 @abstractmethod 

65 def _error_messages(self) -> list[str]: 

66 """ 

67 Return list of validation error messages. 

68 

69 Should check all required models and return descriptive errors. 

70 Return empty list if no errors. 

71 

72 Example: 

73 @property 

74 def _error_messages(self) -> list[str]: 

75 errors = [] 

76 if err := self._validate_model(self._from_user, "from_user"): 

77 errors.append(err) 

78 if err := self._validate_model(self._to_user, "to_user"): 

79 errors.append(err) 

80 if err := self._validate_model(self._friend_model, "friend_model"): 

81 errors.append(err) 

82 return errors 

83 

84 """ 

85 ... 

86 

87 @abstractmethod 

88 def validate(self) -> bool: 

89 """ 

90 Validate that all required models are present and valid. 

91 

92 Should check all models using _is_model_valid() helper. 

93 

94 Example: 

95 def validate(self) -> bool: 

96 return (self._is_model_valid(self._from_user) and 

97 self._is_model_valid(self._to_user) and 

98 self._is_model_valid(self._friend_model)) 

99 

100 """ 

101 ... 

102 

103 @property 

104 def valid(self) -> bool: 

105 """Whether this context passed validation.""" 

106 return self._cached_valid 

107 

108 @property 

109 def error_str(self) -> str | None: 

110 """ 

111 Get formatted validation error message, or None if valid. 

112 

113 Consistent naming with existing context classes (validation_error vs error_str). 

114 """ 

115 if self.valid: 

116 return None 

117 

118 messages = self._error_messages 

119 if not messages: 

120 return f"{self.__class__.__name__} validation failed with no specific errors" 

121 

122 msg = f"{self.__class__.__name__} validation errors:\n" 

123 for m in messages: 

124 msg += f" - {m}\n" 

125 return msg.rstrip() 

126 

127 @property 

128 def validation_error(self) -> str | None: 

129 """ 

130 Get formatted validation error message, or None if valid. 

131 

132 Consistent naming with existing context classes (validation_error vs error_str). 

133 """ 

134 if self.valid: 

135 return None 

136 

137 messages = self._error_messages 

138 if not messages: 

139 return f"{self.__class__.__name__} validation failed with no specific errors" 

140 

141 msg = f"{self.__class__.__name__} validation errors:\n" 

142 for m in messages: 

143 msg += f" - {m}\n" 

144 return msg.rstrip() 

145 

146 def _require_valid(self, context: str = "") -> None: 

147 """ 

148 Raise ValueError if context is invalid. 

149 

150 Args: 

151 context: Description of what operation requires validity (e.g., "access friend_model") 

152 

153 Raises: 

154 ValueError: If context is invalid 

155 

156 Usage: 

157 @property 

158 def friend_model(self) -> FriendModel: 

159 self._require_valid("access friend_model") 

160 return self._friend_model # type: ignore 

161 

162 """ 

163 if not self.valid: 

164 error_msg = self.validation_error or "Context is invalid" 

165 if context: 

166 raise ValueError( 

167 f"Invalid {self.__class__.__name__}: cannot {context}.\n{error_msg}", 

168 ) 

169 raise ValueError(f"Invalid {self.__class__.__name__}.\n{error_msg}") 

170 

171 def _is_model_valid(self, model: PersistedWrapper[Any] | None) -> bool: 

172 """ 

173 Check if a persisted model is valid (not None). 

174 

175 Since PersistedWrapper guarantees doc_id exists, we only need to check for None. 

176 

177 Args: 

178 model: PersistedWrapper to validate 

179 

180 Returns: 

181 True if model exists, False if None 

182 

183 """ 

184 return model is not None 

185 

186 def _validate_model(self, model: PersistedWrapper[Any] | None, name: str) -> str | None: 

187 """ 

188 Validate a persisted model and return error message if invalid. 

189 

190 Since PersistedWrapper guarantees doc_id exists, we only check if None. 

191 

192 Args: 

193 model: PersistedWrapper to validate 

194 name: Name to use in error message (e.g., "from_user", "friend") 

195 

196 Returns: 

197 Error message if None, None if valid 

198 

199 Example: 

200 @property 

201 def _error_messages(self) -> list[str]: 

202 errors = [] 

203 if err := self._validate_model(self._from_user, "from_user"): 

204 errors.append(err) 

205 if err := self._validate_model(self._to_user, "to_user"): 

206 errors.append(err) 

207 return errors 

208 

209 """ 

210 if model is None: 

211 return f"{name} is not set" 

212 return None