Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/db/model/batch.py: 100%

75 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2025-01-06 17:01 -0600

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# WuttJamaican -- Base package for Wutta Framework 

5# Copyright © 2023-2024 Lance Edgar 

6# 

7# This file is part of Wutta Framework. 

8# 

9# Wutta Framework is free software: you can redistribute it and/or modify it 

10# under the terms of the GNU General Public License as published by the Free 

11# Software Foundation, either version 3 of the License, or (at your option) any 

12# later version. 

13# 

14# Wutta Framework is distributed in the hope that it will be useful, but 

15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

17# more details. 

18# 

19# You should have received a copy of the GNU General Public License along with 

20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>. 

21# 

22################################################################################ 

23""" 

24Batch data models 

25""" 

26 

27import datetime 

28 

29import sqlalchemy as sa 

30from sqlalchemy import orm 

31from sqlalchemy.ext.declarative import declared_attr 

32from sqlalchemy.ext.orderinglist import ordering_list 

33 

34from wuttjamaican.db.model import uuid_column, uuid_fk_column, User 

35from wuttjamaican.db.util import UUID 

36 

37 

38class BatchMixin: 

39 """ 

40 Mixin base class for :term:`data models <data model>` which 

41 represent a :term:`batch`. 

42 

43 See also :class:`BatchRowMixin` which should be used for the row 

44 model. 

45 

46 For a batch model (table) to be useful, at least one :term:`batch 

47 handler` must be defined, which is able to process data for that 

48 :term:`batch type`. 

49 

50 .. attribute:: batch_type 

51 

52 This is the canonical :term:`batch type` for the batch model. 

53 

54 By default this will match the underlying table name for the 

55 batch, but the model class can set it explicitly to override. 

56 

57 .. attribute:: __row_class__ 

58 

59 Reference to the specific :term:`data model` class used for the 

60 :term:`batch rows <batch row>`. 

61 

62 This will be a subclass of :class:`BatchRowMixin` (among other 

63 classes). 

64 

65 When defining the batch model, you do not have to set this as 

66 it will be assigned automatically based on 

67 :attr:`BatchRowMixin.__batch_class__`. 

68 

69 .. attribute:: id 

70 

71 Numeric ID for the batch, unique across all batches (regardless 

72 of type). 

73 

74 See also :attr:`id_str`. 

75 

76 .. attribute:: description 

77 

78 Simple description for the batch. 

79 

80 .. attribute:: notes 

81 

82 Arbitrary notes for the batch. 

83 

84 .. attribute:: rows 

85 

86 List of data rows for the batch, aka. :term:`batch rows <batch 

87 row>`. 

88 

89 Each will be an instance of :class:`BatchRowMixin` (among other 

90 base classes). 

91 

92 .. attribute:: row_count 

93 

94 Cached row count for the batch, i.e. how many :attr:`rows` it has. 

95 

96 No guarantees perhaps, but this should ideally be accurate (it 

97 ultimately depends on the :term:`batch handler` 

98 implementation). 

99 

100 .. attribute:: STATUS 

101 

102 Dict of possible batch status codes and their human-readable 

103 names. 

104 

105 Each key will be a possible :attr:`status_code` and the 

106 corresponding value will be the human-readable name. 

107 

108 See also :attr:`status_text` for when more detail/subtlety is 

109 needed. 

110 

111 Typically each "key" (code) is also defined as its own 

112 "constant" on the model class. For instance:: 

113 

114 from collections import OrderedDict 

115 from wuttjamaican.db import model 

116 

117 class MyBatch(model.BatchMixin, model.Base): 

118 \""" my custom batch \""" 

119 

120 STATUS_INCOMPLETE = 1 

121 STATUS_EXECUTABLE = 2 

122 

123 STATUS = OrderedDict([ 

124 (STATUS_INCOMPLETE, "incomplete"), 

125 (STATUS_EXECUTABLE, "executable"), 

126 ]) 

127 

128 # TODO: column definitions... 

129 

130 And in fact, the above status definition is the built-in 

131 default. However it is expected for subclass to overwrite the 

132 definition entirely (in similar fashion to above) when needed. 

133 

134 .. note:: 

135 There is not any built-in logic around these integer codes; 

136 subclass can use any the developer prefers. 

137 

138 Of course, once you define one, if any live batches use it, 

139 you should not then change its fundamental meaning (although 

140 you can change the human-readable text). 

141 

142 It's recommended to use 

143 :class:`~python:collections.OrderedDict` (as shown above) to 

144 ensure the possible status codes are displayed in the 

145 correct order, when applicable. 

146 

147 .. attribute:: status_code 

148 

149 Status code for the batch as a whole. This indicates whether 

150 the batch is "okay" and ready to execute, or (why) not etc. 

151 

152 This must correspond to an existing key within the 

153 :attr:`STATUS` dict. 

154 

155 See also :attr:`status_text`. 

156 

157 .. attribute:: status_text 

158 

159 Text which may (briefly) further explain the batch 

160 :attr:`status_code`, if needed. 

161 

162 For example, assuming built-in default :attr:`STATUS` 

163 definition:: 

164 

165 batch.status_code = batch.STATUS_INCOMPLETE 

166 batch.status_text = "cannot execute batch because it is missing something" 

167 

168 .. attribute:: created 

169 

170 When the batch was first created. 

171 

172 .. attribute:: created_by 

173 

174 Reference to the :class:`~wuttjamaican.db.model.auth.User` who 

175 first created the batch. 

176 

177 .. attribute:: executed 

178 

179 When the batch was executed. 

180 

181 .. attribute:: executed_by 

182 

183 Reference to the :class:`~wuttjamaican.db.model.auth.User` who 

184 executed the batch. 

185 """ 

186 

187 @declared_attr 

188 def __table_args__(cls): 

189 return cls.__default_table_args__() 

190 

191 @classmethod 

192 def __default_table_args__(cls): 

193 return cls.__batch_table_args__() 

194 

195 @classmethod 

196 def __batch_table_args__(cls): 

197 return ( 

198 sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid']), 

199 sa.ForeignKeyConstraint(['executed_by_uuid'], ['user.uuid']), 

200 ) 

201 

202 @declared_attr 

203 def batch_type(cls): 

204 return cls.__tablename__ 

205 

206 uuid = uuid_column() 

207 

208 id = sa.Column(sa.Integer(), nullable=False) 

209 description = sa.Column(sa.String(length=255), nullable=True) 

210 notes = sa.Column(sa.Text(), nullable=True) 

211 row_count = sa.Column(sa.Integer(), nullable=True, default=0) 

212 

213 STATUS_INCOMPLETE = 1 

214 STATUS_EXECUTABLE = 2 

215 

216 STATUS = { 

217 STATUS_INCOMPLETE : "incomplete", 

218 STATUS_EXECUTABLE : "executable", 

219 } 

220 

221 status_code = sa.Column(sa.Integer(), nullable=True) 

222 status_text = sa.Column(sa.String(length=255), nullable=True) 

223 

224 created = sa.Column(sa.DateTime(timezone=True), nullable=False, 

225 default=datetime.datetime.now) 

226 created_by_uuid = sa.Column(UUID(), nullable=False) 

227 

228 @declared_attr 

229 def created_by(cls): 

230 return orm.relationship( 

231 User, 

232 primaryjoin=lambda: User.uuid == cls.created_by_uuid, 

233 foreign_keys=lambda: [cls.created_by_uuid], 

234 cascade_backrefs=False) 

235 

236 

237 executed = sa.Column(sa.DateTime(timezone=True), nullable=True) 

238 executed_by_uuid = sa.Column(UUID(), nullable=True) 

239 

240 @declared_attr 

241 def executed_by(cls): 

242 return orm.relationship( 

243 User, 

244 primaryjoin=lambda: User.uuid == cls.executed_by_uuid, 

245 foreign_keys=lambda: [cls.executed_by_uuid], 

246 cascade_backrefs=False) 

247 

248 def __repr__(self): 

249 cls = self.__class__.__name__ 

250 return f"{cls}(uuid={repr(self.uuid)})" 

251 

252 def __str__(self): 

253 return self.id_str if self.id else "(new)" 

254 

255 @property 

256 def id_str(self): 

257 """ 

258 Property which returns the :attr:`id` as a string, zero-padded 

259 to 8 digits:: 

260 

261 batch.id = 42 

262 print(batch.id_str) # => '00000042' 

263 """ 

264 if self.id: 

265 return f'{self.id:08d}' 

266 

267 

268class BatchRowMixin: 

269 """ 

270 Mixin base class for :term:`data models <data model>` which 

271 represent a :term:`batch row`. 

272 

273 See also :class:`BatchMixin` which should be used for the (parent) 

274 batch model. 

275 

276 .. attribute:: __batch_class__ 

277 

278 Reference to the :term:`data model` for the parent 

279 :term:`batch` class. 

280 

281 This will be a subclass of :class:`BatchMixin` (among other 

282 classes). 

283 

284 When defining the batch row model, you must set this attribute 

285 explicitly! And then :attr:`BatchMixin.__row_class__` will be 

286 set automatically to match. 

287 

288 .. attribute:: batch 

289 

290 Reference to the parent :term:`batch` to which the row belongs. 

291 

292 This will be an instance of :class:`BatchMixin` (among other 

293 base classes). 

294 

295 .. attribute:: sequence 

296 

297 Sequence (aka. line) number for the row, within the parent 

298 batch. This is 1-based so the first row has sequence 1, etc. 

299 

300 .. attribute:: STATUS 

301 

302 Dict of possible row status codes and their human-readable 

303 names. 

304 

305 Each key will be a possible :attr:`status_code` and the 

306 corresponding value will be the human-readable name. 

307 

308 See also :attr:`status_text` for when more detail/subtlety is 

309 needed. 

310 

311 Typically each "key" (code) is also defined as its own 

312 "constant" on the model class. For instance:: 

313 

314 from collections import OrderedDict 

315 from wuttjamaican.db import model 

316 

317 class MyBatchRow(model.BatchRowMixin, model.Base): 

318 \""" my custom batch row \""" 

319 

320 STATUS_INVALID = 1 

321 STATUS_GOOD_TO_GO = 2 

322 

323 STATUS = OrderedDict([ 

324 (STATUS_INVALID, "invalid"), 

325 (STATUS_GOOD_TO_GO, "good to go"), 

326 ]) 

327 

328 # TODO: column definitions... 

329 

330 Whereas there is a built-in default for the 

331 :attr:`BatchMixin.STATUS`, there is no built-in default defined 

332 for the ``BatchRowMixin.STATUS``. Subclass must overwrite the 

333 definition entirely, in similar fashion to above. 

334 

335 .. note:: 

336 There is not any built-in logic around these integer codes; 

337 subclass can use any the developer prefers. 

338 

339 Of course, once you define one, if any live batches use it, 

340 you should not then change its fundamental meaning (although 

341 you can change the human-readable text). 

342 

343 It's recommended to use 

344 :class:`~python:collections.OrderedDict` (as shown above) to 

345 ensure the possible status codes are displayed in the 

346 correct order, when applicable. 

347 

348 .. attribute:: status_code 

349 

350 Current status code for the row. This indicates if the row is 

351 "good to go" or has "warnings" or is outright "invalid" etc. 

352 

353 This must correspond to an existing key within the 

354 :attr:`STATUS` dict. 

355 

356 See also :attr:`status_text`. 

357 

358 .. attribute:: status_text 

359 

360 Text which may (briefly) further explain the row 

361 :attr:`status_code`, if needed. 

362 

363 For instance, assuming the example :attr:`STATUS` definition 

364 shown above:: 

365 

366 row.status_code = row.STATUS_INVALID 

367 row.status_text = "input data for this row is missing fields: foo, bar" 

368 

369 .. attribute:: modified 

370 

371 Last modification time of the row. This should be 

372 automatically set when the row is first created, as well as 

373 anytime it's updated thereafter. 

374 """ 

375 

376 uuid = uuid_column() 

377 

378 @declared_attr 

379 def __table_args__(cls): 

380 return cls.__default_table_args__() 

381 

382 @classmethod 

383 def __default_table_args__(cls): 

384 return cls.__batchrow_table_args__() 

385 

386 @classmethod 

387 def __batchrow_table_args__(cls): 

388 batch_table = cls.__batch_class__.__tablename__ 

389 return ( 

390 sa.ForeignKeyConstraint(['batch_uuid'], [f'{batch_table}.uuid']), 

391 ) 

392 

393 batch_uuid = sa.Column(UUID(), nullable=False) 

394 

395 @declared_attr 

396 def batch(cls): 

397 batch_class = cls.__batch_class__ 

398 row_class = cls 

399 batch_class.__row_class__ = row_class 

400 

401 # must establish `Batch.rows` here instead of from within the 

402 # Batch above, because BatchRow class doesn't yet exist above. 

403 batch_class.rows = orm.relationship( 

404 row_class, 

405 order_by=lambda: row_class.sequence, 

406 collection_class=ordering_list('sequence', count_from=1), 

407 cascade='all, delete-orphan', 

408 cascade_backrefs=False, 

409 back_populates='batch') 

410 

411 # now, here's the `BatchRow.batch` 

412 return orm.relationship( 

413 batch_class, 

414 back_populates='rows', 

415 cascade_backrefs=False) 

416 

417 sequence = sa.Column(sa.Integer(), nullable=False) 

418 

419 STATUS = {} 

420 

421 status_code = sa.Column(sa.Integer(), nullable=True) 

422 status_text = sa.Column(sa.String(length=255), nullable=True) 

423 

424 modified = sa.Column(sa.DateTime(timezone=True), nullable=True, 

425 default=datetime.datetime.now, 

426 onupdate=datetime.datetime.now)