Skip to content

Commit a4ad252

Browse files
committed
Merge branch 'development' into deck-pitch
2 parents 08eb9f2 + ac186a0 commit a4ad252

16 files changed

Lines changed: 434 additions & 42 deletions

File tree

.github/workflows/lint.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ jobs:
1919
with:
2020
python-version: "3.11"
2121

22+
- name: Install uv
23+
uses: astral-sh/setup-uv@v6
24+
2225
- name: Install linter
23-
run: |
24-
python -m pip install --upgrade pip
25-
pip install ruff
26+
run: uv pip install --system ruff
2627

2728
- name: Run linter
2829
run: |

.github/workflows/tests.yml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,20 @@ jobs:
1818
with:
1919
python-version: "3.11"
2020

21+
- name: Install uv
22+
uses: astral-sh/setup-uv@v6
23+
2124
- name: Install dependencies
22-
run: |
23-
python -m pip install --upgrade pip
24-
pip install -r requirements.txt
25+
run: uv pip install --system -r requirements.txt
2526

2627
- name: Run tests
2728
env:
2829
PYTHONPATH: .
2930
run: |
30-
python -m pytest tests/ -v --tb=short
31+
python -m pytest tests/ -v --tb=short
32+
33+
- name: Check for un-migrated model changes
34+
env:
35+
PYTHONPATH: .
36+
run: |
37+
python -m alembic check

Makefile

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean
1+
.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean status ready-banner sync
22

33
COMPOSE = docker compose -f docker/dev/compose.yml --env-file docker/.env.dev
44
ENV_DEV = docker/.env.dev
@@ -22,13 +22,17 @@ help:
2222
@echo "make build - Build Docker images"
2323
@echo "make up - Start all containers (detached)"
2424
@echo "make down - Stop all containers"
25+
@echo "make sync - Fast-install new requirements.txt deps into running app (no rebuild)"
26+
@echo "make status - Show compact container health summary"
2527
@echo "make logs - Stream all container logs"
2628
@echo "make logs-app - Stream app container logs"
2729
@echo "make logs-ollama - Stream Ollama container logs"
2830
@echo "make logs-worker - Stream Celery worker logs"
2931
@echo "make shell - Open shell in running app container"
3032
@echo "make pull-model - Pull Ollama model from .env.dev ($(OLLAMA_MODEL))"
3133
@echo "make test - Run test suite"
34+
@echo "make migrate - Run pending Alembic migrations"
35+
@echo "make migration - Generate new migration (msg='description')"
3236
@echo "make clean - Stop containers (preserves volumes)"
3337
@echo "make super-clean - [CAUTION] Stop containers, delete volumes, prune Docker"
3438

@@ -44,30 +48,38 @@ init:
4448
*) echo "Run 'make fireform' when ready." ;; \
4549
esac
4650

47-
fireform: build up
48-
@printf "Waiting for Ollama to be ready..."
49-
@until $(COMPOSE) exec -T ollama ollama list > /dev/null 2>&1; do \
50-
printf '.'; sleep 2; \
51-
done
52-
@echo " ready."
51+
fireform:
52+
@$(COMPOSE) up -d --build
5353
@if $(COMPOSE) exec -T ollama ollama list 2>/dev/null | grep -q "^$(OLLAMA_MODEL)"; then \
5454
echo " Model $(OLLAMA_MODEL) already pulled."; \
5555
else \
5656
echo " Pulling $(OLLAMA_MODEL)..."; \
5757
$(COMPOSE) exec -T ollama ollama pull $(OLLAMA_MODEL); \
5858
fi
59-
@echo ""
60-
@echo "FireForm is ready!"
61-
@echo " API: http://localhost:8000"
62-
@echo " API Docs: http://localhost:8000/docs"
63-
@echo ""
64-
@echo "Run 'make logs' to view live logs, 'make down' to stop."
59+
@$(MAKE) --no-print-directory ready-banner
6560

6661
build:
6762
@$(COMPOSE) build
6863

6964
up:
7065
@$(COMPOSE) up -d
66+
@$(MAKE) --no-print-directory ready-banner
67+
68+
# Fast path for "I added a package": install the delta into the running container
69+
# (no image rebuild, no 1.6GB layer re-export). uv installs only what's missing in
70+
sync:
71+
@$(COMPOSE) exec -T app sh -c "UV_TORCH_BACKEND=cpu uv pip install --system -r requirements.txt"
72+
73+
status:
74+
@$(COMPOSE) ps --format 'table {{.Service}}\t{{.Status}}'
75+
76+
ready-banner:
77+
@echo ""
78+
@echo "FireForm is ready!"
79+
@echo " API: http://localhost:8000"
80+
@echo " API Docs: http://localhost:8000/docs"
81+
@echo ""
82+
@echo "Run 'make logs' to view live logs, 'make down' to stop."
7183

7284
down:
7385
@$(COMPOSE) down --remove-orphans
@@ -93,6 +105,12 @@ pull-model:
93105
test:
94106
@$(COMPOSE) exec -T app python3 -m pytest tests/ -v
95107

108+
migrate:
109+
@$(COMPOSE) exec -T app alembic upgrade head
110+
111+
migration:
112+
@$(COMPOSE) exec -T app alembic revision --autogenerate -m "$(msg)"
113+
96114
clean:
97115
@$(COMPOSE) down
98116

alembic.ini

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[alembic]
2+
script_location = alembic
3+
prepend_sys_path = .
4+
path_separator = os
5+
6+
[loggers]
7+
keys = root,sqlalchemy,alembic
8+
9+
[handlers]
10+
keys = console
11+
12+
[formatters]
13+
keys = generic
14+
15+
[logger_root]
16+
level = WARN
17+
handlers = console
18+
19+
[logger_sqlalchemy]
20+
level = WARN
21+
handlers =
22+
qualname = sqlalchemy.engine
23+
24+
[logger_alembic]
25+
level = INFO
26+
handlers =
27+
qualname = alembic
28+
29+
[handler_console]
30+
class = StreamHandler
31+
args = (sys.stderr,)
32+
level = NOTSET
33+
formatter = generic
34+
35+
[formatter_generic]
36+
format = %(levelname)-5.5s [%(name)s] %(message)s
37+
datefmt = %H:%M:%S

alembic/env.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from logging.config import fileConfig
2+
3+
from alembic import context
4+
from sqlalchemy import engine_from_config, pool
5+
6+
from sqlmodel import SQLModel
7+
8+
from app.core.config import DATABASE_URL
9+
from app.models import FormSubmission, Job, Template
10+
11+
config = context.config
12+
13+
if not config.get_main_option("sqlalchemy.url"):
14+
config.set_main_option("sqlalchemy.url", DATABASE_URL)
15+
16+
if config.config_file_name is not None:
17+
fileConfig(config.config_file_name)
18+
19+
target_metadata = SQLModel.metadata
20+
21+
22+
def run_migrations_offline():
23+
url = config.get_main_option("sqlalchemy.url")
24+
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
25+
with context.begin_transaction():
26+
context.run_migrations()
27+
28+
29+
def run_migrations_online():
30+
connectable = config.attributes.get("connection", None)
31+
32+
if connectable is None:
33+
connectable = engine_from_config(
34+
config.get_section(config.config_ini_section, {}),
35+
prefix="sqlalchemy.",
36+
poolclass=pool.NullPool,
37+
)
38+
with connectable.connect() as connection:
39+
context.configure(connection=connection, target_metadata=target_metadata)
40+
with context.begin_transaction():
41+
context.run_migrations()
42+
else:
43+
context.configure(connection=connectable, target_metadata=target_metadata)
44+
with context.begin_transaction():
45+
context.run_migrations()
46+
47+
48+
if context.is_offline_mode():
49+
run_migrations_offline()
50+
else:
51+
run_migrations_online()

alembic/script.py.mako

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""${message}
2+
3+
Revision ID: ${up_revision}
4+
Revises: ${down_revision | comma,n}
5+
Create Date: ${create_date}
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
import sqlmodel
13+
${imports if imports else ""}
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = ${repr(up_revision)}
17+
down_revision: Union[str, None] = ${repr(down_revision)}
18+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
19+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
20+
21+
22+
def upgrade() -> None:
23+
${upgrades if upgrades else "pass"}
24+
25+
26+
def downgrade() -> None:
27+
${downgrades if downgrades else "pass"}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Initial schema — Template, FormSubmission, and Job tables.
2+
3+
Revision ID: 001
4+
Revises:
5+
Create Date: 2026-06-22
6+
7+
"""
8+
from collections.abc import Sequence
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
import sqlmodel
13+
14+
revision: str = "001"
15+
down_revision: str | None = None
16+
branch_labels: str | Sequence[str] | None = None
17+
depends_on: str | Sequence[str] | None = None
18+
19+
20+
def upgrade() -> None:
21+
op.create_table(
22+
"template",
23+
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
24+
sa.Column("name", sqlmodel.sql.sqltypes.AutoString, nullable=False),
25+
sa.Column("fields", sa.JSON, nullable=False),
26+
sa.Column("pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=False),
27+
sa.Column("created_at", sa.DateTime, nullable=False),
28+
)
29+
30+
op.create_table(
31+
"formsubmission",
32+
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
33+
sa.Column("template_id", sa.Integer, sa.ForeignKey("template.id"), nullable=False),
34+
sa.Column("input_text", sqlmodel.sql.sqltypes.AutoString, nullable=False),
35+
sa.Column("output_pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=False),
36+
sa.Column("created_at", sa.DateTime, nullable=False),
37+
)
38+
39+
op.create_table(
40+
"job",
41+
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
42+
sa.Column("job_id", sqlmodel.sql.sqltypes.AutoString, nullable=False),
43+
sa.Column("celery_task_id", sqlmodel.sql.sqltypes.AutoString, nullable=False),
44+
sa.Column("job_type", sqlmodel.sql.sqltypes.AutoString, nullable=False),
45+
sa.Column("template_id", sa.Integer, sa.ForeignKey("template.id"), nullable=True),
46+
sa.Column("input_text", sqlmodel.sql.sqltypes.AutoString, nullable=True),
47+
sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False),
48+
sa.Column("progress_percent", sa.Integer, nullable=False),
49+
sa.Column("result_url", sqlmodel.sql.sqltypes.AutoString, nullable=True),
50+
sa.Column("error", sa.JSON, nullable=True),
51+
sa.Column("model", sqlmodel.sql.sqltypes.AutoString, nullable=True),
52+
sa.Column("created_at", sa.DateTime, nullable=False),
53+
sa.Column("updated_at", sa.DateTime, nullable=False),
54+
)
55+
op.create_index("ix_job_job_id", "job", ["job_id"], unique=True)
56+
op.create_index("ix_job_celery_task_id", "job", ["celery_task_id"], unique=False)
57+
58+
59+
def downgrade() -> None:
60+
op.drop_index("ix_job_celery_task_id", table_name="job")
61+
op.drop_index("ix_job_job_id", table_name="job")
62+
op.drop_table("job")
63+
op.drop_table("formsubmission")
64+
op.drop_table("template")

app/db/init_db.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,37 @@
11
import datetime
22
import logging
3+
from pathlib import Path
34

4-
from sqlmodel import Session, SQLModel, select
5+
from alembic import command
6+
from alembic.config import Config
7+
from sqlmodel import Session, select
58

69
from app.core.config import DEFAULT_TEMPLATE_DIR
710
from app.db.database import engine
8-
911
from app.models import FormSubmission, Template # noqa: F401
1012

1113
logger = logging.getLogger(__name__)
1214

15+
ALEMBIC_INI = str(Path(__file__).resolve().parents[2] / "alembic.ini")
16+
17+
18+
def _alembic_cfg() -> Config:
19+
cfg = Config(ALEMBIC_INI)
20+
return cfg
21+
22+
23+
def run_migrations():
24+
logger.info("Running Alembic migrations...")
25+
command.upgrade(_alembic_cfg(), "head")
26+
logger.info("Migrations complete.")
27+
1328

1429
def seed_db():
1530
with Session(engine) as session:
16-
# Check if we already have templates
1731
statement = select(Template)
1832
try:
1933
results = session.exec(statement).first()
2034
except Exception:
21-
# Table might not exist yet if called at a weird time
2235
results = None
2336

2437
if not results:
@@ -33,7 +46,6 @@ def seed_db():
3346
"Date": "string",
3447
}
3548

36-
# Using ID 2 as agreed to avoid any ID 1 corruption
3749
default_template = Template(
3850
id=2,
3951
name="Manual Test Template",
@@ -47,7 +59,7 @@ def seed_db():
4759

4860

4961
def init_db():
50-
SQLModel.metadata.create_all(engine)
62+
run_migrations()
5163
seed_db()
5264

5365

app/models/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class Template(SQLModel, table=True):
99
id: int | None = Field(default=None, primary_key=True)
1010
name: str
11-
fields: dict = Field(sa_column=Column(JSON))
11+
fields: dict = Field(sa_column=Column(JSON, nullable=False))
1212
pdf_path: str
1313
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
1414

0 commit comments

Comments
 (0)