Coverage for .tox/coverage/lib/python3.13/site-packages/wuttjamaican/db/util.py: 100%
62 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-22 12:31 -0500
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-22 12:31 -0500
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"""
24Database Utilities
25"""
27import uuid as _uuid
28from importlib.metadata import version
30from packaging.version import Version
31import sqlalchemy as sa
32from sqlalchemy import orm
33from sqlalchemy.dialects.postgresql import UUID as PGUUID
35from wuttjamaican.util import make_true_uuid
37# nb. this convention comes from upstream docs
38# https://docs.sqlalchemy.org/en/14/core/constraints.html#constraint-naming-conventions
39naming_convention = {
40 "ix": "ix_%(column_0_label)s",
41 "uq": "uq_%(table_name)s_%(column_0_name)s",
42 "ck": "ck_%(table_name)s_%(constraint_name)s",
43 "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
44 "pk": "pk_%(table_name)s",
45}
48SA2 = True # pylint: disable=invalid-name
49if Version(version("SQLAlchemy")) < Version("2"): # pragma: no cover
50 SA2 = False # pylint: disable=invalid-name
53class ModelBase: # pylint: disable=empty-docstring
54 """ """
56 def __iter__(self):
57 # nb. we override this to allow for `dict(self)`
58 # nb. this does *not* include association proxy values;
59 # see also wuttjamaican.util.get_value()
60 state = sa.inspect(self)
61 fields = [attr.key for attr in state.attrs]
62 return iter([(field, getattr(self, field)) for field in fields])
64 def __getitem__(self, key):
65 # nb. we override this to allow for `x = self['field']`
66 state = sa.inspect(self)
67 if hasattr(state.attrs, key):
68 return getattr(self, key)
69 raise KeyError(
70 f"{self.__class__.__name__} instance has no attr with key: {key}"
71 )
74class UUID(
75 sa.types.TypeDecorator
76): # pylint: disable=abstract-method,too-many-ancestors
77 """
78 Platform-independent UUID type.
80 Uses PostgreSQL's UUID type, otherwise uses CHAR(32), storing as
81 stringified hex values.
83 This type definition is based on example from the `SQLAlchemy
84 documentation
85 <https://docs.sqlalchemy.org/en/14/core/custom_types.html#backend-agnostic-guid-type>`_.
86 """
88 impl = sa.CHAR
89 cache_ok = True
90 """ """ # nb. suppress sphinx autodoc for cache_ok
92 def load_dialect_impl(self, dialect): # pylint: disable=empty-docstring
93 """ """
94 if dialect.name == "postgresql":
95 return dialect.type_descriptor(PGUUID())
96 return dialect.type_descriptor(sa.CHAR(32))
98 def process_bind_param(self, value, dialect): # pylint: disable=empty-docstring
99 """ """
100 if value is None:
101 return value
103 if dialect.name == "postgresql":
104 return str(value)
106 if not isinstance(value, _uuid.UUID):
107 value = _uuid.UUID(value)
109 # hexstring
110 return f"{value.int:032x}"
112 def process_result_value(
113 self, value, dialect
114 ): # pylint: disable=unused-argument,empty-docstring
115 """ """
116 if value is None:
117 return value
118 if not isinstance(value, _uuid.UUID):
119 value = _uuid.UUID(value)
120 return value
123def uuid_column(*args, **kwargs):
124 """
125 Returns a UUID column for use as a table's primary key.
126 """
127 if not args:
128 args = (UUID(),)
129 kwargs.setdefault("primary_key", True)
130 kwargs.setdefault("nullable", False)
131 kwargs.setdefault("default", make_true_uuid)
132 if kwargs["primary_key"]:
133 kwargs.setdefault("doc", "UUID primary key for the table.")
134 return sa.Column(*args, **kwargs)
137def uuid_fk_column(target_column, *args, **kwargs):
138 """
139 Returns a UUID column for use as a foreign key to another table.
141 :param target_column: Name of the table column on the remote side,
142 e.g. ``'user.uuid'``.
143 """
144 if not args:
145 args = (UUID(), sa.ForeignKey(target_column))
146 return sa.Column(*args, **kwargs)
149def make_topo_sortkey(model):
150 """
151 Returns a function suitable for use as a ``key`` kwarg to a
152 standard Python sorting call. This key function will expect a
153 single class mapper and return a sequence number associated with
154 that model. The sequence is determined by SQLAlchemy's
155 topological table sorting.
157 :param model: Usually the :term:`app model`, but can be any module
158 containing model classes.
159 """
160 metadata = model.Base.metadata
161 tables = {table.name: i for i, table in enumerate(metadata.sorted_tables, 1)}
163 def sortkey(name):
164 cls = getattr(model, name)
165 mapper = orm.class_mapper(cls)
166 return tuple(tables[t.name] for t in mapper.tables)
168 return sortkey