Coverage for functions \ flipdare \ firestore \ context \ dare_context.py: 68%

180 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 

13from __future__ import annotations 

14 

15from abc import abstractmethod 

16from typing import TYPE_CHECKING, Any, TypeVar, override 

17 

18from flipdare.app_log import LOG 

19from flipdare.app_types import DatabaseDict 

20from flipdare.firestore.context._model_context import ModelContext 

21from flipdare.firestore.context._model_context_factory import ModelContextFactory 

22from flipdare.generated.model.dare_model import DareModel 

23from flipdare.wrapper import ( 

24 DareWrapper, 

25 GroupWrapper, 

26 UserWrapper, 

27) 

28 

29if TYPE_CHECKING: 

30 from flipdare.manager.db_manager import DbManager 

31 

32__all__ = ["DareContextFactory", "DareContext", "UserDareContext", "GroupDareContext"] 

33 

34T = TypeVar("T", bound="DareContext") 

35 

36 

37class DareContextFactory(ModelContextFactory[DareWrapper, "DareContext"]): 

38 """Factory for creating DareContext instances (UserDareContext or GroupDareContext).""" 

39 

40 def __init__(self, db_manager: DbManager | None = None) -> None: 

41 super().__init__(db_manager=db_manager) 

42 

43 @override 

44 def create(self, obj: Any) -> DareContext | None: 

45 if isinstance(obj, DareContext): 

46 return obj 

47 if isinstance(obj, DareWrapper): 

48 return self._from_model(obj) 

49 if isinstance(obj, str): 

50 return self._from_id(obj) 

51 obj_data: DatabaseDict = obj # set explicity type for type checkers. 

52 return self._from_data(obj_data) 

53 

54 @override 

55 def _from_id(self, doc_id: str) -> DareContext | None: 

56 dare_db = self.dare_db 

57 try: 

58 dare = dare_db.get(doc_id) 

59 if dare is None: 

60 LOG().error(f"Dare {doc_id} not found in db.") 

61 return None 

62 

63 return self._from_model(dare.model) # Unwrap before passing to _from_model 

64 except Exception: 

65 LOG().error(f"Error retrieving dare {doc_id} from db.") 

66 return None 

67 

68 @override 

69 def _from_data(self, data: DatabaseDict) -> DareContext | None: 

70 wrapper: DareWrapper | None = None 

71 try: 

72 wrapper = DareWrapper.from_dict(data) 

73 except Exception: 

74 LOG().error(f"Error parsing dare data into model: {data}") 

75 return None 

76 

77 return self._from_model(wrapper) 

78 

79 @override 

80 def _from_model(self, model: DareWrapper | DareModel) -> DareContext | None: 

81 user_db = self.user_db 

82 group_db = self.group_db 

83 

84 from_uid = model.from_uid 

85 obj_id = model.obj_id 

86 

87 doc_id = model.doc_id if isinstance(model, DareWrapper) else model.id 

88 if doc_id is None: 

89 LOG().error(f"Dare model is missing doc_id: {model}") 

90 return None 

91 

92 # db.get() already returns UserWrapper | None 

93 from_user = user_db.get(from_uid) 

94 if isinstance(model, DareModel): 

95 model = DareWrapper.from_model(model) 

96 

97 to_user = user_db.get(obj_id) 

98 

99 if not model.is_group_dare: 

100 if to_user is None: 

101 msg = f"To user {obj_id} for dare {model.doc_id} not found in db." 

102 LOG().error(msg) 

103 return None 

104 

105 return UserDareContext(from_user=from_user, to_user=to_user, dare=model) 

106 else: 

107 group_model = group_db.get(obj_id) 

108 if group_model is None: 

109 msg = f"To group {obj_id} for dare {model.doc_id} not found in db." 

110 LOG().error(msg) 

111 return None 

112 

113 return GroupDareContext( 

114 from_user=from_user, 

115 to_group=group_model, 

116 dare=model, 

117 to_user=to_user, 

118 ) 

119 

120 

121class DareContext(ModelContext): 

122 """Abstract base context for dare relationships (user-to-user or user-to-group).""" 

123 

124 def __init__( 

125 self, 

126 from_user: UserWrapper | None = None, 

127 dare: DareWrapper | None = None, 

128 ) -> None: 

129 self._from_user = from_user 

130 self._dare = dare 

131 # Call super().__init__() LAST - it calls validate() 

132 super().__init__() 

133 

134 @property 

135 @override 

136 def doc_id(self) -> str: 

137 return self.dare.doc_id 

138 

139 @property 

140 @abstractmethod 

141 def to_id(self) -> str: ... 

142 

143 @property 

144 @abstractmethod 

145 def to_obj(self) -> UserWrapper | GroupWrapper: ... 

146 

147 @property 

148 def is_group_dare(self) -> bool: 

149 self._require_valid("determine is_group_dare") 

150 return self.dare.model.is_group_dare 

151 

152 @property 

153 def short_description(self) -> str: 

154 self._require_valid("generate short description") 

155 to_str: str 

156 if isinstance(self.to_obj, GroupWrapper): 

157 to_str = self.to_obj.name 

158 else: 

159 to_str = self.to_obj.model.safe_name 

160 

161 return f"Dare '{self.dare.model.title}' from {self.from_user.model.safe_name} to {to_str}" 

162 

163 @property 

164 def from_user(self) -> UserWrapper: 

165 self._require_valid("access from_user") 

166 assert self._from_user is not None # narrowing 

167 return self._from_user 

168 

169 @property 

170 def from_id(self) -> str: 

171 self._require_valid("access from_id") 

172 assert self._from_user is not None # narrowing 

173 return self._from_user.doc_id 

174 

175 @property 

176 def dare(self) -> DareWrapper: 

177 self._require_valid("access dare") 

178 assert self._dare is not None # narrowing 

179 return self._dare 

180 

181 @property 

182 def dare_id(self) -> str: 

183 self._require_valid("access dare_id") 

184 assert self._dare is not None # narrowing 

185 return self._dare.doc_id 

186 

187 @override 

188 def validate(self) -> bool: 

189 """Validate that all required models exist and have doc_ids.""" 

190 return self._is_model_valid(self._from_user) and self._is_model_valid(self._dare) 

191 

192 @property 

193 @override 

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

195 """Build list of validation errors.""" 

196 errors: list[str] = [] 

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

198 errors.append(err) 

199 if err := self._validate_model(self._dare, "dare"): 

200 errors.append(err) 

201 return errors 

202 

203 

204class UserDareContext(DareContext): 

205 """Context for user-to-user dare relationships.""" 

206 

207 def __init__( 

208 self, 

209 from_user: UserWrapper | None = None, 

210 to_user: UserWrapper | None = None, 

211 dare: DareWrapper | None = None, 

212 ) -> None: 

213 self._to_user = to_user 

214 # Call parent __init__ after setting _to_user so validation can check it 

215 super().__init__(from_user=from_user, dare=dare) 

216 

217 @property 

218 @override 

219 def to_id(self) -> str: 

220 self._require_valid("access to_id") 

221 assert self._to_user is not None # narrowing 

222 return self._to_user.doc_id 

223 

224 @property 

225 @override 

226 def to_obj(self) -> UserWrapper: 

227 self._require_valid("access to_user") 

228 assert self._to_user is not None # narrowing 

229 return self._to_user 

230 

231 @override 

232 def validate(self) -> bool: 

233 """Validate that all required models exist and have doc_ids.""" 

234 return super().validate() and self._is_model_valid(self._to_user) 

235 

236 @property 

237 @override 

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

239 """Build list of validation errors.""" 

240 errors = super()._error_messages 

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

242 errors.append(err) 

243 return errors 

244 

245 

246class GroupDareContext(DareContext): 

247 """Context for user-to-group dare relationships.""" 

248 

249 def __init__( 

250 self, 

251 from_user: UserWrapper | None = None, 

252 to_group: GroupWrapper | None = None, 

253 # i.e. the user that has accepted/completed the group dare 

254 to_user: UserWrapper | None = None, 

255 dare: DareWrapper | None = None, 

256 ) -> None: 

257 self._to_group = to_group 

258 self._to_user = to_user 

259 

260 # Call parent __init__ after setting _to_group so validation can check it 

261 super().__init__(from_user=from_user, dare=dare) 

262 

263 @property 

264 @override 

265 def to_id(self) -> str: 

266 self._require_valid("access to_id") 

267 assert self._to_group is not None # narrowing 

268 return self._to_group.doc_id 

269 

270 @property 

271 @override 

272 def to_obj(self) -> GroupWrapper: 

273 self._require_valid("access to_group") 

274 assert self._to_group is not None # narrowing 

275 return self._to_group 

276 

277 @property 

278 def to_user(self) -> UserWrapper | None: 

279 """The user that has accepted/completed the group dare, if applicable.""" 

280 self._require_valid("access to_user") 

281 return self._to_user 

282 

283 @override 

284 def validate(self) -> bool: 

285 """Validate that all required models exist and have doc_ids.""" 

286 return super().validate() and self._is_model_valid(self._to_group) 

287 

288 @property 

289 @override 

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

291 """Build list of validation errors.""" 

292 errors = super()._error_messages 

293 if err := self._validate_model(self._to_group, "to_group"): 

294 errors.append(err) 

295 return errors