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

76 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-12-15 16:36 -0600

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

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

3# 

4# WuttJamaican -- Base package for Wutta Framework 

5# Copyright © 2023-2025 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 sqlalchemy as sa 

28from sqlalchemy import orm 

29from sqlalchemy.ext.declarative import declared_attr 

30from sqlalchemy.ext.orderinglist import ordering_list 

31 

32from wuttjamaican.db.model.base import uuid_column 

33from wuttjamaican.db.model.auth import User 

34from wuttjamaican.db.util import UUID 

35from wuttjamaican.util import make_utc 

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): # pylint: disable=no-self-argument 

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): # pylint: disable=empty-docstring,no-self-argument 

204 """ """ 

205 return cls.__tablename__ 

206 

207 uuid = uuid_column() 

208 

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

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

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

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

213 

214 STATUS_INCOMPLETE = 1 

215 STATUS_EXECUTABLE = 2 

216 

217 STATUS = { 

218 STATUS_INCOMPLETE: "incomplete", 

219 STATUS_EXECUTABLE: "executable", 

220 } 

221 

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

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

224 

225 created = sa.Column(sa.DateTime(), nullable=False, default=make_utc) 

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

227 

228 @declared_attr 

229 def created_by(cls): # pylint: disable=empty-docstring,no-self-argument 

230 """ """ 

231 return orm.relationship( 

232 User, 

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

234 foreign_keys=lambda: [cls.created_by_uuid], 

235 cascade_backrefs=False, 

236 ) 

237 

238 executed = sa.Column(sa.DateTime(), nullable=True) 

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

240 

241 @declared_attr 

242 def executed_by(cls): # pylint: disable=empty-docstring,no-self-argument 

243 """ """ 

244 return orm.relationship( 

245 User, 

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

247 foreign_keys=lambda: [cls.executed_by_uuid], 

248 cascade_backrefs=False, 

249 ) 

250 

251 def __repr__(self): 

252 cls = self.__class__.__name__ 

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

254 

255 def __str__(self): 

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

257 

258 @property 

259 def id_str(self): 

260 """ 

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

262 to 8 digits:: 

263 

264 batch.id = 42 

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

266 """ 

267 if self.id: 

268 return f"{self.id:08d}" 

269 return None 

270 

271 

272class BatchRowMixin: # pylint: disable=too-few-public-methods 

273 """ 

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

275 represent a :term:`batch row`. 

276 

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

278 batch model. 

279 

280 .. attribute:: __batch_class__ 

281 

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

283 :term:`batch` class. 

284 

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

286 classes). 

287 

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

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

290 set automatically to match. 

291 

292 .. attribute:: batch 

293 

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

295 

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

297 base classes). 

298 

299 .. attribute:: sequence 

300 

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

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

303 

304 .. attribute:: STATUS 

305 

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

307 names. 

308 

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

310 corresponding value will be the human-readable name. 

311 

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

313 needed. 

314 

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

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

317 

318 from collections import OrderedDict 

319 from wuttjamaican.db import model 

320 

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

322 \""" my custom batch row \""" 

323 

324 STATUS_INVALID = 1 

325 STATUS_GOOD_TO_GO = 2 

326 

327 STATUS = OrderedDict([ 

328 (STATUS_INVALID, "invalid"), 

329 (STATUS_GOOD_TO_GO, "good to go"), 

330 ]) 

331 

332 # TODO: column definitions... 

333 

334 Whereas there is a built-in default for the 

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

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

337 definition entirely, in similar fashion to above. 

338 

339 .. note:: 

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

341 subclass can use any the developer prefers. 

342 

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

344 you should not then change its fundamental meaning (although 

345 you can change the human-readable text). 

346 

347 It's recommended to use 

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

349 ensure the possible status codes are displayed in the 

350 correct order, when applicable. 

351 

352 .. attribute:: status_code 

353 

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

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

356 

357 This must correspond to an existing key within the 

358 :attr:`STATUS` dict. 

359 

360 See also :attr:`status_text`. 

361 

362 .. attribute:: status_text 

363 

364 Text which may (briefly) further explain the row 

365 :attr:`status_code`, if needed. 

366 

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

368 shown above:: 

369 

370 row.status_code = row.STATUS_INVALID 

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

372 

373 .. attribute:: modified 

374 

375 Last modification time of the row. This should be 

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

377 anytime it's updated thereafter. 

378 """ 

379 

380 uuid = uuid_column() 

381 

382 @declared_attr 

383 def __table_args__(cls): # pylint: disable=no-self-argument 

384 return cls.__default_table_args__() 

385 

386 @classmethod 

387 def __default_table_args__(cls): 

388 return cls.__batchrow_table_args__() 

389 

390 @classmethod 

391 def __batchrow_table_args__(cls): 

392 batch_table = cls.__batch_class__.__tablename__ 

393 return (sa.ForeignKeyConstraint(["batch_uuid"], [f"{batch_table}.uuid"]),) 

394 

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

396 

397 @declared_attr 

398 def batch(cls): # pylint: disable=empty-docstring,no-self-argument 

399 """ """ 

400 batch_class = cls.__batch_class__ 

401 row_class = cls 

402 batch_class.__row_class__ = row_class 

403 

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

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

406 batch_class.rows = orm.relationship( 

407 row_class, 

408 order_by=lambda: row_class.sequence, 

409 collection_class=ordering_list("sequence", count_from=1), 

410 cascade="all, delete-orphan", 

411 cascade_backrefs=False, 

412 back_populates="batch", 

413 ) 

414 

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

416 return orm.relationship( 

417 batch_class, back_populates="rows", cascade_backrefs=False 

418 ) 

419 

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

421 

422 STATUS = {} 

423 

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

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

426 

427 modified = sa.Column( 

428 sa.DateTime(), nullable=True, default=make_utc, onupdate=make_utc 

429 )