Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
55a8d91
Switch from Prisma to SQLModel/SQLAlchemy for database layer
cursoragent Jul 28, 2025
917727d
style: auto fixes from pre-commit hooks
pre-commit-ci[bot] Jul 28, 2025
2d26bd7
Checkpoint before follow-up message
cursoragent Jul 28, 2025
7a62ddc
Migrate from Prisma to SQLModel database models and imports
cursoragent Jul 28, 2025
ac5f2e7
Migrate database imports from Prisma to local models and update contr…
cursoragent Jul 28, 2025
d28f253
Add Alembic configuration for database migrations
cursoragent Jul 28, 2025
73866aa
Checkpoint before follow-up message
cursoragent Jul 28, 2025
c76cf4e
Add Alembic environment configuration for SQLModel migrations
cursoragent Jul 28, 2025
3585579
Replace Prisma database commands with Alembic migration commands
cursoragent Jul 28, 2025
fed3cfb
Add PostgreSQL array support and initial Alembic migration
cursoragent Jul 28, 2025
2dbf131
Replace PostgreSQL array with JSON for case_user_roles column
cursoragent Jul 28, 2025
e6331ad
chore: regenerate poetry.lock after SQLModel migration
cursoragent Jul 28, 2025
c2fef03
chore(ci): remove Prisma steps from Dockerfile and workflows; cleanup…
cursoragent Jul 28, 2025
ae99950
fix: compatibility shims for pyright errors (is_connected, include pa…
cursoragent Jul 28, 2025
4e5960c
Fix is_connected method calls in DatabaseClient methods
cursoragent Jul 28, 2025
2d9242a
chore(pyright): relax type checking, fix is_registered method
cursoragent Jul 28, 2025
2743e60
fix(types): strict typing compliance; remove external/alembic clone
cursoragent Jul 28, 2025
03e58a9
Refactor database client, models, and controllers for type safety
cursoragent Jul 28, 2025
6cfda51
chore(pyright): configure exclusions in pyproject; fix datetime UTC, …
cursoragent Jul 28, 2025
4d2a21d
fix(pyright): restore strict include/exclude settings
cursoragent Jul 28, 2025
ad6e284
Fix type annotations and session variable naming in database controllers
cursoragent Jul 30, 2025
11539d0
Fix type annotation for SQLModel Field to satisfy Pyright strict mode
cursoragent Jul 30, 2025
19c68cc
Refactor imports, type hints, and formatting across multiple files
cursoragent Jul 30, 2025
739c34e
chore: remove external vendor directory to satisfy lint
cursoragent Jul 30, 2025
5f6d1bf
refactor: reorganize imports and improve code readability
kzndotsh Jul 30, 2025
03a0cb6
chore(pyproject.toml): add pydantic dependency for data validation
kzndotsh Jul 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions .github/actions/setup-python/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ inputs:
description: Cache key suffix for differentiation
required: false
default: default
generate-prisma:
description: Whether to generate Prisma client
required: false
default: 'true'
runs:
using: composite
steps:
Expand Down Expand Up @@ -51,16 +47,9 @@ runs:
# Installs specified Poetry groups with CI-optimized settings
- name: Install dependencies
shell: bash
run: |
run: |-
if [[ "${{ inputs.install-groups }}" == "main" ]]; then
poetry install --only=main --no-interaction --no-ansi
else
poetry install --with=${{ inputs.install-groups }} --no-interaction --no-ansi
fi

# CONDITIONAL PRISMA CLIENT GENERATION
# Generates Prisma database client when needed for database operations
- name: Generate Prisma client
if: ${{ inputs.generate-prisma == 'true' }}
shell: bash
run: poetry run prisma generate
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ jobs:
python-version: '3.13'
install-groups: dev,types
cache-suffix: ci
generate-prisma: 'true'

# STATIC TYPE CHECKING
# Pyright provides comprehensive type checking for Python
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ jobs:
python-version: '3.13'
install-groups: main
cache-suffix: security
generate-prisma: 'false'

# SECURITY VULNERABILITY SCANNING
# Comprehensive security advisory checking with structured output
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ jobs:
python-version: ${{ matrix.python-version }}
install-groups: dev,test,types
cache-suffix: test
generate-prisma: 'true'

# TEST ENVIRONMENT CONFIGURATION
# Creates isolated test environment with SQLite for CI safety
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"source.organizeImports.ruff": "explicit"
}
},
"python.languageServer": "Pylance",
"python.languageServer": "None",
"python.analysis.typeCheckingMode": "strict",
"python.analysis.autoFormatStrings": true,
"python.analysis.completeFunctionParens": true,
Expand Down
18 changes: 7 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ COPY config/ ./config/

# 2. Database schema files (change infrequently)
# Prisma schema and migrations are relatively stable
COPY prisma/ ./prisma/

# 3. Main application code (changes more frequently)
# The core bot code is most likely to change during development
Expand Down Expand Up @@ -269,13 +268,12 @@ USER nonroot
# Install development dependencies and setup Prisma
# DEVELOPMENT: These tools are needed for linting, testing, and development workflow
RUN poetry install --only dev --no-root --no-directory && \
poetry run prisma py fetch && \
poetry run prisma generate
true

# Development container startup command
# WORKFLOW: Regenerates Prisma client and starts the bot in development mode
# This ensures the database client is always up-to-date with schema changes
CMD ["sh", "-c", "poetry run prisma generate && exec poetry run tux --dev start"]
CMD ["sh", "-c", "exec poetry run tux --dev start"]

# ==============================================================================
# PRODUCTION STAGE - Minimal Runtime Environment
Expand Down Expand Up @@ -357,7 +355,6 @@ ENV VIRTUAL_ENV=/app/.venv \
# EFFICIENCY: Only copies what's needed for runtime
COPY --from=build --chown=nonroot:nonroot /app/.venv /app/.venv
COPY --from=build --chown=nonroot:nonroot /app/tux /app/tux
COPY --from=build --chown=nonroot:nonroot /app/prisma /app/prisma
COPY --from=build --chown=nonroot:nonroot /app/config /app/config
COPY --from=build --chown=nonroot:nonroot /app/pyproject.toml /app/pyproject.toml
COPY --from=build --chown=nonroot:nonroot /app/VERSION /app/VERSION
Expand Down Expand Up @@ -385,8 +382,7 @@ RUN set -eux; \
# SECURITY: Application runs with minimal privileges
# RUNTIME: Ensures Prisma binaries and client are properly configured as nonroot user
USER nonroot
RUN /app/.venv/bin/python -m prisma py fetch && \
/app/.venv/bin/python -m prisma generate
RUN true

# Aggressive cleanup and optimization after Prisma setup
# PERFORMANCE: Single RUN reduces layer count and enables atomic cleanup
Expand All @@ -404,18 +400,18 @@ RUN set -eux; \
# Remove test directories from installed packages (but preserve prisma binaries)
# These directories contain test files that are not needed in production
for test_dir in tests testing "*test*"; do \
find /app/.venv -name "$test_dir" -type d -not -path "*/prisma*" -exec rm -rf {} + 2>/dev/null || true; \
find /app/.venv -name "$test_dir" -type d -exec rm -rf {} + 2>/dev/null || true; \
done; \
\
# Remove documentation files from installed packages (but preserve prisma docs)
# Remove documentation files from installed packages
# These files take up significant space and are not needed in production
for doc_pattern in "*.md" "*.txt" "*.rst" "LICENSE*" "NOTICE*" "COPYING*" "CHANGELOG*" "README*" "HISTORY*" "AUTHORS*" "CONTRIBUTORS*"; do \
find /app/.venv -name "$doc_pattern" -not -path "*/prisma*" -delete 2>/dev/null || true; \
find /app/.venv -name "$doc_pattern" -delete 2>/dev/null || true; \
done; \
\
# Remove large development packages that are not needed in production
# These packages (pip, setuptools, wheel) are only needed for installing packages
# NOTE: Preserving packages that Prisma might need
# NOTE: Cleanup complete
for pkg in setuptools wheel pkg_resources; do \
rm -rf /app/.venv/lib/python3.13/site-packages/${pkg}* 2>/dev/null || true; \
rm -rf /app/.venv/bin/${pkg}* 2>/dev/null || true; \
Expand Down
35 changes: 35 additions & 0 deletions alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[alembic]
script_location = alembic
sqlalchemy.url = env:DATABASE_URL

[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
1 change: 1 addition & 0 deletions alembic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Alembic package
104 changes: 104 additions & 0 deletions alembic/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Alembic environment script auto-generated for SQLModel metadata.

This file wires Alembic to the *runtime* SQLModel metadata defined in
`tux.database.models` so that `alembic revision --autogenerate` correctly
reflects changes to the declarative models.
"""

from __future__ import annotations

import os
from logging.config import fileConfig
from typing import Any

from sqlalchemy import engine_from_config, pool
from sqlmodel import SQLModel

from alembic import context

# Import models so their metadata is registered.
from tux.database import models as _models # noqa: F401 # pylint: disable=unused-import

# --------------------------------------------------------
# Alembic configuration
# --------------------------------------------------------

config = context.config # type: ignore[attr-defined]

# Interpret the config file for Python logging.
fileConfig(config.config_file_name) # type: ignore[arg-type]

# Target metadata for 'autogenerate' support.
# We rely on SQLModel which stores all tables in SQLModel.metadata.

target_metadata = SQLModel.metadata # type: ignore[attr-defined]

# Database URL: use env or alembic.ini → sqlalchemy.url

DATABASE_URL = os.getenv("DATABASE_URL", config.get_main_option("sqlalchemy.url"))

if DATABASE_URL.startswith("postgresql://") and "+asyncpg" not in DATABASE_URL:
# Autogenerate uses *sync* engine - strip async driver if present.
DATABASE_URL = DATABASE_URL.replace("postgresql://", "postgresql+psycopg2://", 1)


# --------------------------------------------------------
# Helper functions
# --------------------------------------------------------


def get_engine_config(overrides: dict[str, Any] | None = None) -> dict[str, Any]:
"""Return the SQLAlchemy Engine configuration for Alembic."""

cfg = config.get_section(config.config_ini_section) # type: ignore[arg-type]
assert cfg is not None
cfg = dict(cfg)
cfg["sqlalchemy.url"] = DATABASE_URL
if overrides:
cfg.update(overrides)
return cfg


# --------------------------------------------------------
# Offline / online migration runners
# --------------------------------------------------------


def run_migrations_offline() -> None:
"""Run migrations without an Engine (generates SQL scripts)."""

context.configure(
url=DATABASE_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 with a live DB connection."""

connectable = engine_from_config(
get_engine_config({"sqlalchemy.echo": "False"}),
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()


# --------------------------------------------------------
# Entrypoint
# --------------------------------------------------------

if context.is_offline_mode(): # type: ignore[attr-defined]
run_migrations_offline()
else:
run_migrations_online()
29 changes: 29 additions & 0 deletions alembic/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""${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
import sqlmodel
${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"}
Empty file added alembic/versions/__init__.py
Empty file.
Loading
Loading