feat: database models and Alembic migrations — User, Recording, Transcription, Protocol, PromptTemplate, ExportFile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
[alembic]
|
||||||
|
script_location = alembic
|
||||||
|
sqlalchemy.url = postgresql+asyncpg://meetproto:changeme@db:5432/meetproto
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import asyncio
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
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():
|
||||||
|
context.configure(
|
||||||
|
url=settings.database_url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def do_run_migrations(connection):
|
||||||
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_migrations_online():
|
||||||
|
connectable = create_async_engine(settings.database_url)
|
||||||
|
async with connectable.connect() as connection:
|
||||||
|
await connection.run_sync(do_run_migrations)
|
||||||
|
await connectable.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
asyncio.run(run_migrations_online())
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
database_url: str = "postgresql+asyncpg://meetproto:changeme@db:5432/meetproto"
|
||||||
|
redis_url: str = "redis://redis:6379/0"
|
||||||
|
jwt_secret: str = "change-this-to-a-random-secret"
|
||||||
|
jwt_algorithm: str = "HS256"
|
||||||
|
jwt_expire_minutes: int = 1440
|
||||||
|
whisper_model: str = "large-v3"
|
||||||
|
whisper_device: str = "cuda"
|
||||||
|
whisper_compute_type: str = "float16"
|
||||||
|
litellm_model: str = "openrouter/meta-llama/llama-3.1-70b-instruct"
|
||||||
|
litellm_api_key: str = ""
|
||||||
|
litellm_api_base: str = ""
|
||||||
|
upload_dir: str = "/app/uploads"
|
||||||
|
export_dir: str = "/app/exports"
|
||||||
|
retention_days: int = 90
|
||||||
|
telegram_bot_token: str = ""
|
||||||
|
telegram_webhook_url: str = ""
|
||||||
|
app_host: str = "0.0.0.0"
|
||||||
|
app_port: int = 8000
|
||||||
|
app_url: str = "http://localhost:8000"
|
||||||
|
|
||||||
|
model_config = {"env_file": ".env", "extra": "ignore"}
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
engine = create_async_engine(settings.database_url, echo=False)
|
||||||
|
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_db():
|
||||||
|
async with async_session() as session:
|
||||||
|
yield session
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from app.models.base import Base
|
||||||
|
from app.models.export_file import ExportFile, ExportFormat
|
||||||
|
from app.models.prompt_template import PromptTemplate, TemplateType
|
||||||
|
from app.models.protocol import Protocol, ProtocolStatus
|
||||||
|
from app.models.recording import Recording, RecordingSource, RecordingStatus
|
||||||
|
from app.models.transcription import Transcription
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Base",
|
||||||
|
"User",
|
||||||
|
"UserRole",
|
||||||
|
"Recording",
|
||||||
|
"RecordingSource",
|
||||||
|
"RecordingStatus",
|
||||||
|
"Transcription",
|
||||||
|
"Protocol",
|
||||||
|
"ProtocolStatus",
|
||||||
|
"PromptTemplate",
|
||||||
|
"TemplateType",
|
||||||
|
"ExportFile",
|
||||||
|
"ExportFormat",
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, func
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TimestampMixin:
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UUIDMixin:
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, Enum, ForeignKey, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ExportFormat(str, enum.Enum):
|
||||||
|
docx = "docx"
|
||||||
|
pdf = "pdf"
|
||||||
|
md = "md"
|
||||||
|
|
||||||
|
|
||||||
|
class ExportFile(UUIDMixin, TimestampMixin, Base):
|
||||||
|
__tablename__ = "export_files"
|
||||||
|
|
||||||
|
protocol_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("protocols.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
|
format: Mapped[ExportFormat] = mapped_column(Enum(ExportFormat))
|
||||||
|
file_path: Mapped[str] = mapped_column(String(1000))
|
||||||
|
file_size: Mapped[int] = mapped_column(BigInteger)
|
||||||
|
|
||||||
|
protocol: Mapped["Protocol"] = relationship(back_populates="export_files")
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import enum
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, Enum, String, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateType(str, enum.Enum):
|
||||||
|
client_survey = "client_survey"
|
||||||
|
client_intro = "client_intro"
|
||||||
|
internal = "internal"
|
||||||
|
custom = "custom"
|
||||||
|
|
||||||
|
|
||||||
|
class PromptTemplate(UUIDMixin, TimestampMixin, Base):
|
||||||
|
__tablename__ = "prompt_templates"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(255))
|
||||||
|
description: Mapped[str] = mapped_column(String(1000), default="")
|
||||||
|
type: Mapped[TemplateType] = mapped_column(Enum(TemplateType))
|
||||||
|
system_prompt: Mapped[str] = mapped_column(Text)
|
||||||
|
user_prompt: Mapped[str] = mapped_column(Text)
|
||||||
|
output_schema: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
|
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
|
protocols: Mapped[list["Protocol"]] = relationship(back_populates="template")
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, Enum, ForeignKey, String, Text, func
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolStatus(str, enum.Enum):
|
||||||
|
generating = "generating"
|
||||||
|
done = "done"
|
||||||
|
error = "error"
|
||||||
|
|
||||||
|
|
||||||
|
class Protocol(UUIDMixin, TimestampMixin, Base):
|
||||||
|
__tablename__ = "protocols"
|
||||||
|
|
||||||
|
transcription_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("transcriptions.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
|
template_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("prompt_templates.id")
|
||||||
|
)
|
||||||
|
content: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
|
raw_text: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
llm_model: Mapped[str] = mapped_column(String(100))
|
||||||
|
status: Mapped[ProtocolStatus] = mapped_column(
|
||||||
|
Enum(ProtocolStatus), default=ProtocolStatus.generating
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
transcription: Mapped["Transcription"] = relationship(back_populates="protocols")
|
||||||
|
template: Mapped["PromptTemplate"] = relationship(back_populates="protocols")
|
||||||
|
export_files: Mapped[list["ExportFile"]] = relationship(back_populates="protocol")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, Enum, ForeignKey, Interval, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingSource(str, enum.Enum):
|
||||||
|
web = "web"
|
||||||
|
telegram = "telegram"
|
||||||
|
lensa = "lensa"
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingStatus(str, enum.Enum):
|
||||||
|
uploaded = "uploaded"
|
||||||
|
processing = "processing"
|
||||||
|
done = "done"
|
||||||
|
error = "error"
|
||||||
|
|
||||||
|
|
||||||
|
class Recording(UUIDMixin, TimestampMixin, Base):
|
||||||
|
__tablename__ = "recordings"
|
||||||
|
|
||||||
|
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"))
|
||||||
|
title: Mapped[str] = mapped_column(String(500))
|
||||||
|
file_path: Mapped[str] = mapped_column(String(1000))
|
||||||
|
file_size: Mapped[int] = mapped_column(BigInteger)
|
||||||
|
duration: Mapped[float | None] = mapped_column(nullable=True)
|
||||||
|
format: Mapped[str] = mapped_column(String(20))
|
||||||
|
source: Mapped[RecordingSource] = mapped_column(Enum(RecordingSource))
|
||||||
|
status: Mapped[RecordingStatus] = mapped_column(
|
||||||
|
Enum(RecordingStatus), default=RecordingStatus.uploaded
|
||||||
|
)
|
||||||
|
|
||||||
|
user: Mapped["User"] = relationship(back_populates="recordings")
|
||||||
|
transcription: Mapped["Transcription | None"] = relationship(
|
||||||
|
back_populates="recording", uselist=False
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey, Interval, String, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||||
|
|
||||||
|
|
||||||
|
class Transcription(UUIDMixin, TimestampMixin, Base):
|
||||||
|
__tablename__ = "transcriptions"
|
||||||
|
|
||||||
|
recording_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("recordings.id", ondelete="CASCADE"), unique=True
|
||||||
|
)
|
||||||
|
text: Mapped[str] = mapped_column(Text)
|
||||||
|
segments: Mapped[dict] = mapped_column(JSONB, default=list)
|
||||||
|
language: Mapped[str | None] = mapped_column(String(10), nullable=True)
|
||||||
|
whisper_model: Mapped[str] = mapped_column(String(50))
|
||||||
|
processing_time: Mapped[float | None] = mapped_column(nullable=True)
|
||||||
|
|
||||||
|
recording: Mapped["Recording"] = relationship(back_populates="transcription")
|
||||||
|
protocols: Mapped[list["Protocol"]] = relationship(back_populates="transcription")
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import enum
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, Enum, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||||
|
|
||||||
|
|
||||||
|
class UserRole(str, enum.Enum):
|
||||||
|
admin = "admin"
|
||||||
|
user = "user"
|
||||||
|
|
||||||
|
|
||||||
|
class User(UUIDMixin, TimestampMixin, Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
username: Mapped[str] = mapped_column(String(150), unique=True, index=True)
|
||||||
|
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
||||||
|
password_hash: Mapped[str] = mapped_column(String(255))
|
||||||
|
role: Mapped[UserRole] = mapped_column(
|
||||||
|
Enum(UserRole), default=UserRole.user
|
||||||
|
)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
|
recordings: Mapped[list["Recording"]] = relationship(back_populates="user")
|
||||||
Reference in New Issue
Block a user