From 72b03d43f18170363fa031c82cfd721afc141d00 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 30 May 2026 23:32:48 +0530 Subject: [PATCH 1/2] refactor: restructure backend into layered app/ package --- .gitignore | 4 +- api/db/database.py | 20 -------- api/main.py | 42 ----------------- app/__init__.py | 8 ++++ {api => app/api}/__init__.py | 0 {api => app/api}/deps.py | 2 +- app/api/router.py | 12 +++++ {api => app/api}/routes/__init__.py | 0 {api => app/api}/routes/forms.py | 24 +++++----- {api => app/api}/routes/templates.py | 15 +++--- {api/db => app/api/schemas}/__init__.py | 0 {api => app/api}/schemas/common.py | 0 {api => app/api}/schemas/forms.py | 1 - {api => app/api}/schemas/templates.py | 0 {api/errors => app/core}/__init__.py | 0 app/core/config.py | 47 +++++++++++++++++++ {api/schemas => app/core/errors}/__init__.py | 0 {api => app/core}/errors/base.py | 0 {api => app/core}/errors/handlers.py | 2 +- app/core/lifespan.py | 17 +++++++ app/core/logging.py | 14 ++++++ {src => app/db}/__init__.py | 0 app/db/database.py | 14 ++++++ {api => app}/db/init_db.py | 34 +++++++++----- {api => app}/db/repositories.py | 2 +- app/main.py | 34 ++++++++++++++ app/models/__init__.py | 5 ++ {api/db => app/models}/models.py | 0 app/services/__init__.py | 0 {src => app/services}/controller.py | 2 +- {src => app/services}/file_manipulator.py | 4 +- {src => app/services}/filler.py | 2 +- {src => app/services}/inputs/input.txt | 0 {src => app/services}/llm.py | 7 +-- .../services}/outputs/test_output_1.json | 0 {src => app/services}/prompt.txt | 0 .../services}/test/example_template.json | 0 {src => app/services}/test/test_output_1.json | 0 examples/pipeline_demo.py | 27 +++++++++++ src/main.py | 42 ----------------- tests/conftest.py | 10 ++-- tests/test_api.py | 2 +- {src/test => tests}/test_model.py | 0 43 files changed, 238 insertions(+), 155 deletions(-) delete mode 100644 api/db/database.py delete mode 100644 api/main.py create mode 100644 app/__init__.py rename {api => app/api}/__init__.py (100%) rename {api => app/api}/deps.py (51%) create mode 100644 app/api/router.py rename {api => app/api}/routes/__init__.py (100%) rename {api => app/api}/routes/forms.py (85%) rename {api => app/api}/routes/templates.py (95%) rename {api/db => app/api/schemas}/__init__.py (100%) rename {api => app/api}/schemas/common.py (100%) rename {api => app/api}/schemas/forms.py (90%) rename {api => app/api}/schemas/templates.py (100%) rename {api/errors => app/core}/__init__.py (100%) create mode 100644 app/core/config.py rename {api/schemas => app/core/errors}/__init__.py (100%) rename {api => app/core}/errors/base.py (100%) rename {api => app/core}/errors/handlers.py (88%) create mode 100644 app/core/lifespan.py create mode 100644 app/core/logging.py rename {src => app/db}/__init__.py (100%) create mode 100644 app/db/database.py rename {api => app}/db/init_db.py (65%) rename {api => app}/db/repositories.py (93%) create mode 100644 app/main.py create mode 100644 app/models/__init__.py rename {api/db => app/models}/models.py (100%) create mode 100644 app/services/__init__.py rename {src => app/services}/controller.py (87%) rename {src => app/services}/file_manipulator.py (96%) rename {src => app/services}/filler.py (97%) rename {src => app/services}/inputs/input.txt (100%) rename {src => app/services}/llm.py (94%) rename {src => app/services}/outputs/test_output_1.json (100%) rename {src => app/services}/prompt.txt (100%) rename {src => app/services}/test/example_template.json (100%) rename {src => app/services}/test/test_output_1.json (100%) create mode 100644 examples/pipeline_demo.py delete mode 100644 src/main.py rename {src/test => tests}/test_model.py (100%) diff --git a/.gitignore b/.gitignore index b5a9a90..268a0ff 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ src/inputs/*.pdf frontend/release/ # Local Claude Code instructions -CLAUDE.md \ No newline at end of file +CLAUDE.md + +*temp/ \ No newline at end of file diff --git a/api/db/database.py b/api/db/database.py deleted file mode 100644 index 97e6e62..0000000 --- a/api/db/database.py +++ /dev/null @@ -1,20 +0,0 @@ -import os -from sqlmodel import create_engine, Session - -# Define path to the database in the user's home directory -HOME_DIR = os.path.expanduser("~") -APP_DIR = os.path.join(HOME_DIR, ".fireform") -os.makedirs(APP_DIR, exist_ok=True) -DB_PATH = os.path.join(APP_DIR, "fireform.db") - -DATABASE_URL = f"sqlite:///{DB_PATH}" - -engine = create_engine( - DATABASE_URL, - echo=True, - connect_args={"check_same_thread": False}, -) - -def get_session(): - with Session(engine) as session: - yield session \ No newline at end of file diff --git a/api/main.py b/api/main.py deleted file mode 100644 index 0c30bc5..0000000 --- a/api/main.py +++ /dev/null @@ -1,42 +0,0 @@ -from contextlib import asynccontextmanager -import os - -# Disable CUDA to prevent PyTorch from trying to find NVIDIA drivers on Mac Silicon / Docker -os.environ["CUDA_VISIBLE_DEVICES"] = "" - -from fastapi import FastAPI -from api.routes import templates, forms -from api.db.init_db import init_db -from api.errors.handlers import register_exception_handlers -from fastapi.middleware.cors import CORSMiddleware -from api.routes import forms, templates - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Startup: Initialize the database and seed it if necessary - print("Initializing database...") - init_db() - yield - # Shutdown logic goes here if needed - -app = FastAPI(lifespan=lifespan) - -register_exception_handlers(app) - -default_origins = "http://127.0.0.1:5173,http://localhost:5173" -allowed_origins = [ - origin.strip() - for origin in os.getenv("FRONTEND_ORIGINS", default_origins).split(",") - if origin.strip() -] - -app.add_middleware( - CORSMiddleware, - allow_origins=allowed_origins, - allow_credentials=False, - allow_methods=["*"], - allow_headers=["*"], -) - -app.include_router(templates.router) -app.include_router(forms.router) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..de9379b --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,8 @@ +"""FireForm backend application package.""" + +import os + +# Force CPU before any service module imports torch / rfdetr. Prevents PyTorch +# from probing for NVIDIA drivers on Mac Silicon and inside Docker. Runs here so +# it is guaranteed to execute before `app.main` imports the service layer. +os.environ["CUDA_VISIBLE_DEVICES"] = "" diff --git a/api/__init__.py b/app/api/__init__.py similarity index 100% rename from api/__init__.py rename to app/api/__init__.py diff --git a/api/deps.py b/app/api/deps.py similarity index 51% rename from api/deps.py rename to app/api/deps.py index c2a0b74..4aa9614 100644 --- a/api/deps.py +++ b/app/api/deps.py @@ -1,4 +1,4 @@ -from api.db.database import get_session +from app.db.database import get_session def get_db(): yield from get_session() \ No newline at end of file diff --git a/app/api/router.py b/app/api/router.py new file mode 100644 index 0000000..ba9c0b5 --- /dev/null +++ b/app/api/router.py @@ -0,0 +1,12 @@ +"""Aggregates every route module into a single API router. + +Add new feature routers here; main.py only mounts this one router. +""" + +from fastapi import APIRouter + +from app.api.routes import forms, templates + +api_router = APIRouter() +api_router.include_router(templates.router) +api_router.include_router(forms.router) diff --git a/api/routes/__init__.py b/app/api/routes/__init__.py similarity index 100% rename from api/routes/__init__.py rename to app/api/routes/__init__.py diff --git a/api/routes/forms.py b/app/api/routes/forms.py similarity index 85% rename from api/routes/forms.py rename to app/api/routes/forms.py index 74069ea..7af5da6 100644 --- a/api/routes/forms.py +++ b/app/api/routes/forms.py @@ -1,19 +1,19 @@ -import os - import requests from fastapi import APIRouter, Depends, File, UploadFile from sqlmodel import Session -from api.deps import get_db -from api.schemas.forms import ( + +from app.api.deps import get_db +from app.api.schemas.forms import ( FormFill, FormFillResponse, ModelsResponse, TranscriptionResponse, ) -from api.db.repositories import create_form, get_template -from api.db.models import FormSubmission -from api.errors.base import AppError -from src.controller import Controller +from app.core.config import OLLAMA_HOST, OLLAMA_MODEL, WHISPER_HOST +from app.core.errors.base import AppError +from app.db.repositories import create_form, get_template +from app.models import FormSubmission +from app.services.controller import Controller router = APIRouter(prefix="/forms", tags=["forms"]) @@ -48,12 +48,11 @@ def list_models(): """List the Whisper-independent extraction models available in the local Ollama instance, plus the configured default. Used by the Fill Form UI's model picker. Falls back to just the default if Ollama is unreachable.""" - default_model = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") - ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") + default_model = OLLAMA_MODEL models: list[str] = [] try: - response = requests.get(f"{ollama_host}/api/tags", timeout=10) + response = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=10) response.raise_for_status() models = [m["name"] for m in response.json().get("models", []) if m.get("name")] except requests.exceptions.RequestException: @@ -75,8 +74,7 @@ def transcribe(audio: UploadFile = File(...)): audio is streamed straight through to the local STT service and never persisted — no PII leaves the machine. """ - whisper_host = os.getenv("WHISPER_HOST", "http://localhost:9000").rstrip("/") - whisper_url = f"{whisper_host}/asr" + whisper_url = f"{WHISPER_HOST}/asr" files = { "audio_file": ( diff --git a/api/routes/templates.py b/app/api/routes/templates.py similarity index 95% rename from api/routes/templates.py rename to app/api/routes/templates.py index 64db2e0..cc98370 100644 --- a/api/routes/templates.py +++ b/app/api/routes/templates.py @@ -5,21 +5,22 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile from fastapi.responses import FileResponse from sqlmodel import Session -from api.deps import get_db -from api.schemas.templates import ( + +from app.api.deps import get_db +from app.api.schemas.templates import ( TemplateCreate, TemplateResponse, TemplateUploadResponse, MakeFillableRequest, MakeFillableResponse, ) -from api.db.repositories import create_template, list_templates -from api.db.models import Template -from src.controller import Controller +from app.core.config import BASE_DIR, DEFAULT_TEMPLATE_DIR +from app.db.repositories import create_template, list_templates +from app.models import Template +from app.services.controller import Controller router = APIRouter(prefix="/templates", tags=["templates"]) -PROJECT_ROOT = Path(__file__).resolve().parents[2] -DEFAULT_TEMPLATE_DIR = "src/inputs" +PROJECT_ROOT = BASE_DIR def _resolve_target_directory(directory: str) -> Path: diff --git a/api/db/__init__.py b/app/api/schemas/__init__.py similarity index 100% rename from api/db/__init__.py rename to app/api/schemas/__init__.py diff --git a/api/schemas/common.py b/app/api/schemas/common.py similarity index 100% rename from api/schemas/common.py rename to app/api/schemas/common.py diff --git a/api/schemas/forms.py b/app/api/schemas/forms.py similarity index 90% rename from api/schemas/forms.py rename to app/api/schemas/forms.py index ca99651..6a833c8 100644 --- a/api/schemas/forms.py +++ b/app/api/schemas/forms.py @@ -4,7 +4,6 @@ class FormFill(BaseModel): template_id: int input_text: str # Optional Ollama model override for this fill; falls back to OLLAMA_MODEL. - # Not persisted (no DB column) — excluded before building FormSubmission. model: str | None = None @field_validator("input_text") diff --git a/api/schemas/templates.py b/app/api/schemas/templates.py similarity index 100% rename from api/schemas/templates.py rename to app/api/schemas/templates.py diff --git a/api/errors/__init__.py b/app/core/__init__.py similarity index 100% rename from api/errors/__init__.py rename to app/core/__init__.py diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..055bbad --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,47 @@ +"""Central configuration. + +Single source of truth for paths, the database URL, external service hosts and +CORS. Read environment once here so the rest of the app imports settings instead +of calling os.getenv() in scattered places. +""" + +import os +from pathlib import Path + +# Repo root. config.py lives at app/core/config.py -> parents[2] is the repo root. +BASE_DIR = Path(__file__).resolve().parents[2] + +# --- App metadata --------------------------------------------------------- +APP_TITLE = "FireForm API" +APP_VERSION = "1.1.0" + +# --- Runtime data paths --------------------------------------------------- +# Uploaded templates and generated PDFs. Project-relative paths the API echoes +# back to the client are resolved against BASE_DIR (the "inside the project" +# guard in the templates routes). Override the data dir with FIREFORM_DATA_DIR. +DATA_DIR = Path(os.getenv("FIREFORM_DATA_DIR", BASE_DIR / "data")).resolve() + +# Directory new uploads land in, as a project-relative string (was "src/inputs" +# before the restructure). Override with FIREFORM_TEMPLATE_DIR. +DEFAULT_TEMPLATE_DIR = os.getenv("FIREFORM_TEMPLATE_DIR", "data/inputs") + +# --- Database ------------------------------------------------------------- +# Keep the SQLite file in the user's home so it survives container rebuilds. +_APP_HOME = Path(os.path.expanduser("~")) / ".fireform" +_APP_HOME.mkdir(parents=True, exist_ok=True) +DB_PATH = Path(os.getenv("FIREFORM_DB_PATH", _APP_HOME / "fireform.db")) +DATABASE_URL = f"sqlite:///{DB_PATH}" +DB_ECHO = os.getenv("FIREFORM_DB_ECHO", "true").lower() == "true" + +# --- External services ---------------------------------------------------- +OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") +OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") +WHISPER_HOST = os.getenv("WHISPER_HOST", "http://localhost:9000").rstrip("/") + +# --- CORS ----------------------------------------------------------------- +_DEFAULT_ORIGINS = "http://127.0.0.1:5173,http://localhost:5173" +ALLOWED_ORIGINS = [ + origin.strip() + for origin in os.getenv("FRONTEND_ORIGINS", _DEFAULT_ORIGINS).split(",") + if origin.strip() +] diff --git a/api/schemas/__init__.py b/app/core/errors/__init__.py similarity index 100% rename from api/schemas/__init__.py rename to app/core/errors/__init__.py diff --git a/api/errors/base.py b/app/core/errors/base.py similarity index 100% rename from api/errors/base.py rename to app/core/errors/base.py diff --git a/api/errors/handlers.py b/app/core/errors/handlers.py similarity index 88% rename from api/errors/handlers.py rename to app/core/errors/handlers.py index 903e744..0285ddb 100644 --- a/api/errors/handlers.py +++ b/app/core/errors/handlers.py @@ -1,6 +1,6 @@ from fastapi import Request from fastapi.responses import JSONResponse -from api.errors.base import AppError +from app.core.errors.base import AppError def register_exception_handlers(app): @app.exception_handler(AppError) diff --git a/app/core/lifespan.py b/app/core/lifespan.py new file mode 100644 index 0000000..7f84273 --- /dev/null +++ b/app/core/lifespan.py @@ -0,0 +1,17 @@ +"""Application lifespan: startup and shutdown hooks.""" + +import logging +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from app.db.init_db import init_db + +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info("Initializing database...") + init_db() + yield diff --git a/app/core/logging.py b/app/core/logging.py new file mode 100644 index 0000000..48d28a8 --- /dev/null +++ b/app/core/logging.py @@ -0,0 +1,14 @@ +"""Logging setup. Call setup_logging() once at app startup.""" + +import logging + + +def setup_logging(level: str = "INFO") -> None: + logging.basicConfig( + level=level, + format="%(asctime)s %(levelname)-8s %(name)s: %(message)s", + ) + + +def get_logger(name: str) -> logging.Logger: + return logging.getLogger(name) diff --git a/src/__init__.py b/app/db/__init__.py similarity index 100% rename from src/__init__.py rename to app/db/__init__.py diff --git a/app/db/database.py b/app/db/database.py new file mode 100644 index 0000000..0437afd --- /dev/null +++ b/app/db/database.py @@ -0,0 +1,14 @@ +from sqlmodel import Session, create_engine + +from app.core.config import DATABASE_URL, DB_ECHO + +engine = create_engine( + DATABASE_URL, + echo=DB_ECHO, + connect_args={"check_same_thread": False}, +) + + +def get_session(): + with Session(engine) as session: + yield session diff --git a/api/db/init_db.py b/app/db/init_db.py similarity index 65% rename from api/db/init_db.py rename to app/db/init_db.py index ea77cb6..fc17147 100644 --- a/api/db/init_db.py +++ b/app/db/init_db.py @@ -1,9 +1,15 @@ -import json import datetime -from sqlmodel import SQLModel, Session, select -from api.db.database import engine -from api.db import models -from api.db.models import Template +import logging + +from sqlmodel import Session, SQLModel, select + +from app.core.config import DEFAULT_TEMPLATE_DIR +from app.db.database import engine + +from app.models import FormSubmission, Template # noqa: F401 + +logger = logging.getLogger(__name__) + def seed_db(): with Session(engine) as session: @@ -14,9 +20,9 @@ def seed_db(): except Exception: # Table might not exist yet if called at a weird time results = None - + if not results: - print("Seeding database with default template...") + logger.info("Seeding database with default template...") fields = { "Employee's name": "string", "Employee's job title": "string", @@ -24,24 +30,26 @@ def seed_db(): "Employee's phone number": "string", "Employee's email": "string", "Signature": "string", - "Date": "string" + "Date": "string", } - + # Using ID 2 as agreed to avoid any ID 1 corruption default_template = Template( id=2, name="Manual Test Template", fields=fields, - pdf_path="src/inputs/file_template_manual.pdf", - created_at=datetime.datetime.now() + pdf_path=f"{DEFAULT_TEMPLATE_DIR}/file_template_manual.pdf", + created_at=datetime.datetime.now(), ) session.add(default_template) session.commit() - print("Database seeded successfully.") + logger.info("Database seeded successfully.") + def init_db(): SQLModel.metadata.create_all(engine) seed_db() + if __name__ == "__main__": - init_db() \ No newline at end of file + init_db() diff --git a/api/db/repositories.py b/app/db/repositories.py similarity index 93% rename from api/db/repositories.py rename to app/db/repositories.py index a9ac3cf..ebb80de 100644 --- a/api/db/repositories.py +++ b/app/db/repositories.py @@ -1,5 +1,5 @@ from sqlmodel import Session, select -from api.db.models import Template, FormSubmission +from app.models import Template, FormSubmission # Templates def create_template(session: Session, template: Template) -> Template: diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..3d3bdad --- /dev/null +++ b/app/main.py @@ -0,0 +1,34 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.router import api_router +from app.core import config +from app.core.errors.handlers import register_exception_handlers +from app.core.lifespan import lifespan +from app.core.logging import setup_logging + + +def create_app() -> FastAPI: + setup_logging() + + app = FastAPI( + title=config.APP_TITLE, + version=config.APP_VERSION, + lifespan=lifespan, + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=config.ALLOWED_ORIGINS, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], + ) + + register_exception_handlers(app) + app.include_router(api_router) + + return app + + +app = create_app() diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..f46e17d --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,5 @@ +"""ORM models. Import from here: `from app.models import Template`.""" + +from app.models.models import FormSubmission, Template + +__all__ = ["Template", "FormSubmission"] diff --git a/api/db/models.py b/app/models/models.py similarity index 100% rename from api/db/models.py rename to app/models/models.py diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controller.py b/app/services/controller.py similarity index 87% rename from src/controller.py rename to app/services/controller.py index 0e19290..49c10e4 100644 --- a/src/controller.py +++ b/app/services/controller.py @@ -1,4 +1,4 @@ -from src.file_manipulator import FileManipulator +from app.services.file_manipulator import FileManipulator class Controller: def __init__(self): diff --git a/src/file_manipulator.py b/app/services/file_manipulator.py similarity index 96% rename from src/file_manipulator.py rename to app/services/file_manipulator.py index 8d1f3a0..357356e 100644 --- a/src/file_manipulator.py +++ b/app/services/file_manipulator.py @@ -1,6 +1,6 @@ import os -from src.filler import Filler -from src.llm import LLM +from app.services.filler import Filler +from app.services.llm import LLM class FileManipulator: diff --git a/src/filler.py b/app/services/filler.py similarity index 97% rename from src/filler.py rename to app/services/filler.py index 7f738c2..aec9756 100644 --- a/src/filler.py +++ b/app/services/filler.py @@ -1,5 +1,5 @@ from pdfrw import PdfReader, PdfWriter -from src.llm import LLM +from app.services.llm import LLM from datetime import datetime diff --git a/src/inputs/input.txt b/app/services/inputs/input.txt similarity index 100% rename from src/inputs/input.txt rename to app/services/inputs/input.txt diff --git a/src/llm.py b/app/services/llm.py similarity index 94% rename from src/llm.py rename to app/services/llm.py index 053b883..2998318 100644 --- a/src/llm.py +++ b/app/services/llm.py @@ -3,6 +3,8 @@ import requests from requests.exceptions import Timeout, RequestException +from app.core.config import OLLAMA_HOST, OLLAMA_MODEL + class LLM: def __init__(self, transcript_text: str=None, target_fields: list=None, json_dict: dict=None, model: str=None): @@ -31,9 +33,8 @@ def main_loop(self): total_fields = len(self._target_fields) for i, (field, field_type) in enumerate(self._target_fields.items(), 1): prompt = self.build_prompt(field, field_type if isinstance(field_type, str) else "string") - ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") - ollama_url = f"{ollama_host}/api/generate" - ollama_model = self._model or os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") + ollama_url = f"{OLLAMA_HOST}/api/generate" + ollama_model = self._model or OLLAMA_MODEL payload = { "model": ollama_model, diff --git a/src/outputs/test_output_1.json b/app/services/outputs/test_output_1.json similarity index 100% rename from src/outputs/test_output_1.json rename to app/services/outputs/test_output_1.json diff --git a/src/prompt.txt b/app/services/prompt.txt similarity index 100% rename from src/prompt.txt rename to app/services/prompt.txt diff --git a/src/test/example_template.json b/app/services/test/example_template.json similarity index 100% rename from src/test/example_template.json rename to app/services/test/example_template.json diff --git a/src/test/test_output_1.json b/app/services/test/test_output_1.json similarity index 100% rename from src/test/test_output_1.json rename to app/services/test/test_output_1.json diff --git a/examples/pipeline_demo.py b/examples/pipeline_demo.py new file mode 100644 index 0000000..dbad305 --- /dev/null +++ b/examples/pipeline_demo.py @@ -0,0 +1,27 @@ +"""Manual, end-to-end demo of the fill pipeline (not part of the package). + +Run from the repo root with a real PDF path. This is a developer convenience +for exercising the services layer without the API; it is not imported anywhere. +""" + +from commonforms import prepare_form +from pypdf import PdfReader + +from app.services.controller import Controller + +if __name__ == "__main__": + file = "./data/inputs/file.pdf" # update to a real input PDF + user_input = ( + "Hi. The employee's name is John Doe. His job title is managing director. " + "His department supervisor is Jane Doe. His phone number is 123456. " + "His email is jdoe@ucsc.edu. The signature is , and the date is 01/02/2005" + ) + prepared_pdf = "temp_outfile.pdf" + prepare_form(file, prepared_pdf) + + reader = PdfReader(prepared_pdf) + fields = reader.get_fields() + num_fields = len(fields) if fields else 0 + + controller = Controller() + controller.fill_form(user_input, fields, file) diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 630d262..0000000 --- a/src/main.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -os.environ["CUDA_VISIBLE_DEVICES"] = "" - -# Monkey patch rfdetr to force CPU usage on Mac Silicon / Docker -try: - import rfdetr.detr - original_ensure = rfdetr.detr._ensure_model_on_device - def patched_ensure(model_ctx): - model_ctx.device = "cpu" - original_ensure(model_ctx) - rfdetr.detr._ensure_model_on_device = patched_ensure -except ImportError: - pass - -from commonforms import prepare_form -from pypdf import PdfReader -from controller import Controller - -if __name__ == "__main__": - file = "./src/inputs/file.pdf" - user_input = "Hi. The employee's name is John Doe. His job title is managing director. His department supervisor is Jane Doe. His phone number is 123456. His email is jdoe@ucsc.edu. The signature is , and the date is 01/02/2005" - fields = [ - "Employee's name", - "Employee's job title", - "Employee's department supervisor", - "Employee's phone number", - "Employee's email", - "Signature", - "Date", - ] - prepared_pdf = "temp_outfile.pdf" - prepare_form(file, prepared_pdf) - - reader = PdfReader(prepared_pdf) - fields = reader.get_fields() - if fields: - num_fields = len(fields) - else: - num_fields = 0 - - controller = Controller() - controller.fill_form(user_input, fields, file) diff --git a/tests/conftest.py b/tests/conftest.py index 5f1eb3f..56dea60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,9 +12,9 @@ from sqlalchemy.pool import StaticPool from sqlmodel import SQLModel, Session, create_engine -from api.main import app -from api.deps import get_db -from api.db.models import Template, FormSubmission # noqa: F401 — registers tables +from app.main import app +from app.api.deps import get_db +from app.models import Template, FormSubmission # noqa: F401 — registers tables # --------------------------------------------------------------------------- # In-memory database @@ -85,8 +85,8 @@ def pdf_upload(pdf_bytes): @pytest.fixture def mock_controller(): """Patch Controller so create_template / fill_form don't touch the FS or LLM.""" - with patch("api.routes.templates.Controller") as tpl_cls, \ - patch("api.routes.forms.Controller") as form_cls: + with patch("app.api.routes.templates.Controller") as tpl_cls, \ + patch("app.api.routes.forms.Controller") as form_cls: tpl_instance = MagicMock() tpl_instance.create_template.return_value = "src/inputs/test_template.pdf" tpl_cls.return_value = tpl_instance diff --git a/tests/test_api.py b/tests/test_api.py index 89a9b25..66696b9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -7,7 +7,7 @@ import pytest from sqlmodel import select -from api.db.models import Template, FormSubmission +from app.models import Template, FormSubmission # ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/test/test_model.py b/tests/test_model.py similarity index 100% rename from src/test/test_model.py rename to tests/test_model.py From f4ee6fe909c6a0c45b9e17f9938cdf6a611b169f Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 30 May 2026 23:34:39 +0530 Subject: [PATCH 2/2] Updated the change of code structure layer in docker, makefile and workflows --- .github/workflows/release.yml | 2 +- Dockerfile | 4 ++-- Makefile | 4 +--- docker-compose.yml | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d6d1f83..9ddeaf3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: working-directory: . shell: bash run: | - pyinstaller --name api-backend --onefile api/main.py + pyinstaller --name api-backend --onefile app/main.py mkdir -p frontend/bin cp dist/api-backend* frontend/bin/ diff --git a/Dockerfile b/Dockerfile index 282eb25..b7b00ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,11 +18,11 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY . . -# All imports use api.*, src.* which require the root to be on the path +# All imports use the app.* package, which requires the root on the path ENV PYTHONPATH=/app # Expose FastAPI port EXPOSE 8000 # Start the FastAPI server (not tail -f /dev/null which does nothing) -CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/Makefile b/Makefile index 026a721..3f15533 100644 --- a/Makefile +++ b/Makefile @@ -67,10 +67,8 @@ shell: # Start the FastAPI server inside the running container run: - docker compose exec app uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload + docker compose exec app uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload -exec: - docker compose exec app python3 src/main.py pull-model: docker compose exec ollama ollama pull $(OLLAMA_MODEL) diff --git a/docker-compose.yml b/docker-compose.yml index a9e8e6e..45ad034 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,7 +49,7 @@ services: condition: service_healthy whisper: condition: service_started - command: /bin/sh -c "python3 -m api.db.init_db && python3 -m uvicorn api.main:app --host 0.0.0.0 --port 8000" + command: /bin/sh -c "python3 -m app.db.init_db && python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000" volumes: - .:/app # Persist the SQLite DB (~/.fireform) across container rebuilds so created