Coverage for functions \ flipdare \ result \ job_result.py: 95%

83 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 

14from dataclasses import dataclass 

15from typing import Any, ClassVar, NoReturn, Self, override 

16from flipdare.result.output_result import OutputResult 

17from flipdare.app_types import DatabaseDict 

18from flipdare.constants import NO_DOC_ID 

19from flipdare.error.app_error_protocol import AppErrorProtocol 

20from flipdare.result.app_result import AppResult 

21from flipdare.generated.shared.app_error_code import AppErrorCode 

22from flipdare.generated.shared.backend.app_job_type import AppJobType 

23from flipdare.result.outcome import Outcome 

24from flipdare.generated.shared.firestore_collections import FirestoreCollections 

25 

26 

27@dataclass(kw_only=True) 

28class JobResult[T](OutputResult): 

29 _ERROR_MSG: ClassVar[str] = "You must set .result or .value before returning." 

30 

31 data: DatabaseDict | None = None 

32 _app_result: AppResult[T] | None = None 

33 

34 @classmethod 

35 def setup( 

36 cls, 

37 message: str, 

38 duration: int = -1, 

39 result: AppResult[T] | None = None, 

40 error_code: AppErrorProtocol | None = None, 

41 doc_id: str | None = None, 

42 job_type: AppJobType | None = None, 

43 collection: FirestoreCollections | None = None, 

44 data: DatabaseDict | None = None, 

45 ) -> Self: 

46 error_code = error_code or (result.main_error if result else None) or None 

47 return cls( 

48 _app_result=result, 

49 duration=duration, 

50 data=data, 

51 outcome=Outcome.ERROR if error_code else Outcome.OK, 

52 error_code=error_code, 

53 doc_id=doc_id or (result.doc_id if result else NO_DOC_ID), 

54 job_type=job_type, 

55 collection=collection, 

56 message=message, 

57 ) 

58 

59 @classmethod 

60 def from_result( 

61 cls, 

62 result: AppResult[T], 

63 duration: int = -1, 

64 message: str | None = None, 

65 doc_id: str | None = None, 

66 job_type: AppJobType | None = None, 

67 collection: FirestoreCollections | None = None, 

68 data: DatabaseDict | None = None, 

69 ) -> Self: 

70 """Create OutputAppResult from an AppResult. Outcome is determined by AppResult state (ok, error).""" 

71 outcome = result.outcome 

72 error_code: AppErrorProtocol | None = None 

73 

74 base_msg = message or result.message 

75 

76 match outcome: 

77 case Outcome.OK: 

78 message = base_msg or "Operation completed successfully." 

79 case Outcome.SKIPPED: 

80 message = base_msg or "Operation skipped." 

81 case Outcome.WARNING: 

82 # we dont set an error code, because its a warning .. 

83 message = base_msg or "Operation completed with warnings." 

84 case Outcome.ERROR: 

85 error_code = result.main_error or AppErrorCode.SERVER 

86 message = base_msg or f"Operation failed with error: {error_code}" 

87 

88 return cls( 

89 _app_result=result, 

90 duration=duration, 

91 data=data, 

92 outcome=outcome, 

93 error_code=error_code, 

94 doc_id=doc_id or result.doc_id, 

95 job_type=job_type, 

96 collection=collection, 

97 message=message, 

98 ) 

99 

100 @classmethod 

101 @override 

102 def ok( 

103 cls, 

104 *, 

105 duration: int = -1, 

106 doc_id: str | None = None, 

107 message: str = "Operation completed successfully.", 

108 job_type: AppJobType | None = None, 

109 collection: FirestoreCollections | None = None, 

110 **kwargs: Any, 

111 ) -> Self: 

112 """Create successful OutputAppResult. Note: Requires doc_id to be provided (no default in practice).""" 

113 return cls( 

114 _app_result=AppResult[T].ok(doc_id=doc_id or NO_DOC_ID, message=message), 

115 outcome=Outcome.OK, 

116 duration=duration, 

117 doc_id=doc_id or NO_DOC_ID, 

118 job_type=job_type, 

119 collection=collection, 

120 message=message, 

121 ) 

122 

123 @classmethod 

124 @override 

125 def partial( 

126 cls, 

127 *, 

128 error_code: AppErrorProtocol, 

129 message: str, 

130 duration: int, 

131 doc_id: str | None = None, 

132 job_type: AppJobType | None = None, 

133 collection: FirestoreCollections | None = None, 

134 **kwargs: Any, 

135 ) -> Self: 

136 """Create partial OutputAppResult. Note: Requires doc_id to be provided (no default in practice).""" 

137 # note: partial are errors that are not critical enough to fail the entire job, 

138 # note: but should still be logged and monitored. 

139 # note: They often indicate issues that need attention, but do not necessarily require immediate action. 

140 return cls( 

141 _app_result=AppResult[T].error( 

142 doc_id=doc_id or NO_DOC_ID, message=message, error_code=error_code 

143 ), 

144 outcome=Outcome.WARNING, 

145 duration=duration, 

146 error_code=error_code, 

147 doc_id=doc_id or NO_DOC_ID, 

148 job_type=job_type, 

149 collection=collection, 

150 message=message, 

151 ) 

152 

153 @classmethod 

154 def skip_doc( 

155 cls, 

156 doc_id: str, 

157 message: str, 

158 duration: int = -1, 

159 job_type: AppJobType | None = None, 

160 collection: FirestoreCollections | None = None, 

161 data: DatabaseDict | None = None, 

162 ) -> Self: 

163 """Create skipped OutputAppResult for a specific document.""" 

164 return cls( 

165 _app_result=AppResult[T].skip(doc_id=doc_id, message=message or "Operation skipped."), 

166 data=data, 

167 duration=duration, 

168 outcome=Outcome.SKIPPED, 

169 doc_id=doc_id, 

170 job_type=job_type, 

171 collection=collection, 

172 message=message, 

173 ) 

174 

175 @classmethod 

176 def skip_job( 

177 cls, 

178 job_type: AppJobType, 

179 message: str, 

180 duration: int = -1, 

181 collection: FirestoreCollections | None = None, 

182 data: DatabaseDict | None = None, 

183 ) -> Self: 

184 return cls( 

185 _app_result=AppResult[T].skip(doc_id=NO_DOC_ID, message=message), 

186 duration=duration, 

187 data=data, 

188 outcome=Outcome.SKIPPED, 

189 doc_id=NO_DOC_ID, 

190 job_type=job_type, 

191 collection=collection, 

192 message=message, 

193 ) 

194 

195 # -------------------------------------------------------------------------------------------- 

196 # STATE UPDATE METHODS 

197 # -------------------------------------------------------------------------------------------- 

198 

199 @override 

200 def set_ok( 

201 self, 

202 message: str, 

203 doc_id: str | None = None, 

204 ) -> None: 

205 current_doc_id = self.app_result.doc_id 

206 self._app_result = AppResult[T].ok(doc_id=current_doc_id, message=message) 

207 self.outcome = Outcome.OK 

208 self.doc_id = doc_id if doc_id is not None else current_doc_id 

209 self.error_code = None 

210 self.message = message 

211 self.data = None 

212 

213 @override # type: ignore[override] 

214 def set_error( 

215 self, 

216 error_code: AppErrorCode, 

217 message: str, 

218 *, 

219 app_result: AppResult[Any] | None = None, 

220 data: DatabaseDict | None = None, 

221 ) -> None: 

222 self._app_result = app_result 

223 self.data = data 

224 self.outcome = Outcome.ERROR 

225 self.error_code = error_code 

226 self.message = message 

227 

228 # -------------------------------------------------------------------------------------------- 

229 # Access state 

230 # -------------------------------------------------------------------------------------------- 

231 @property 

232 def is_finalized(self) -> bool: 

233 return self._app_result is not None 

234 

235 @property 

236 def app_result(self) -> AppResult[T]: 

237 if self._app_result is None: 

238 self._finalize_error() 

239 return self._app_result 

240 

241 @app_result.setter 

242 def app_result(self, val: AppResult[T]) -> None: 

243 self._app_result = val 

244 

245 # -------------------------------------------------------------------------------------------- 

246 # Properties 

247 # -------------------------------------------------------------------------------------------- 

248 

249 @property 

250 def should_log(self) -> bool: 

251 return not (self.outcome.is_ok or self.outcome.is_skipped) 

252 

253 def _finalize_error(self) -> NoReturn: 

254 msg = f"OutputResult for {self.doc_id} was never finalized! {self._ERROR_MSG}" 

255 raise ValueError(msg)