diff --git a/docs/api/wuttjamaican/db.util.rst b/docs/api/wuttjamaican/db.util.rst new file mode 100644 index 0000000..66b4df1 --- /dev/null +++ b/docs/api/wuttjamaican/db.util.rst @@ -0,0 +1,6 @@ + +``wuttjamaican.db.util`` +======================== + +.. automodule:: wuttjamaican.db.util + :members: diff --git a/docs/api/wuttjamaican/index.rst b/docs/api/wuttjamaican/index.rst index 69a754e..0c9d7c0 100644 --- a/docs/api/wuttjamaican/index.rst +++ b/docs/api/wuttjamaican/index.rst @@ -17,6 +17,7 @@ db.model.base db.model.upgrades db.sess + db.util email email.handler email.message diff --git a/src/wuttjamaican/db/model/__init__.py b/src/wuttjamaican/db/model/__init__.py index 267738c..d623775 100644 --- a/src/wuttjamaican/db/model/__init__.py +++ b/src/wuttjamaican/db/model/__init__.py @@ -27,8 +27,6 @@ This is the default :term:`app model` module. The ``wuttjamaican.db.model`` namespace contains the following: -* :func:`~wuttjamaican.db.model.base.uuid_column()` -* :func:`~wuttjamaican.db.model.base.uuid_fk_column()` * :class:`~wuttjamaican.db.model.base.Base` * :class:`~wuttjamaican.db.model.base.Setting` * :class:`~wuttjamaican.db.model.base.Person` @@ -39,6 +37,9 @@ The ``wuttjamaican.db.model`` namespace contains the following: * :class:`~wuttjamaican.db.model.upgrades.Upgrade` """ -from .base import uuid_column, uuid_fk_column, Base, Setting, Person +# TODO: remove these +from wuttjamaican.db.util import uuid_column, uuid_fk_column + +from .base import Base, Setting, Person from .auth import Role, Permission, User, UserRole from .upgrades import Upgrade diff --git a/src/wuttjamaican/db/model/base.py b/src/wuttjamaican/db/model/base.py index 6da0cb3..24c850b 100644 --- a/src/wuttjamaican/db/model/base.py +++ b/src/wuttjamaican/db/model/base.py @@ -31,62 +31,15 @@ Base Models import sqlalchemy as sa from sqlalchemy import orm -from wuttjamaican.util import make_uuid +from wuttjamaican.db.util import (naming_convention, ModelBase, + uuid_column, uuid_fk_column) -# nb. this convention comes from upstream docs -# https://docs.sqlalchemy.org/en/14/core/constraints.html#constraint-naming-conventions -naming_convention = { - 'ix': 'ix_%(column_0_label)s', - 'uq': 'uq_%(table_name)s_%(column_0_name)s', - 'ck': 'ck_%(table_name)s_%(constraint_name)s', - 'fk': 'fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s', - 'pk': 'pk_%(table_name)s', -} - metadata = sa.MetaData(naming_convention=naming_convention) - -class ModelBase: - """ """ - - def __iter__(self): - # nb. we override this to allow for `dict(self)` - state = sa.inspect(self) - fields = [attr.key for attr in state.attrs] - return iter([(field, getattr(self, field)) - for field in fields]) - - def __getitem__(self, key): - # nb. we override this to allow for `x = self['field']` - state = sa.inspect(self) - if hasattr(state.attrs, key): - return getattr(self, key) - - Base = orm.declarative_base(metadata=metadata, cls=ModelBase) -def uuid_column(*args, **kwargs): - """ - Returns a UUID column for use as a table's primary key. - """ - kwargs.setdefault('primary_key', True) - kwargs.setdefault('nullable', False) - kwargs.setdefault('default', make_uuid) - return sa.Column(sa.String(length=32), *args, **kwargs) - - -def uuid_fk_column(target_column, *args, **kwargs): - """ - Returns a UUID column for use as a foreign key to another table. - - :param target_column: Name of the table column on the remote side, - e.g. ``'user.uuid'``. - """ - return sa.Column(sa.String(length=32), sa.ForeignKey(target_column), *args, **kwargs) - - class Setting(Base): """ Represents a :term:`config setting`. diff --git a/src/wuttjamaican/db/util.py b/src/wuttjamaican/db/util.py new file mode 100644 index 0000000..88362c4 --- /dev/null +++ b/src/wuttjamaican/db/util.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttJamaican -- Base package for Wutta Framework +# Copyright © 2023-2024 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +Database Utilities +""" + +import sqlalchemy as sa + +from wuttjamaican.util import make_uuid + + +# nb. this convention comes from upstream docs +# https://docs.sqlalchemy.org/en/14/core/constraints.html#constraint-naming-conventions +naming_convention = { + 'ix': 'ix_%(column_0_label)s', + 'uq': 'uq_%(table_name)s_%(column_0_name)s', + 'ck': 'ck_%(table_name)s_%(constraint_name)s', + 'fk': 'fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s', + 'pk': 'pk_%(table_name)s', +} + + +class ModelBase: + """ """ + + def __iter__(self): + # nb. we override this to allow for `dict(self)` + state = sa.inspect(self) + fields = [attr.key for attr in state.attrs] + return iter([(field, getattr(self, field)) + for field in fields]) + + def __getitem__(self, key): + # nb. we override this to allow for `x = self['field']` + state = sa.inspect(self) + if hasattr(state.attrs, key): + return getattr(self, key) + + +def uuid_column(*args, **kwargs): + """ + Returns a UUID column for use as a table's primary key. + """ + kwargs.setdefault('primary_key', True) + kwargs.setdefault('nullable', False) + kwargs.setdefault('default', make_uuid) + return sa.Column(sa.String(length=32), *args, **kwargs) + + +def uuid_fk_column(target_column, *args, **kwargs): + """ + Returns a UUID column for use as a foreign key to another table. + + :param target_column: Name of the table column on the remote side, + e.g. ``'user.uuid'``. + """ + return sa.Column(sa.String(length=32), sa.ForeignKey(target_column), *args, **kwargs) diff --git a/tests/db/model/test_base.py b/tests/db/model/test_base.py index 1d55436..50e95a5 100644 --- a/tests/db/model/test_base.py +++ b/tests/db/model/test_base.py @@ -3,42 +3,16 @@ from unittest import TestCase try: - import sqlalchemy as sa - from wuttjamaican.db.model import base as model + from wuttjamaican.db.model import base as mod from wuttjamaican.db.model.auth import User except ImportError: pass else: - class TestModelBase(TestCase): - - def test_dict_behavior(self): - setting = model.Setting() - self.assertEqual(list(iter(setting)), [('name', None), ('value', None)]) - self.assertIsNone(setting['name']) - setting.name = 'foo' - self.assertEqual(setting['name'], 'foo') - - class TestUUIDColumn(TestCase): - - def test_basic(self): - column = model.uuid_column() - self.assertIsInstance(column, sa.Column) - self.assertIsInstance(column.type, sa.String) - self.assertEqual(column.type.length, 32) - - class TestUUIDFKColumn(TestCase): - - def test_basic(self): - column = model.uuid_fk_column('foo.bar') - self.assertIsInstance(column, sa.Column) - self.assertIsInstance(column.type, sa.String) - self.assertEqual(column.type.length, 32) - class TestSetting(TestCase): def test_basic(self): - setting = model.Setting() + setting = mod.Setting() self.assertEqual(str(setting), "") setting.name = 'foo' self.assertEqual(str(setting), "foo") @@ -46,13 +20,13 @@ else: class TestPerson(TestCase): def test_basic(self): - person = model.Person() + person = mod.Person() self.assertEqual(str(person), "") person.full_name = "Barney Rubble" self.assertEqual(str(person), "Barney Rubble") def test_users(self): - person = model.Person() + person = mod.Person() self.assertIsNone(person.user) user = User() diff --git a/tests/db/test_util.py b/tests/db/test_util.py new file mode 100644 index 0000000..566b68a --- /dev/null +++ b/tests/db/test_util.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8; -*- + +from unittest import TestCase + + +try: + import sqlalchemy as sa + from wuttjamaican.db import util as mod + from wuttjamaican.db.model.base import Setting +except ImportError: + pass +else: + + + class TestModelBase(TestCase): + + def test_dict_behavior(self): + setting = Setting() + self.assertEqual(list(iter(setting)), [('name', None), ('value', None)]) + self.assertIsNone(setting['name']) + setting.name = 'foo' + self.assertEqual(setting['name'], 'foo') + + + class TestUUIDColumn(TestCase): + + def test_basic(self): + column = mod.uuid_column() + self.assertIsInstance(column, sa.Column) + self.assertIsInstance(column.type, sa.String) + self.assertEqual(column.type.length, 32) + + + class TestUUIDFKColumn(TestCase): + + def test_basic(self): + column = mod.uuid_fk_column('foo.bar') + self.assertIsInstance(column, sa.Column) + self.assertIsInstance(column.type, sa.String) + self.assertEqual(column.type.length, 32)