Coverage for cc_modules/cc_simpleobjects.py: 58%

72 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 15:51 +0100

1""" 

2camcops_server/cc_modules/cc_simpleobjects.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

11 CamCOPS is free software: you can redistribute it and/or modify 

12 it under the terms of the GNU General Public License as published by 

13 the Free Software Foundation, either version 3 of the License, or 

14 (at your option) any later version. 

15 

16 CamCOPS is distributed in the hope that it will be useful, 

17 but WITHOUT ANY WARRANTY; without even the implied warranty of 

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

21 You should have received a copy of the GNU General Public License 

22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

23 

24=============================================================================== 

25 

26**Simple struct-like classes.** 

27 

28""" 

29 

30import copy 

31from typing import Any, List, Optional, TYPE_CHECKING 

32 

33from pendulum import Date 

34 

35from cardinal_pythonlib.datetimefunc import format_datetime 

36from cardinal_pythonlib.reprfunc import auto_repr 

37 

38from camcops_server.cc_modules.cc_constants import DateFormat 

39 

40if TYPE_CHECKING: 

41 from camcops_server.cc_modules.cc_request import CamcopsRequest 

42 

43# Prefer classes to collections.namedtuple; both support type checking but 

44# classes support better parameter checking (and refactoring) via PyCharm. 

45 

46 

47# ============================================================================= 

48# IdNumReference 

49# ============================================================================= 

50 

51 

52class IdNumReference(object): 

53 """ 

54 A simple way of referring to an ID number. 

55 

56 It's not stored in the database -- it's just an object to be passed around 

57 that encapsulates ``which_idnum`` and ``idnum_value``. 

58 

59 As an example, suppose our administrator has defined ID type 

60 (``which_idnum``) 7 to be "NHS number". Then if a patient has NHS number 

61 9999999999, we might represent this ID of theirs as 

62 ``IdNumReference(which_idnum=7, idnum_value=9999999999)``. 

63 """ 

64 

65 def __init__(self, which_idnum: int, idnum_value: int) -> None: 

66 self.which_idnum = which_idnum 

67 self.idnum_value = idnum_value 

68 

69 def __str__(self) -> str: 

70 return f"idnum{self.which_idnum}={self.idnum_value}" 

71 

72 def __repr__(self) -> str: 

73 return auto_repr(self) 

74 

75 def is_valid(self) -> bool: 

76 return ( 

77 self.which_idnum is not None 

78 and self.which_idnum > 0 

79 and self.idnum_value is not None 

80 and self.idnum_value > 0 

81 ) 

82 

83 def __eq__(self, other: object) -> bool: 

84 if not isinstance(other, IdNumReference): 

85 return False 

86 return ( 

87 self.which_idnum == other.which_idnum 

88 and self.idnum_value == other.idnum_value 

89 ) 

90 

91 def description(self, req: "CamcopsRequest") -> str: 

92 if not self.is_valid(): 

93 return "[invalid_IdNumReference]" 

94 return f"{req.get_id_shortdesc(self.which_idnum)} = {self.idnum_value}" 

95 

96 

97# ============================================================================= 

98# HL7PatientIdentifier 

99# ============================================================================= 

100 

101 

102# noinspection PyShadowingBuiltins 

103class HL7PatientIdentifier(object): 

104 """ 

105 Represents a patient identifier for the HL7 protocol. 

106 """ 

107 

108 def __init__( 

109 self, pid: str, id_type: str, assigning_authority: str 

110 ) -> None: 

111 self.pid = pid 

112 # ... called "pid" not "id" as type checker sometimes thinks "id" must 

113 # be integer, as in Python's id(object). 

114 self.id_type = id_type 

115 self.assigning_authority = assigning_authority 

116 

117 

118# ============================================================================= 

119# BarePatientInfo 

120# ============================================================================= 

121 

122 

123class BarePatientInfo(object): 

124 """ 

125 Represents information about a patient using a simple object with no 

126 connection to a database. 

127 

128 In some situations we avoid using 

129 :class:`camcops_server.cc_modules.cc_patient.Patient`: specifically, when 

130 we would otherwise have to deal with mutual dependency problems and the use 

131 of the database (prior to full database initialization). 

132 """ 

133 

134 def __init__( 

135 self, 

136 forename: str = None, 

137 surname: str = None, 

138 sex: str = None, 

139 dob: Optional[Date] = None, 

140 address: str = None, 

141 email: str = None, 

142 gp: str = None, 

143 otherdetails: str = None, 

144 idnum_definitions: List[IdNumReference] = None, 

145 ) -> None: 

146 self.forename = forename 

147 self.surname = surname 

148 self.sex = sex 

149 self.dob = dob 

150 self.address = address 

151 self.email = email 

152 self.gp = gp 

153 self.otherdetails = otherdetails 

154 self.idnum_definitions = ( 

155 idnum_definitions or [] 

156 ) # type: List[IdNumReference] 

157 

158 def __str__(self) -> str: 

159 return ( 

160 "Patient(forename={f!r}, surname={sur!r}, sex={sex!r}, DOB={dob}, " 

161 "address={a!r}, email={email!r}, gp={gp!r}, otherdetails={o!r}, " 

162 "idnums={i})".format( 

163 f=self.forename, 

164 sur=self.surname, 

165 sex=self.sex, 

166 dob=format_datetime(self.dob, DateFormat.ISO8601_DATE_ONLY), 

167 a=self.address, 

168 email=self.email, 

169 gp=self.gp, 

170 o=self.otherdetails, 

171 i="[{}]".format( 

172 ", ".join(str(idnum) for idnum in self.idnum_definitions) 

173 ), 

174 ) 

175 ) 

176 

177 def __repr__(self) -> str: 

178 return auto_repr(self) 

179 

180 def add_idnum(self, idref: IdNumReference) -> None: 

181 """ 

182 Adds an ID number. No checks in relation to what's already present. 

183 

184 Args: 

185 idref: a :class:`IdNumReference` 

186 """ 

187 self.idnum_definitions.append(idref) 

188 

189 def __eq__(self, other: object) -> bool: 

190 """ 

191 Do all data elements match those of ``other``? 

192 """ 

193 if not isinstance(other, BarePatientInfo): 

194 return False 

195 return ( 

196 self.forename == other.forename 

197 and self.surname == other.surname 

198 and self.sex == other.sex 

199 and self.dob == other.dob 

200 and self.address == other.address 

201 and self.email == other.email 

202 and self.gp == other.gp 

203 and self.otherdetails == other.otherdetails 

204 and self.idnum_definitions == other.idnum_definitions 

205 ) 

206 

207 

208# ============================================================================= 

209# Raw XML value 

210# ============================================================================= 

211 

212 

213class XmlSimpleValue(object): 

214 """ 

215 Represents XML lowest-level items. See functions in ``cc_xml.py``. 

216 """ 

217 

218 def __init__(self, value: Any) -> None: 

219 self.value = value 

220 

221 

222# ============================================================================= 

223# TaskExportOptions 

224# ============================================================================= 

225 

226 

227class TaskExportOptions(object): 

228 """ 

229 Information-holding object for options controlling XML and other 

230 representations of tasks. 

231 """ 

232 

233 def __init__( 

234 self, 

235 db_patient_id_per_row: bool = False, 

236 db_make_all_tables_even_empty: bool = False, 

237 db_include_summaries: bool = False, 

238 include_blobs: bool = False, 

239 xml_include_ancillary: bool = False, 

240 xml_include_calculated: bool = False, 

241 xml_include_comments: bool = True, 

242 xml_include_patient: bool = False, 

243 xml_include_plain_columns: bool = False, 

244 xml_include_snomed: bool = False, 

245 xml_skip_fields: List[str] = None, 

246 xml_sort_by_name: bool = True, 

247 xml_with_header_comments: bool = False, 

248 ) -> None: 

249 """ 

250 Args: 

251 db_patient_id_per_row: 

252 generates an anonymisation staging database -- that is, a 

253 database with patient IDs in every row of every table, suitable 

254 for feeding into an anonymisation system like CRATE 

255 (https://doi.org/10.1186%2Fs12911-017-0437-1). 

256 db_make_all_tables_even_empty: 

257 create all tables, even empty ones 

258 

259 include_blobs: 

260 include binary large objects (BLOBs) (applies to several export 

261 formats) 

262 

263 xml_include_ancillary: 

264 include ancillary tables as well as the main? 

265 xml_include_calculated: 

266 include fields calculated by the task 

267 xml_include_comments: 

268 include comments in XML? 

269 xml_include_patient: 

270 include patient details? 

271 xml_include_plain_columns: 

272 include the base columns 

273 xml_include_snomed: 

274 include SNOMED-CT codes, if available? 

275 xml_skip_fields: 

276 fieldnames to skip 

277 xml_sort_by_name: 

278 sort by field/attribute names? 

279 xml_with_header_comments: 

280 include header-style comments? 

281 """ 

282 self.db_patient_id_in_each_row = db_patient_id_per_row 

283 self.db_make_all_tables_even_empty = db_make_all_tables_even_empty 

284 self.db_include_summaries = db_include_summaries 

285 

286 self.include_blobs = include_blobs 

287 

288 self.xml_include_ancillary = xml_include_ancillary 

289 self.xml_include_calculated = xml_include_calculated 

290 self.xml_include_comments = xml_include_comments 

291 self.xml_include_patient = xml_include_patient 

292 self.xml_include_plain_columns = xml_include_plain_columns 

293 self.xml_include_snomed = xml_include_snomed 

294 self.xml_skip_fields = xml_skip_fields or [] # type: List[str] 

295 self.xml_sort_by_name = xml_sort_by_name 

296 self.xml_with_header_comments = xml_with_header_comments 

297 

298 def clone(self) -> "TaskExportOptions": 

299 """ 

300 Returns a copy of this object. 

301 """ 

302 return copy.copy(self)