diff --git a/.gitignore b/.gitignore index 70d051b..bd780f2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ __build__/ __pycache__/ /dist/ -/.idea/ \ No newline at end of file +/.idea/ +pillar_tool.toml diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..2adb551 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,87 @@ +import tomllib +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + with open("./pillar_tool.toml", 'rb') as f: + data = tomllib.load(f) + user = data['db']['user'] + password = data['db']['password'] + host = data['db']['host'] + port = data['db']['port'] + database = data['db']['database'] + url = f"postgresql://{user}:{password}@{host}:{port}/{database}" + #url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..1101630 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/pillar_tool.toml.example b/pillar_tool.toml.example new file mode 100644 index 0000000..f6d8919 --- /dev/null +++ b/pillar_tool.toml.example @@ -0,0 +1,6 @@ +[db] +user = "db user" +password = "db password" +host = "db host" +post = 5432 +database = "db name" \ No newline at end of file diff --git a/pillar_tool/__init__.py b/pillar_tool/__init__.py index 18f2ebc..563ef3f 100644 --- a/pillar_tool/__init__.py +++ b/pillar_tool/__init__.py @@ -1,3 +1,5 @@ +from contextlib import asynccontextmanager + from fastapi import FastAPI from starlette.middleware.authentication import AuthenticationMiddleware from starlette.requests import Request @@ -5,6 +7,18 @@ from fastapi.responses import HTMLResponse, PlainTextResponse from starlette.responses import JSONResponse from pillar_tool.middleware.basicauth_backend import BasicAuthBackend +from pillar_tool.util import load_config, config +from pillar_tool.db import run_db_migrations + +# load config so everything else can work +load_config() + + +@asynccontextmanager +async def app_lifespan(app: FastAPI): + run_db_migrations() + + yield def on_auth_error(request: Request, exc: Exception): @@ -17,10 +31,11 @@ def on_general_error(request: Request, exc: Exception): print("wtf?") response = PlainTextResponse(str(exc), status_code=500) -app = FastAPI() +app = FastAPI(lifespan=app_lifespan) app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend(), on_error=on_auth_error) app.exception_handler(Exception)(on_general_error) + @app.get("/") async def root(): return {"message": "Hello World"} diff --git a/pillar_tool/db/__init__.py b/pillar_tool/db/__init__.py index e69de29..fdebf28 100644 --- a/pillar_tool/db/__init__.py +++ b/pillar_tool/db/__init__.py @@ -0,0 +1,25 @@ +import os + +import alembic + +from alembic.config import Config +from alembic.command import upgrade +from pillar_tool.util import config + + +from models import * + +cfg = config() +user = cfg.db.user +password = cfg.db.password +host = cfg.db.host +port = cfg.db.port +database = cfg.db.database +alembic_cfg = Config() +alembic_cfg.set_main_option('script_location', f'{os.path.dirname(os.path.realpath(__file__))}/migrations') +alembic_cfg.set_main_option('sqlalchemy.url', f'postgres://{user}:{password}@{host}:{port}/{database}') +alembic_cfg.set_main_option('prepend_sys_path', '.') + + +def run_db_migrations(): + upgrade(config=alembic_cfg, revision='head') diff --git a/pillar_tool/db/database.py b/pillar_tool/db/database.py index 5ea226f..ba8004e 100644 --- a/pillar_tool/db/database.py +++ b/pillar_tool/db/database.py @@ -1,12 +1,33 @@ from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker, declarative_base +from sqlalchemy.sql.annotation import Annotated +from pillar_tool.util import config + + +cfg = config() +SessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=create_engine( + url=f"postgresql+psycopg2://{cfg.db.user}:{cfg.db.password}@{cfg.host}:{cfg.port}/{cfg.database}" + ) +) + +Base = declarative_base() def get_connection(): - db_user = "pillar_tool" - db_password = "pillar_tool" - db_host = "127.0.0.1" - db_port = "5432" - db_name = "pillar_tool" - return create_engine( - url=f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}" - ) \ No newline at end of file + session = SessionLocal() + # noinspection PyBroadException + try: + yield session + except: + session.rollback() + session.close() + else: + session.commit() + session.close() + + +DB = Annotated[Session, get_connection()] + diff --git a/pillar_tool/db/models.py b/pillar_tool/db/models.py deleted file mode 100644 index fd40910..0000000 --- a/pillar_tool/db/models.py +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/pillar_tool/db/models/__init__.py b/pillar_tool/db/models/__init__.py new file mode 100644 index 0000000..82da278 --- /dev/null +++ b/pillar_tool/db/models/__init__.py @@ -0,0 +1 @@ +from .user import * \ No newline at end of file diff --git a/pillar_tool/db/models/user.py b/pillar_tool/db/models/user.py new file mode 100644 index 0000000..fde37e3 --- /dev/null +++ b/pillar_tool/db/models/user.py @@ -0,0 +1,15 @@ +from pydantic import UUID4 +from sqlalchemy import Column, String + +from pillar_tool.db.database import Base + + + +class User(Base): + __tablename__ = 'users' + id = Column(UUID4, primary_key=True) + username = Column(String, nullable=False) + pw_hash = Column(String, nullable=False) + pw_salt = Column(String, nullable=False) + +