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
« 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"""
27import datetime
29import sqlalchemy as sa
30from sqlalchemy import orm
31from sqlalchemy.ext.declarative import declared_attr
32from sqlalchemy.ext.orderinglist import ordering_list
34from wuttjamaican.db.model import uuid_column, uuid_fk_column, User
35from wuttjamaican.db.util import UUID
38class BatchMixin:
39 """
40 Mixin base class for :term:`data models <data model>` which
41 represent a :term:`batch`.
43 See also :class:`BatchRowMixin` which should be used for the row
44 model.
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`.
50 .. attribute:: batch_type
52 This is the canonical :term:`batch type` for the batch model.
54 By default this will match the underlying table name for the
55 batch, but the model class can set it explicitly to override.
57 .. attribute:: __row_class__
59 Reference to the specific :term:`data model` class used for the
60 :term:`batch rows <batch row>`.
62 This will be a subclass of :class:`BatchRowMixin` (among other
63 classes).
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__`.
69 .. attribute:: id
71 Numeric ID for the batch, unique across all batches (regardless
72 of type).
74 See also :attr:`id_str`.
76 .. attribute:: description
78 Simple description for the batch.
80 .. attribute:: notes
82 Arbitrary notes for the batch.
84 .. attribute:: rows
86 List of data rows for the batch, aka. :term:`batch rows <batch
87 row>`.
89 Each will be an instance of :class:`BatchRowMixin` (among other
90 base classes).
92 .. attribute:: row_count
94 Cached row count for the batch, i.e. how many :attr:`rows` it has.
96 No guarantees perhaps, but this should ideally be accurate (it
97 ultimately depends on the :term:`batch handler`
98 implementation).
100 .. attribute:: STATUS
102 Dict of possible batch status codes and their human-readable
103 names.
105 Each key will be a possible :attr:`status_code` and the
106 corresponding value will be the human-readable name.
108 See also :attr:`status_text` for when more detail/subtlety is
109 needed.
111 Typically each "key" (code) is also defined as its own
112 "constant" on the model class. For instance::
114 from collections import OrderedDict
115 from wuttjamaican.db import model
117 class MyBatch(model.BatchMixin, model.Base):
118 \""" my custom batch \"""
120 STATUS_INCOMPLETE = 1
121 STATUS_EXECUTABLE = 2
123 STATUS = OrderedDict([
124 (STATUS_INCOMPLETE, "incomplete"),
125 (STATUS_EXECUTABLE, "executable"),
126 ])
128 # TODO: column definitions...
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.
134 .. note::
135 There is not any built-in logic around these integer codes;
136 subclass can use any the developer prefers.
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).
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.
147 .. attribute:: status_code
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.
152 This must correspond to an existing key within the
153 :attr:`STATUS` dict.
155 See also :attr:`status_text`.
157 .. attribute:: status_text
159 Text which may (briefly) further explain the batch
160 :attr:`status_code`, if needed.
162 For example, assuming built-in default :attr:`STATUS`
163 definition::
165 batch.status_code = batch.STATUS_INCOMPLETE
166 batch.status_text = "cannot execute batch because it is missing something"
168 .. attribute:: created
170 When the batch was first created.
172 .. attribute:: created_by
174 Reference to the :class:`~wuttjamaican.db.model.auth.User` who
175 first created the batch.
177 .. attribute:: executed
179 When the batch was executed.
181 .. attribute:: executed_by
183 Reference to the :class:`~wuttjamaican.db.model.auth.User` who
184 executed the batch.
185 """
187 @declared_attr
188 def __table_args__(cls):
189 return cls.__default_table_args__()
191 @classmethod
192 def __default_table_args__(cls):
193 return cls.__batch_table_args__()
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 )
202 @declared_attr
203 def batch_type(cls):
204 return cls.__tablename__
206 uuid = uuid_column()
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)
213 STATUS_INCOMPLETE = 1
214 STATUS_EXECUTABLE = 2
216 STATUS = {
217 STATUS_INCOMPLETE : "incomplete",
218 STATUS_EXECUTABLE : "executable",
219 }
221 status_code = sa.Column(sa.Integer(), nullable=True)
222 status_text = sa.Column(sa.String(length=255), nullable=True)
224 created = sa.Column(sa.DateTime(timezone=True), nullable=False,
225 default=datetime.datetime.now)
226 created_by_uuid = sa.Column(UUID(), nullable=False)
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)
237 executed = sa.Column(sa.DateTime(timezone=True), nullable=True)
238 executed_by_uuid = sa.Column(UUID(), nullable=True)
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)
248 def __repr__(self):
249 cls = self.__class__.__name__
250 return f"{cls}(uuid={repr(self.uuid)})"
252 def __str__(self):
253 return self.id_str if self.id else "(new)"
255 @property
256 def id_str(self):
257 """
258 Property which returns the :attr:`id` as a string, zero-padded
259 to 8 digits::
261 batch.id = 42
262 print(batch.id_str) # => '00000042'
263 """
264 if self.id:
265 return f'{self.id:08d}'
268class BatchRowMixin:
269 """
270 Mixin base class for :term:`data models <data model>` which
271 represent a :term:`batch row`.
273 See also :class:`BatchMixin` which should be used for the (parent)
274 batch model.
276 .. attribute:: __batch_class__
278 Reference to the :term:`data model` for the parent
279 :term:`batch` class.
281 This will be a subclass of :class:`BatchMixin` (among other
282 classes).
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.
288 .. attribute:: batch
290 Reference to the parent :term:`batch` to which the row belongs.
292 This will be an instance of :class:`BatchMixin` (among other
293 base classes).
295 .. attribute:: sequence
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.
300 .. attribute:: STATUS
302 Dict of possible row status codes and their human-readable
303 names.
305 Each key will be a possible :attr:`status_code` and the
306 corresponding value will be the human-readable name.
308 See also :attr:`status_text` for when more detail/subtlety is
309 needed.
311 Typically each "key" (code) is also defined as its own
312 "constant" on the model class. For instance::
314 from collections import OrderedDict
315 from wuttjamaican.db import model
317 class MyBatchRow(model.BatchRowMixin, model.Base):
318 \""" my custom batch row \"""
320 STATUS_INVALID = 1
321 STATUS_GOOD_TO_GO = 2
323 STATUS = OrderedDict([
324 (STATUS_INVALID, "invalid"),
325 (STATUS_GOOD_TO_GO, "good to go"),
326 ])
328 # TODO: column definitions...
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.
335 .. note::
336 There is not any built-in logic around these integer codes;
337 subclass can use any the developer prefers.
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).
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.
348 .. attribute:: status_code
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.
353 This must correspond to an existing key within the
354 :attr:`STATUS` dict.
356 See also :attr:`status_text`.
358 .. attribute:: status_text
360 Text which may (briefly) further explain the row
361 :attr:`status_code`, if needed.
363 For instance, assuming the example :attr:`STATUS` definition
364 shown above::
366 row.status_code = row.STATUS_INVALID
367 row.status_text = "input data for this row is missing fields: foo, bar"
369 .. attribute:: modified
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 """
376 uuid = uuid_column()
378 @declared_attr
379 def __table_args__(cls):
380 return cls.__default_table_args__()
382 @classmethod
383 def __default_table_args__(cls):
384 return cls.__batchrow_table_args__()
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 )
393 batch_uuid = sa.Column(UUID(), nullable=False)
395 @declared_attr
396 def batch(cls):
397 batch_class = cls.__batch_class__
398 row_class = cls
399 batch_class.__row_class__ = row_class
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')
411 # now, here's the `BatchRow.batch`
412 return orm.relationship(
413 batch_class,
414 back_populates='rows',
415 cascade_backrefs=False)
417 sequence = sa.Column(sa.Integer(), nullable=False)
419 STATUS = {}
421 status_code = sa.Column(sa.Integer(), nullable=True)
422 status_text = sa.Column(sa.String(length=255), nullable=True)
424 modified = sa.Column(sa.DateTime(timezone=True), nullable=True,
425 default=datetime.datetime.now,
426 onupdate=datetime.datetime.now)