initial setup for database

This commit is contained in:
Linus Vogel 2025-12-22 22:39:18 +01:00
parent ae4c7bb931
commit f204040ed0
11 changed files with 210 additions and 14 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@
__build__/ __build__/
__pycache__/ __pycache__/
/dist/ /dist/
/.idea/ /.idea/
pillar_tool.toml

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

87
migrations/env.py Normal file
View File

@ -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()

28
migrations/script.py.mako Normal file
View File

@ -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"}

6
pillar_tool.toml.example Normal file
View File

@ -0,0 +1,6 @@
[db]
user = "db user"
password = "db password"
host = "db host"
post = 5432
database = "db name"

View File

@ -1,3 +1,5 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.requests import Request from starlette.requests import Request
@ -5,6 +7,18 @@ from fastapi.responses import HTMLResponse, PlainTextResponse
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from pillar_tool.middleware.basicauth_backend import BasicAuthBackend 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): def on_auth_error(request: Request, exc: Exception):
@ -17,10 +31,11 @@ def on_general_error(request: Request, exc: Exception):
print("wtf?") print("wtf?")
response = PlainTextResponse(str(exc), status_code=500) 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.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend(), on_error=on_auth_error)
app.exception_handler(Exception)(on_general_error) app.exception_handler(Exception)(on_general_error)
@app.get("/") @app.get("/")
async def root(): async def root():
return {"message": "Hello World"} return {"message": "Hello World"}

View File

@ -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')

View File

@ -1,12 +1,33 @@
from sqlalchemy import create_engine 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(): def get_connection():
db_user = "pillar_tool" session = SessionLocal()
db_password = "pillar_tool" # noinspection PyBroadException
db_host = "127.0.0.1" try:
db_port = "5432" yield session
db_name = "pillar_tool" except:
return create_engine( session.rollback()
url=f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}" session.close()
) else:
session.commit()
session.close()
DB = Annotated[Session, get_connection()]

View File

@ -1,4 +0,0 @@

View File

@ -0,0 +1 @@
from .user import *

View File

@ -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)