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
« 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"""
27import sqlalchemy as sa
28from sqlalchemy import orm
29from sqlalchemy.ext.declarative import declared_attr
30from sqlalchemy.ext.orderinglist import ordering_list
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
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): # pylint: disable=no-self-argument
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): # pylint: disable=empty-docstring,no-self-argument
204 """ """
205 return cls.__tablename__
207 uuid = uuid_column()
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)
214 STATUS_INCOMPLETE = 1
215 STATUS_EXECUTABLE = 2
217 STATUS = {
218 STATUS_INCOMPLETE: "incomplete",
219 STATUS_EXECUTABLE: "executable",
220 }
222 status_code = sa.Column(sa.Integer(), nullable=True)
223 status_text = sa.Column(sa.String(length=255), nullable=True)
225 created = sa.Column(sa.DateTime(), nullable=False, default=make_utc)
226 created_by_uuid = sa.Column(UUID(), nullable=False)
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 )
238 executed = sa.Column(sa.DateTime(), nullable=True)
239 executed_by_uuid = sa.Column(UUID(), nullable=True)
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 )
251 def __repr__(self):
252 cls = self.__class__.__name__
253 return f"{cls}(uuid={repr(self.uuid)})"
255 def __str__(self):
256 return self.id_str if self.id else "(new)"
258 @property
259 def id_str(self):
260 """
261 Property which returns the :attr:`id` as a string, zero-padded
262 to 8 digits::
264 batch.id = 42
265 print(batch.id_str) # => '00000042'
266 """
267 if self.id:
268 return f"{self.id:08d}"
269 return None
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`.
277 See also :class:`BatchMixin` which should be used for the (parent)
278 batch model.
280 .. attribute:: __batch_class__
282 Reference to the :term:`data model` for the parent
283 :term:`batch` class.
285 This will be a subclass of :class:`BatchMixin` (among other
286 classes).
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.
292 .. attribute:: batch
294 Reference to the parent :term:`batch` to which the row belongs.
296 This will be an instance of :class:`BatchMixin` (among other
297 base classes).
299 .. attribute:: sequence
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.
304 .. attribute:: STATUS
306 Dict of possible row status codes and their human-readable
307 names.
309 Each key will be a possible :attr:`status_code` and the
310 corresponding value will be the human-readable name.
312 See also :attr:`status_text` for when more detail/subtlety is
313 needed.
315 Typically each "key" (code) is also defined as its own
316 "constant" on the model class. For instance::
318 from collections import OrderedDict
319 from wuttjamaican.db import model
321 class MyBatchRow(model.BatchRowMixin, model.Base):
322 \""" my custom batch row \"""
324 STATUS_INVALID = 1
325 STATUS_GOOD_TO_GO = 2
327 STATUS = OrderedDict([
328 (STATUS_INVALID, "invalid"),
329 (STATUS_GOOD_TO_GO, "good to go"),
330 ])
332 # TODO: column definitions...
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.
339 .. note::
340 There is not any built-in logic around these integer codes;
341 subclass can use any the developer prefers.
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).
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.
352 .. attribute:: status_code
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.
357 This must correspond to an existing key within the
358 :attr:`STATUS` dict.
360 See also :attr:`status_text`.
362 .. attribute:: status_text
364 Text which may (briefly) further explain the row
365 :attr:`status_code`, if needed.
367 For instance, assuming the example :attr:`STATUS` definition
368 shown above::
370 row.status_code = row.STATUS_INVALID
371 row.status_text = "input data for this row is missing fields: foo, bar"
373 .. attribute:: modified
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 """
380 uuid = uuid_column()
382 @declared_attr
383 def __table_args__(cls): # pylint: disable=no-self-argument
384 return cls.__default_table_args__()
386 @classmethod
387 def __default_table_args__(cls):
388 return cls.__batchrow_table_args__()
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"]),)
395 batch_uuid = sa.Column(UUID(), nullable=False)
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
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 )
415 # now, here's the `BatchRow.batch`
416 return orm.relationship(
417 batch_class, back_populates="rows", cascade_backrefs=False
418 )
420 sequence = sa.Column(sa.Integer(), nullable=False)
422 STATUS = {}
424 status_code = sa.Column(sa.Integer(), nullable=True)
425 status_text = sa.Column(sa.String(length=255), nullable=True)
427 modified = sa.Column(
428 sa.DateTime(), nullable=True, default=make_utc, onupdate=make_utc
429 )