Add initial database infrastructure

Add base model class
Add initialize function and basic import structure
Add datatypes module
This commit is contained in:
Ethan Paul 2020-02-21 22:08:58 -05:00
parent ee99c1b9ab
commit 2b5eafa71a
3 changed files with 190 additions and 0 deletions

View File

@ -0,0 +1,95 @@
"""Database interface and tooling module
Keyosk uses the
`Peewee Object Relational Model <http://docs.peewee-orm.com/en/latest/peewee/quickstart.html>`_
Python library for its database interface. The database interface object is available as the
``interface`` variable under the ``keyosk.database`` namespace. The individual models (a.k.a.
database tables) are available in the same namespace.
The database connection can be accessed using the
`Peewee atomic transaction context manager <http://docs.peewee-orm.com/en/latest/peewee/database.html#context-manager>`_.
Similarly, the ORM models- which correspond to database tables- can be accessed in the same way:
::
from keyosk import database
with database.interface.atomic():
database.Account.get(username == "atticusfinch")
"""
import logging
from typing import List
from typing import Type
import peewee
from keyosk import config
from keyosk import datatypes
from keyosk.database._shared import INTERFACE as interface
from keyosk.database._shared import KeyoskBaseModel
from keyosk.database.account import Account
from keyosk.database.domain import Domain
from keyosk.database.mappings import AccountACL
from keyosk.database.mappings import AccountAssignment
from keyosk.database.mappings import DomainAccessList
from keyosk.database.mappings import DomainAdmin
from keyosk.database.mappings import DomainPermission
MODELS: List[Type[KeyoskBaseModel]] = [
Account,
Domain,
DomainAccessList,
DomainPermission,
DomainAdmin,
AccountACL,
AccountAssignment,
]
def initialize(conf: config.KeyoskConfig):
"""Initialize the database interface
Defining the database as an
`unconfigured proxy object <http://docs.peewee-orm.com/en/latest/peewee/database.html#setting-the-database-at-run-time>`_
allows it to be configured at runtime based on the config values.
:param config: Populated configuration container object
"""
logger = logging.getLogger(__name__)
if conf.storage.backend == datatypes.StorageBackend.SQLITE:
logger.debug("Using SQLite database backend")
pragmas = {
**conf.storage.sqlite.pragmas,
**{
"journal_mode": "wal",
"cache_size": -1 * 64000,
"foreign_keys": 1,
"ignore_check_constraints": 0,
"synchronous": 0,
},
}
for key, value in pragmas:
logger.debug(f"Applying pragma '{key}' with value '{value}'")
database = peewee.SqliteDatabase(conf.storage.sqlite.path, pragmas=pragmas)
elif conf.storage.backend == datatypes.StorageBackend.MARIA:
logger.debug("Using MariaDB database backend")
database = peewee.MySQLDatabase(
conf.storage.maria.schema,
host=conf.storage.maria.host,
port=conf.storage.maria.port,
user=conf.storage.maria.username,
password=conf.storage.maria.password,
charset="utf8mb4",
)
logger.debug(
f"Configuring MariaDB: {conf.storage.maria.username}@{conf.storage.maria.host}:{conf.storage.maria.port} `{conf.storage.maria.schema}`"
)
interface.initialize(database)
with interface.atomic():
interface.create_tables(MODELS)

View File

@ -0,0 +1,78 @@
"""Internally shared database components
This submodule exists to avoid circular imports: architecturally there's no reason why
this module's base model and the :func:`initialize` function cannot both go in
``__init__``, or indeed why they can't both go here. However if both existed in the same
module then every other submodule would need to both import :class:`KeyoskBaseModel`
from that module, and be imported into that module for the :func:`initialize` function
to work. This would lead to a circular import.
Thus the :func:`initialize` function and :class:`KeyoskBaseModel` class need to go in
separate modules, and for somewhat arbitrary reasons the base model was put here and the
init function kept in init.
"""
from typing import Any
from typing import Generator
from typing import List
from typing import Tuple
import peewee
INTERFACE = peewee.DatabaseProxy()
class KeyoskBaseModel(peewee.Model):
"""Base model for primary models to inherit from
* Attaches the ``uuid`` field to the model as the primary key
* Attaches the model- and all child models- to the database proxy
* Provides the structure for casting the model to a dictionary
.. warning:: This model is a stub and should not be created in the database or
used for querying.
"""
class Meta: # pylint: disable=missing-docstring,too-few-public-methods
database = INTERFACE
uuid = peewee.UUIDField(null=False, unique=True, primary_key=True)
@staticmethod
def dict_keys() -> List[str]:
"""Return tuple of attribute names that should be included in the dict form of the model
Inteneded to be used in a dictionary comprehension; see the :meth:`__iter__` method for
usage example.
"""
return []
@staticmethod
def foreign_ref() -> List[str]:
"""Return tuple of attribute names that point to foreign key references on the model
Intended for usage when recursively converting models into dictionaries ahead of
serialization; see the :meth:`__iter__` method for usage example.
.. warning:: Foreign keys should only be included here when their attribute appears in the
tuple returned from :meth:`dict_keys`
"""
return []
@staticmethod
def foreign_backref() -> List[str]:
"""Return tuple of attribute names that point to foreign backreferences on the model
Inteneded for usage when recursively converting models into dictionaries ahead of
serialization; see the :meth:`__iter__` method for usage example.
.. warning:: Foreign keys should only be included here when their attribute appears in the
tuple returned from :meth:`dict_keys`
"""
return []
def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
for key in self.dict_keys():
if key in self.foreign_ref():
yield key, dict(getattr(self, key))
elif key in self.foreign_backref():
yield key, [dict(item) for item in getattr(self, key)]
else:
yield key, getattr(self, key)

17
keyosk/datatypes.py Normal file
View File

@ -0,0 +1,17 @@
import enum
from typing import Dict
from typing import Union
Extras = Dict[str, Union[int, float, bool, str, None]]
class TokenUsage(enum.Enum):
REFRESH = enum.auto()
ACCESS = enum.auto()
@enum.unique
class StorageBackend(enum.Enum):
SQLITE = "sqlite"
MARIA = "maria"