Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# fastcache_api

API for fastcache

## Database

Schema is managed by Alembic (`src/fastcache_api/alembic/`), not
create-on-boot. After changing `tables.py`, or on a fresh checkout:

```sh
uv run alembic upgrade head # apply migrations
uv run python scripts/gen_migration.py "message" # autogenerate a new one
```

`alembic upgrade head` must be run once before the first start (and again
after pulling any change that adds a migration); the systemd unit does this
automatically via `ExecStartPre`.
16 changes: 16 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ authors = [
requires-python = ">=3.14"
dependencies = [
"aiosqlite>=0.22.1",
"alembic>=1.18.5",
"anyio>=4.14.0",
"fastapi>=0.138.0",
"fastapi-jwks>=2.0.2",
Expand All @@ -34,6 +35,21 @@ dev = [
"types-psutil>=7.2.2.20260518",
]

[tool.alembic]
script_location = "fastcache_api:alembic"

[[tool.alembic.post_write_hooks]]
name = "ruff_format"
type = "exec"
executable = "uv"
options = "run ruff format REVISION_SCRIPT_FILENAME"

[[tool.alembic.post_write_hooks]]
name = "ruff_check"
type = "exec"
executable = "uv"
options = "run ruff check --fix REVISION_SCRIPT_FILENAME"

[tool.ruff]
target-version = "py314"
line-length = 88
Expand Down
46 changes: 46 additions & 0 deletions scripts/gen_migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import sys
import tempfile
from pathlib import Path

from alembic import command
from alembic.config import Config
from sqlalchemy import create_engine

import fastcache_api.tables as _tables # noqa: F401 - registers tables on Base.metadata

REPO_ROOT = Path(__file__).parent.parent
VERSIONS_DIR = REPO_ROOT / "src/fastcache_api/alembic/versions"


def main() -> None:
message = (
" ".join(sys.argv[1:])
if len(sys.argv) > 1
else input("Migration name: ").strip()
)
if not message:
print("Aborted: migration name cannot be empty.")
sys.exit(1)

before = {f for f in VERSIONS_DIR.glob("*.py") if f.name != "__init__.py"}

with tempfile.TemporaryDirectory() as tmp:
db_path = Path(tmp) / "gen_migration.sqlite"
engine = create_engine(f"sqlite:///{db_path}")
cfg = Config(toml_file=str(REPO_ROOT / "pyproject.toml"))
cfg.set_main_option(
"sqlalchemy.url", engine.url.render_as_string(hide_password=False)
)
Comment thread
swelborn marked this conversation as resolved.
command.upgrade(cfg, "head")
command.revision(cfg, autogenerate=True, message=message)
engine.dispose()

new_files = {
f for f in VERSIONS_DIR.glob("*.py") if f.name != "__init__.py"
} - before
for f in sorted(new_files):
print(f"Generated: {f.name}")


if __name__ == "__main__":
main()
Empty file.
41 changes: 41 additions & 0 deletions src/fastcache_api/alembic/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import logging

from alembic import context
from sqlalchemy import create_engine, pool

import fastcache_api.tables as _tables # noqa: F401 - registers tables
from fastcache_api.config import settings
from fastcache_api.tables import Base

config = context.config
logging.basicConfig(level=logging.INFO)
target_metadata = Base.metadata


def get_url() -> str:
# set programmatically by scripts/gen_migration.py for a throwaway db
url = config.get_alembic_option("sqlalchemy.url")
if url:
return url
# for running migrations against the real db: alembic needs a sync
# driver, unlike the app's own aiosqlite engine.
return f"sqlite:///{settings.SQLITE_PATH}"
Comment thread
swelborn marked this conversation as resolved.


def run_migrations() -> None:
connectable = create_engine(get_url(), poolclass=pool.NullPool)

with connectable.connect() as connection:
# batch mode: sqlite can't ALTER most column properties in place,
# alembic works around it by recreating the table.
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=True,
)

with context.begin_transaction():
context.run_migrations()


run_migrations()
24 changes: 24 additions & 0 deletions src/fastcache_api/alembic/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: str | list[str] | None = ${repr(down_revision)}
branch_labels: str | list[str] | None = ${repr(branch_labels)}
depends_on: str | list[str] | None = ${repr(depends_on)}


def upgrade():
${upgrades if upgrades else "pass"}


def downgrade():
${downgrades if downgrades else "pass"}
53 changes: 53 additions & 0 deletions src/fastcache_api/alembic/versions/1d91b0606408_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""init

Revision ID: 1d91b0606408
Revises:
Create Date: 2026-07-10 16:20:35.643455

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "1d91b0606408"
down_revision: str | list[str] | None = None
branch_labels: str | list[str] | None = None
depends_on: str | list[str] | None = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"caches",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("key", sa.String(), nullable=True),
sa.Column("pid", sa.Integer(), nullable=False),
sa.Column("create_time", sa.Float(), nullable=True),
sa.Column("user", sa.String(), nullable=False),
sa.Column("state", sa.String(), nullable=False),
sa.Column("exit_code", sa.Integer(), nullable=True),
sa.Column("log_path", sa.String(), nullable=False),
sa.Column("config", sa.JSON(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("key"),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("caches")
# ### end Alembic commands ###
Empty file.
6 changes: 0 additions & 6 deletions src/fastcache_api/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
)

from .config import settings
from .tables import Base

engine: AsyncEngine = create_async_engine(settings.DATABASE_URL)

Expand All @@ -17,11 +16,6 @@
)


async def init_db() -> None:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)


async def get_session() -> AsyncGenerator[AsyncSession]:
async with SessionLocal() as session:
yield session
2 changes: 0 additions & 2 deletions src/fastcache_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from fastapi.routing import APIRoute

from .config import settings
from .db import init_db
from .lifecycle import exit_watchers
from .reconcile import monitor_caches, reconcile_caches
from .routes import api_router
Expand All @@ -25,7 +24,6 @@ def custom_generate_unique_id(route: APIRoute) -> str:
@asynccontextmanager
async def lifespan(application: FastAPI) -> AsyncGenerator[None]:
logger.info("Starting %s...", settings.PROJECT_NAME)
await init_db()
await reconcile_caches()
monitor = asyncio.create_task(monitor_caches())
try:
Expand Down
1 change: 1 addition & 0 deletions systemd/fastcache-api.service
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ StartLimitBurst=5
Type=simple
WorkingDirectory=%h/gits/lclstream/fastcache_api
EnvironmentFile=%h/.config/fastcache_api/fastcache_api.env
ExecStartPre=%h/gits/lclstream/fastcache_api/.venv/bin/alembic upgrade head
# systemd 239 here predates StandardOutput=append: (needs v240+), so redirect
# via a shell instead. Logs (uvicorn + our own logging) land in a real file
# under .local/state instead of only the (often unreadable) user journal.
Expand Down
58 changes: 58 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.