Initial Commit

This commit is contained in:
2022-07-12 05:44:09 -07:00
commit 57c9c70cbb
31 changed files with 2257 additions and 0 deletions

84
sigl/database/__init__.py Normal file
View File

@@ -0,0 +1,84 @@
"""Sigl Database Package.
Simple Grocery List (Sigl) | sigl.app
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
"""
import re
from alembic.script import ScriptDirectory
from flask import current_app
from sqlalchemy import inspect
from sigl.exc import Error
from .globals import db, migrate
from .util import make_uri
__all__ = ('db', 'migrate', 'init_db')
def check_database():
"""Check that the database schema is initialized."""
if current_app.config.get('_SIGL_DB_NEEDS_INIT'):
raise Error('Database is not initialized')
def init_db(app):
"""Initialize the Global Database Object for the Application."""
# Set SQLalchemy Defaults (suppresses warning)
app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False)
# Load SQLalchemy URI
if 'SQLALCHEMY_DATABASE_URI' not in app.config:
if 'DB_URI' in app.config:
app.config['SQLALCHEMY_DATABASE_URI'] = app.config['DB_URI']
else:
app.config['SQLALCHEMY_DATABASE_URI'] = make_uri(app)
# Initialize Object Relational Mapping
from .orm import init_orm
init_orm()
# Log the URI (masked)
app.logger.debug(
'Starting SQLalchemy with URI: "%s"', re.sub(
r':([.+]|[0-9a-z\'_-~]|%[0-9A-F]{2})+@',
':(masked)@',
app.config['SQLALCHEMY_DATABASE_URI'],
flags=re.I
)
)
# Initialize Database
db.init_app(app)
migrate.init_app(app, db)
# Check the Database Revision
app.config['_SIGL_DB_NEEDS_INIT'] = True
app.config['_SIGL_DB_NEEDS_UPGRADE'] = False
app.config['_SIGL_DB_HEAD_REV'] = None
app.config['_SIGL_DB_CUR_REV'] = None
with app.app_context():
if inspect(db.engine).has_table('alembic_version'):
app.config['_SIGL_DB_NEEDS_INIT'] = False
db_version_sql = db.text('select version_num from alembic_version')
db_version = db.engine.execute(db_version_sql).scalar()
app.config['_SIGL_DB_CUR_REV'] = db_version
script_dir = ScriptDirectory.from_config(migrate.get_config())
for rev in script_dir.walk_revisions():
if rev.is_head:
app.config['_SIGL_DB_HEAD_REV'] = rev.revision
if rev.revision == db_version:
app.config['_SIGL_DB_NEEDS_UPGRADE'] = not rev.is_head
# Register Database Setup Hook
app.before_request(check_database)
# Notify Initialization
app.logger.debug('Database Initialized')
# Return Success
return True

14
sigl/database/globals.py Normal file
View File

@@ -0,0 +1,14 @@
"""Sigl Database Global Objects.
Simple Grocery List (Sigl) | sigl.app
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
"""
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
#: Global Database Object for Models
db = SQLAlchemy()
#: Global Alembic Migration Object
migrate = Migrate()

34
sigl/database/orm.py Normal file
View File

@@ -0,0 +1,34 @@
"""Sigl Database ORM Setup.
Simple Grocery List (Sigl) | sigl.app
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
"""
from sigl.domain.models import (
Product,
ProductLocation,
)
from .globals import db
from .tables import (
product_locations,
products,
)
__all__ = ('init_orm', )
def init_orm():
"""Initialize the Sigl ORM."""
# Products
db.mapper(Product, products, properties={
'locations': db.relationship(
ProductLocation,
backref='product',
cascade='all, delete-orphan',
),
})
# Product Locations
db.mapper(ProductLocation, product_locations)

56
sigl/database/tables.py Normal file
View File

@@ -0,0 +1,56 @@
"""Sigl Database Tables.
Simple Grocery List (Sigl) | sigl.app
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
"""
from .globals import db
#: Sigl Server Configuration Table
sigl_config = db.Table(
'sigl_config',
# Key/Value Store
db.Column('key', db.String, primary_key=True),
db.Column('value', db.String(128)),
)
#: Product Table
products = db.Table(
'products',
# Primary Key
db.Column('id', db.Integer, primary_key=True),
# Product Attributes
db.Column('name', db.String(128), nullable=False),
db.Column('category', db.String(128), nullable=False, index=True),
# Mixin Columns
db.Column('notes', db.String(), default=None),
db.Column('created_at', db.DateTime(), default=None),
db.Column('modified_at', db.DateTime(), default=None),
)
#: Product Location Table
product_locations = db.Table(
'product_locations',
# Primary Keys
db.Column(
'product_id',
db.ForeignKey('products.id'),
nullable=False,
primary_key=True,
),
db.Column('store', db.String(128), nullable=False, primary_key=True),
# Location Attributes
db.Column('aisle', db.String(64), nullable=False),
db.Column('bin', db.String(64), default=None),
# Mixin Columns
db.Column('notes', db.String(), default=None),
db.Column('created_at', db.DateTime(), default=None),
db.Column('modified_at', db.DateTime(), default=None),
)

78
sigl/database/util.py Normal file
View File

@@ -0,0 +1,78 @@
"""Sigl Database Utilities.
Simple Grocery List (Sigl) | sigl.app
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
"""
import os
from urllib.parse import quote_plus
from sigl.exc import ConfigError
__all__ = ('make_uri', )
def make_uri(app):
'''
Assembles the Database URI from Application Configuration values.
:param app: :class:`Flask` application object
'''
if 'DB_DRIVER' not in app.config:
raise ConfigError(
'DB_DRIVER must be defined in application configuration',
config_key='DB_DRIVER'
)
# SQLite
if app.config['DB_DRIVER'] == 'sqlite':
if 'DB_FILE' not in app.config:
raise ConfigError(
'DB_FILE must be defined in application configuration for '
'SQLite Database',
config_key='DB_FILE'
)
return 'sqlite:///{db_file}'.format(
db_file=os.path.abspath(app.config['DB_FILE']),
)
# Database Servers (note only MySQL and PostgreSQL are tested)
uri_auth = ''
uri_port = ''
for k in ('DB_HOST', 'DB_NAME'):
if k not in app.config:
raise ConfigError(
f'{k} must be defined in application configuration',
config_key=k
)
# Assemble Port Suffix
if 'DB_PORT' in app.config:
uri_port = ':{}'.format(app.config['DB_PORT'])
# Assemble Authentication Portion
if 'DB_USERNAME' in app.config:
if 'DB_PASSWORD' in app.config:
uri_auth = '{username}:{password}@'.format(
username=quote_plus(app.config['DB_USERNAME']),
password=quote_plus(app.config['DB_PASSWORD']),
)
else:
uri_auth = '{username}@'.format(
username=quote_plus(app.config['DB_USERNAME']),
)
elif 'DB_PASSWORD' in app.config:
raise ConfigError(
'DB_USERNAME must be defined if DB_PASSWORD is set in '
'application configuration',
config_key='DB_USERNAME'
)
return '{driver}://{auth}{host}{port}/{name}'.format(
driver=app.config['DB_DRIVER'],
auth=uri_auth,
host=app.config['DB_HOST'],
port=uri_port,
name=app.config['DB_NAME'],
)