feat: add database models and alembic setup

- SQLAlchemy async models for sources, jobs, schedules, executions, backups, settings
- Alembic migration configuration
This commit is contained in:
2026-05-11 20:43:23 +02:00
parent 3deab5fdf3
commit 13488f7b21
6 changed files with 179 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
[alembic]
script_location = alembic
prepend_sys_path = .
version_path_separator = os
sqlalchemy.url = sqlite+aiosqlite:///./backup_tool.db
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration.
+47
View File
@@ -0,0 +1,47 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from app.models import Base
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
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 do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+28
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"}
+16
View File
@@ -0,0 +1,16 @@
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import declarative_base
DATABASE_URL = "sqlite+aiosqlite:///./backup_tool.db"
engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
Base = declarative_base()
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
+82
View File
@@ -0,0 +1,82 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey, JSON
from sqlalchemy.orm import relationship
from datetime import datetime
from .database import Base
class Source(Base):
__tablename__ = "sources"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
type = Column(String, nullable=False) # local, ssh, database
config = Column(JSON, default=dict)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
jobs = relationship("Job", back_populates="source", cascade="all, delete-orphan")
class Job(Base):
__tablename__ = "jobs"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
source_id = Column(Integer, ForeignKey("sources.id"), nullable=False)
strategy = Column(String, nullable=False, default="full") # full, incremental
destination_path = Column(String, nullable=False)
exclude_patterns = Column(JSON, default=list)
enabled = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
source = relationship("Source", back_populates="jobs")
schedule = relationship("Schedule", back_populates="job", uselist=False, cascade="all, delete-orphan")
executions = relationship("JobExecution", back_populates="job", cascade="all, delete-orphan")
class Schedule(Base):
__tablename__ = "schedules"
id = Column(Integer, primary_key=True, index=True)
job_id = Column(Integer, ForeignKey("jobs.id"), unique=True, nullable=False)
cron_expression = Column(String, nullable=False)
enabled = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
job = relationship("Job", back_populates="schedule")
class JobExecution(Base):
__tablename__ = "job_executions"
id = Column(Integer, primary_key=True, index=True)
job_id = Column(Integer, ForeignKey("jobs.id"), nullable=False)
status = Column(String, nullable=False, default="pending") # pending, running, success, failed, cancelled
started_at = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
bytes_processed = Column(Integer, default=0)
bytes_backed_up = Column(Integer, default=0)
error_message = Column(Text, nullable=True)
triggered_by = Column(String, nullable=False) # manual, schedule
job = relationship("Job", back_populates="executions")
backups = relationship("Backup", back_populates="execution", cascade="all, delete-orphan")
class Backup(Base):
__tablename__ = "backups"
id = Column(Integer, primary_key=True, index=True)
execution_id = Column(Integer, ForeignKey("job_executions.id"), nullable=False)
storage_path = Column(String, nullable=False)
size_bytes = Column(Integer, default=0)
checksum = Column(String, nullable=True)
type = Column(String, nullable=False) # full, incremental
parent_backup_id = Column(Integer, ForeignKey("backups.id"), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
execution = relationship("JobExecution", back_populates="backups")
parent_backup = relationship("Backup", remote_side=[id])
class Setting(Base):
__tablename__ = "settings"
key = Column(String, primary_key=True)
value = Column(Text, nullable=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)