Coverage for functions \ flipdare \ firestore \ context \ friend_context.py: 63%

116 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 typing import TYPE_CHECKING, Any, override 

16 

17from flipdare.app_log import LOG 

18from flipdare.app_types import DatabaseDict 

19from flipdare.firestore.context._model_context import ModelContext 

20from flipdare.firestore.context._model_context_factory import ModelContextFactory 

21from flipdare.generated.model.friend_model import FriendModel 

22from flipdare.wrapper import FriendWrapper, UserWrapper 

23 

24if TYPE_CHECKING: 

25 from flipdare.manager.db_manager import DbManager 

26 

27__all__ = ["FriendContext", "FriendContextFactory"] 

28 

29 

30class FriendContextFactory(ModelContextFactory[FriendWrapper, "FriendContext"]): 

31 """Factory for creating FriendContext instances from various input types.""" 

32 

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

34 super().__init__(db_manager=db_manager) 

35 

36 @override 

37 def create(self, obj: Any) -> FriendContext | None: 

38 if isinstance(obj, FriendContext): 

39 return obj 

40 if isinstance(obj, FriendWrapper): 

41 return self._from_model(obj) 

42 if isinstance(obj, str): 

43 return self._from_id(obj) 

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

45 return self._from_data(obj_data) 

46 

47 @override 

48 def _from_id(self, doc_id: str) -> FriendContext | None: 

49 try: 

50 friend = self.friend_db.get(doc_id) 

51 if friend is None: 

52 LOG().error(f"Friend {doc_id} not found in db.") 

53 return None 

54 return self._from_model(friend.model) 

55 except Exception: 

56 LOG().error(f"Error retrieving friend {doc_id} from db.") 

57 return None 

58 

59 @override 

60 def _from_data(self, data: DatabaseDict) -> FriendContext | None: 

61 friend: FriendWrapper | None = None 

62 try: 

63 friend = FriendWrapper.from_dict(data) 

64 except Exception: 

65 return None 

66 

67 return self._from_model(friend) 

68 

69 @override 

70 def _from_model(self, model: FriendWrapper | FriendModel) -> FriendContext | None: 

71 from_uid = model.from_uid 

72 to_uid = model.to_uid 

73 

74 model_id = model.doc_id if isinstance(model, FriendWrapper) else model.id 

75 if model_id is None: 

76 LOG().error(f"Friend model is missing doc_id: {model}") 

77 return None 

78 

79 try: 

80 try: 

81 from_user = self.get_user(from_uid) # Returns UserWrapper | None 

82 except Exception: 

83 LOG().error(f"From user {from_uid} for friend {model_id} not found in db.") 

84 from_user = None 

85 try: 

86 to_user = self.get_user(to_uid) # Returns UserWrapper | None 

87 except Exception: 

88 LOG().error(f"To user {to_uid} for friend {model_id} not found in db.") 

89 to_user = None 

90 

91 # Wrap friend model 

92 if isinstance(model, FriendModel): 

93 model = FriendWrapper.from_model(model) 

94 

95 return FriendContext(friend=model, from_user=from_user, to_user=to_user) 

96 except Exception: 

97 return None 

98 

99 

100class FriendContext(ModelContext): 

101 """Context wrapping a friend relationship with both users.""" 

102 

103 def __init__( 

104 self, 

105 friend: FriendWrapper, 

106 from_user: UserWrapper | None = None, 

107 to_user: UserWrapper | None = None, 

108 ) -> None: 

109 self._friend_model = friend 

110 self._from_user = from_user 

111 self._to_user = to_user 

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

113 super().__init__() 

114 

115 @property 

116 def from_user(self) -> UserWrapper: 

117 self._require_valid("access from_user") 

118 return self._from_user # type: ignore 

119 

120 @property 

121 def from_id(self) -> str: 

122 self._require_valid("access from_id") 

123 assert self._from_user is not None # for mypy 

124 return self._from_user.doc_id 

125 

126 @property 

127 def to_user(self) -> UserWrapper: 

128 self._require_valid("access to_user") 

129 return self._to_user # type: ignore 

130 

131 @property 

132 def to_id(self) -> str: 

133 self._require_valid("access to_id") 

134 assert self._to_user is not None # for mypy 

135 return self._to_user.doc_id 

136 

137 @property 

138 def friend(self) -> FriendWrapper: 

139 self._require_valid("access friend_model") 

140 return self._friend_model 

141 

142 @property 

143 def friend_id(self) -> str: 

144 self._require_valid("access friend_id") 

145 assert self._friend_model is not None # for mypy 

146 return self._friend_model.doc_id 

147 

148 @property 

149 @override 

150 def doc_id(self) -> str: 

151 return self.friend.doc_id 

152 

153 @override 

154 def validate(self) -> bool: 

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

156 return ( 

157 self._is_model_valid(self._from_user) 

158 and self._is_model_valid(self._to_user) 

159 and self._is_model_valid(self._friend_model) 

160 ) 

161 

162 @property 

163 @override 

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

165 """Build list of validation errors.""" 

166 errors: list[str] = [] 

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

168 errors.append(err) 

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

170 errors.append(err) 

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

172 errors.append(err) 

173 return errors