Coverage for bc/kwai-bc-club/src/kwai_bc_club/import_members.py: 86%

71 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2024-01-01 00:00 +0000

1"""Module that implements a use case for importing members.""" 

2 

3from abc import ABC 

4from dataclasses import dataclass, replace 

5from typing import AsyncGenerator 

6 

7from kwai_core.domain.presenter import AsyncPresenter, IterableResult 

8from kwai_core.domain.use_case import UseCaseResult 

9 

10from kwai_bc_club.domain.file_upload import FileUploadEntity 

11from kwai_bc_club.domain.member import MemberEntity 

12from kwai_bc_club.repositories import member_importer 

13from kwai_bc_club.repositories.file_upload_repository import FileUploadRepository 

14from kwai_bc_club.repositories.member_repository import ( 

15 MemberNotFoundException, 

16 MemberRepository, 

17) 

18 

19 

20@dataclass(kw_only=True, slots=True, frozen=True) 

21class MemberImportResult(UseCaseResult, ABC): 

22 """The result of the use case ImportMembers.""" 

23 

24 file_upload: FileUploadEntity 

25 row: int 

26 

27 

28@dataclass(kw_only=True, slots=True, frozen=True) 

29class OkMemberImportResult(MemberImportResult): 

30 """A successful import of a member.""" 

31 

32 member: MemberEntity 

33 

34 def to_message(self) -> str: 

35 return f"Member {self.member.id}(row={self.row}) imported successfully." 

36 

37 

38@dataclass(kw_only=True, slots=True, frozen=True) 

39class FailureMemberImportResult(MemberImportResult): 

40 """An import of a member failed.""" 

41 

42 message: str 

43 

44 def to_message(self) -> str: 

45 return self.message 

46 

47 

48@dataclass(kw_only=True, slots=True, frozen=True) 

49class ImportMembersCommand: 

50 """Input for the use case "ImportMembers".""" 

51 

52 preview: bool = True 

53 

54 

55class ImportMembers: 

56 """Use case for importing members. 

57 

58 Note: The presenter will need to process all elements from this generator 

59 to save all the members into the database. 

60 """ 

61 

62 def __init__( 

63 self, 

64 importer: member_importer.MemberImporter, 

65 file_upload_repo: FileUploadRepository, 

66 member_repo: MemberRepository, 

67 presenter: AsyncPresenter[IterableResult[MemberImportResult]], 

68 ): 

69 """Initialize the use case. 

70 

71 Args: 

72 importer: A class that is responsible for importing members from a resource. 

73 file_upload_repo: A repository for storing the file upload information. 

74 member_repo: A repository for managing members. 

75 presenter: A presenter 

76 """ 

77 self._importer = importer 

78 self._file_upload_repo = file_upload_repo 

79 self._member_repo = member_repo 

80 self._presenter = presenter 

81 

82 async def execute(self, command: ImportMembersCommand): 

83 """Execute the use case.""" 

84 file_upload_entity = await self._file_upload_repo.create( 

85 self._importer.create_file_upload_entity(command.preview) 

86 ) 

87 

88 await self._presenter.present( 

89 IterableResult( 

90 count=0, 

91 iterator=self.process_member(file_upload_entity, command.preview), 

92 ) 

93 ) 

94 

95 if not command.preview: 

96 await self._activate_members(file_upload_entity) 

97 

98 async def process_member( 

99 self, file_upload_entity: FileUploadEntity, preview: bool 

100 ) -> AsyncGenerator[MemberImportResult, None]: 

101 """Get the next imported member.""" 

102 async for import_result in self._importer.import_(): 

103 match import_result: 

104 case member_importer.OkResult(): 

105 member = await self._save_member( 

106 file_upload_entity, import_result.member, preview 

107 ) 

108 yield OkMemberImportResult( 

109 file_upload=file_upload_entity, 

110 row=import_result.row, 

111 member=member, 

112 ) 

113 case member_importer.FailureResult(): 

114 yield FailureMemberImportResult( 

115 file_upload=file_upload_entity, 

116 row=import_result.row, 

117 message=import_result.message, 

118 ) 

119 

120 async def _save_member( 

121 self, file_upload: FileUploadEntity, member: MemberEntity, preview: bool 

122 ) -> MemberEntity: 

123 """Create or update the member.""" 

124 existing_member = await self._get_member(member) 

125 if existing_member is not None: 

126 updated_member = self._update_member(existing_member, member) 

127 await self._file_upload_repo.save_member(file_upload, updated_member) 

128 if not preview: 

129 await self._member_repo.update(updated_member) 

130 return updated_member 

131 

132 if not preview: 

133 member = await self._member_repo.create(member) 

134 await self._file_upload_repo.save_member(file_upload, member) 

135 

136 return member 

137 

138 @classmethod 

139 def _update_member( 

140 cls, old_member: MemberEntity, new_member: MemberEntity 

141 ) -> MemberEntity: 

142 """Update an existing member with the new imported data.""" 

143 updated_contact = replace( 

144 old_member.person.contact, 

145 traceable_time=old_member.person.contact.traceable_time.mark_for_update(), 

146 ) 

147 updated_person = replace( 

148 old_member.person, 

149 id=old_member.person.id, 

150 contact=updated_contact, 

151 traceable_time=old_member.person.traceable_time.mark_for_update(), 

152 ) 

153 updated_member = replace( 

154 new_member, 

155 id=old_member.id, 

156 uuid=old_member.uuid, 

157 remark=old_member.remark, 

158 competition=old_member.competition, 

159 active=old_member.active, 

160 person=updated_person, 

161 traceable_time=old_member.traceable_time.mark_for_update(), 

162 ) 

163 return updated_member 

164 

165 async def _get_member(self, member: MemberEntity) -> MemberEntity | None: 

166 """Return the member. 

167 

168 Returns: 

169 If found the member is returned, otherwise None is returned. 

170 """ 

171 member_query = self._member_repo.create_query() 

172 member_query.filter_by_license(member.license.number) 

173 

174 try: 

175 member = await self._member_repo.get(member_query) 

176 except MemberNotFoundException: 

177 return None 

178 

179 return member 

180 

181 async def _activate_members(self, upload_entity: FileUploadEntity): 

182 """Activate members. 

183 

184 Members that are part of the upload will be activated. 

185 Members not part of the upload will be deactivated. 

186 """ 

187 await self._member_repo.activate_members(upload_entity) 

188 await self._member_repo.deactivate_members(upload_entity)