Add SQLAlchemy and database boilerplate code

This commit is contained in:
Lexi / Zoe 2022-04-02 01:06:59 +02:00
parent 2da6e43797
commit 31df889dcb
Signed by: binaryDiv
GPG key ID: F8D4956E224DA232
15 changed files with 454 additions and 60 deletions

View file

View file

@ -0,0 +1,3 @@
from .metadata import metadata_obj
from .model import Model
from .sqlalchemy import SQLAlchemy

View file

@ -0,0 +1,17 @@
from sqlalchemy import MetaData
__all__ = [
'metadata_obj',
]
# Define naming convention for constraints
_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"
}
# Create global metadata object for database schemas
metadata_obj = MetaData(naming_convention=_naming_convention)

View file

@ -0,0 +1,10 @@
from sqlalchemy.orm import declarative_base
from .metadata import metadata_obj
__all__ = [
'Model',
]
# Generate declarative base class for database models
Model = declarative_base(name='Model', metadata=metadata_obj)

View file

@ -0,0 +1,87 @@
from typing import Optional, cast
from flask import Flask
from sqlalchemy import MetaData, create_engine
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session, scoped_session, sessionmaker
from .metadata import metadata_obj
__all__ = [
'SQLAlchemy',
]
class SQLAlchemy:
"""
Wrapper class for integrating SQLAlchemy into the application as a Flask extension.
"""
_engine: Optional[Engine] = None
_scoped_session: Optional[scoped_session] = None
def init_database(self, app: Flask):
"""
Initializes the SQLAlchemy engine.
"""
self._engine = self._create_engine()
self._scoped_session = self._create_scoped_session()
@app.teardown_appcontext
def shutdown_session(_exception=None):
self._scoped_session.remove()
def _create_engine(self) -> Engine:
"""
Create the database engine using the app configuration.
"""
# TODO: Use config
return create_engine('sqlite:////tmp/test.db')
def _create_scoped_session(self) -> scoped_session:
"""
Create a scoped session.
"""
return scoped_session(
sessionmaker(
autocommit=False,
autoflush=False,
bind=self._engine,
)
)
@property
def engine(self) -> Engine:
"""
Database engine.
"""
assert self._engine, 'Engine not ready yet.'
return self._engine
@property
def session(self) -> Session:
"""
Scoped database session.
"""
assert self._scoped_session, 'Session not ready yet.'
# For all further purposes, the scoped session should be treated like a regular Session object.
# Use cast() so we can use Session as the type annotation.
return cast(Session, self._scoped_session)
@property
def metadata(self) -> MetaData:
"""
Database metadata object.
"""
return metadata_obj
def create_all_tables(self) -> None:
"""
Create tables in the database for all models defined in the metadata.
"""
self.metadata.create_all(self.engine)
def drop_all_tables(self) -> None:
"""
Delete tables in the database for all models defined in the metadata.
"""
self.metadata.drop_all(self.engine)

View file

@ -0,0 +1 @@
from .base_blueprint import BaseBlueprint

View file

@ -0,0 +1,72 @@
from abc import ABC, abstractmethod
from typing import Callable, Type, TYPE_CHECKING
from flask import Blueprint
from flask.views import View
if TYPE_CHECKING:
from tofu_api.app import App
__all__ = [
'BaseBlueprint',
]
class BaseBlueprint(Blueprint, ABC):
"""
Base class for Flask blueprints.
"""
# Reference to the application object
app: 'App'
@property
@abstractmethod
def name(self) -> str:
"""
The name of the blueprint. Will be prepended to each endpoint name.
"""
raise NotImplementedError
@property
@abstractmethod
def import_name(self) -> str:
"""
The name of the blueprint package. Should be set to `__name__`.
"""
raise NotImplementedError
@property
@abstractmethod
def url_prefix(self) -> str:
"""
Prefix for all URLs.
"""
raise NotImplementedError
def __init__(self, app: 'App'):
"""
Initialize blueprint. Needs the Flask application object.
"""
super().__init__(
name=self.name,
import_name=self.import_name,
url_prefix=self.url_prefix,
)
self.app = app
self.init_blueprint()
@abstractmethod
def init_blueprint(self) -> None:
"""
Register child blueprints and URL rules.
"""
raise NotImplementedError
@staticmethod
def create_view_func(view_cls: Type[View], *args, **kwargs) -> Callable:
"""
Helper function to create a view function from a `View` class using `view_cls.as_view()`.
All arguments are passed to the constructor of the view class.
"""
return view_cls.as_view(view_cls.__name__, *args, **kwargs)