From d092c19b4105ebb48a956718845fce2aacf45a24 Mon Sep 17 00:00:00 2001 From: Martin Hohenberg Date: Thu, 18 Jun 2026 21:19:59 +0200 Subject: [PATCH] Scaffold SQLAlchemy models, routers, and Alembic migrations - app/models/: Printer, Filament, PrintJob, Todo, PricingConfig with full SQLAlchemy 2.x mapped columns and relationships - app/routers/: list + get-by-id endpoints for all five resources - app/db.py: SQLAlchemy engine + SessionLocal + get_db dependency - alembic/: env.py reads MYSQL_* env vars; 0001_initial_schema mirrors db.sql - alembic.ini: script_location configured, URL injected at runtime Since db.sql already created the tables, stamp the DB before running future migrations: alembic stamp head Co-Authored-By: Claude Sonnet 4.6 --- alembic.ini | 39 +++++++++ alembic/env.py | 54 ++++++++++++ alembic/script.py.mako | 25 ++++++ alembic/versions/0001_initial_schema.py | 112 ++++++++++++++++++++++++ app/db.py | 35 +++++--- app/main.py | 7 ++ app/models/__init__.py | 8 ++ app/models/base.py | 6 ++ app/models/filament.py | 38 ++++++++ app/models/pricing.py | 20 +++++ app/models/print_job.py | 41 +++++++++ app/models/printer.py | 33 +++++++ app/models/todo.py | 21 +++++ app/routers/__init__.py | 0 app/routers/filaments.py | 21 +++++ app/routers/pricing.py | 21 +++++ app/routers/print_jobs.py | 21 +++++ app/routers/printers.py | 21 +++++ app/routers/todos.py | 21 +++++ requirements.txt | 2 + 20 files changed, 534 insertions(+), 12 deletions(-) create mode 100644 alembic.ini create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/0001_initial_schema.py create mode 100644 app/models/__init__.py create mode 100644 app/models/base.py create mode 100644 app/models/filament.py create mode 100644 app/models/pricing.py create mode 100644 app/models/print_job.py create mode 100644 app/models/printer.py create mode 100644 app/models/todo.py create mode 100644 app/routers/__init__.py create mode 100644 app/routers/filaments.py create mode 100644 app/routers/pricing.py create mode 100644 app/routers/print_jobs.py create mode 100644 app/routers/printers.py create mode 100644 app/routers/todos.py diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..f2734d4 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,39 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +# URL is set at runtime from env vars in alembic/env.py +sqlalchemy.url = placeholder + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[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 diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..a1558c7 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,54 @@ +import os +from logging.config import fileConfig + +from sqlalchemy import engine_from_config, pool + +from alembic import context + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +from app.models.base import Base +import app.models # noqa: F401 — registers all models with Base.metadata + +target_metadata = Base.metadata + + +def get_url() -> str: + return ( + f"mysql+pymysql://{os.environ['MYSQL_USER']}:{os.environ['MYSQL_PASSWORD']}" + f"@{os.environ['MYSQL_HOST']}:{os.environ.get('MYSQL_PORT', 3306)}" + f"/{os.environ['MYSQL_DATABASE']}" + ) + + +def run_migrations_offline() -> None: + context.configure( + url=get_url(), + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + config.set_main_option("sqlalchemy.url", get_url()) + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + 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() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..17dcba0 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,25 @@ +"""${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 +${imports if imports else ""} + +revision: str = ${repr(up_revision)} +down_revision: Union[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: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/0001_initial_schema.py b/alembic/versions/0001_initial_schema.py new file mode 100644 index 0000000..b410e0a --- /dev/null +++ b/alembic/versions/0001_initial_schema.py @@ -0,0 +1,112 @@ +"""initial schema + +Revision ID: 0001 +Revises: +Create Date: 2026-06-18 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import mysql + +revision: str = "0001" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "printers", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("model", sa.String(length=100), nullable=True), + sa.Column("location", sa.String(length=100), nullable=True), + sa.Column( + "status", + mysql.ENUM("idle", "printing", "error", "offline"), + nullable=False, + server_default="offline", + ), + sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")), + sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "filaments", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("brand", sa.String(length=100), nullable=True), + sa.Column( + "material", + mysql.ENUM("PLA", "PETG", "ABS", "ASA", "TPU", "Nylon", "PC", "Other"), + nullable=False, + server_default="PLA", + ), + sa.Column("color", sa.String(length=100), nullable=True), + sa.Column("color_hex", sa.String(length=7), nullable=True), + sa.Column("weight_total_g", sa.Numeric(precision=8, scale=2), nullable=False), + sa.Column("weight_remaining_g", sa.Numeric(precision=8, scale=2), nullable=False), + sa.Column("price_per_kg_eur", sa.Numeric(precision=8, scale=2), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")), + sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "pricing_config", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=100), nullable=False, server_default="default"), + sa.Column("electricity_kwh_eur", sa.Numeric(precision=6, scale=4), nullable=False, server_default="0.3000"), + sa.Column("printer_watt", sa.Numeric(precision=8, scale=2), nullable=False, server_default="200.00"), + sa.Column("hourly_rate_eur", sa.Numeric(precision=8, scale=2), nullable=False, server_default="0.00"), + sa.Column("markup_percent", sa.Numeric(precision=5, scale=2), nullable=False, server_default="20.00"), + sa.Column("valid_from", sa.Date(), nullable=False, server_default=sa.text("(CURRENT_DATE)")), + sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "todos", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("title", sa.String(length=255), nullable=False), + sa.Column("done", sa.Boolean(), nullable=False, server_default="0"), + sa.Column("printer_id", sa.Integer(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")), + sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")), + sa.ForeignKeyConstraint(["printer_id"], ["printers.id"], name="fk_todo_printer"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "print_jobs", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("printer_id", sa.Integer(), nullable=False), + sa.Column("filament_id", sa.Integer(), nullable=True), + sa.Column("name", sa.String(length=200), nullable=False), + sa.Column("file_name", sa.String(length=255), nullable=True), + sa.Column( + "status", + mysql.ENUM("queued", "printing", "done", "failed", "cancelled"), + nullable=False, + server_default="queued", + ), + sa.Column("queue_position", sa.Integer(), nullable=True), + sa.Column("started_at", sa.DateTime(), nullable=True), + sa.Column("finished_at", sa.DateTime(), nullable=True), + sa.Column("duration_minutes", sa.Integer(), nullable=True), + sa.Column("filament_used_g", sa.Numeric(precision=8, scale=2), nullable=True), + sa.Column("cost_eur", sa.Numeric(precision=8, scale=2), nullable=True), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")), + sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")), + sa.ForeignKeyConstraint(["filament_id"], ["filaments.id"], name="fk_job_filament"), + sa.ForeignKeyConstraint(["printer_id"], ["printers.id"], name="fk_job_printer"), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade() -> None: + op.drop_table("print_jobs") + op.drop_table("todos") + op.drop_table("pricing_config") + op.drop_table("filaments") + op.drop_table("printers") diff --git a/app/db.py b/app/db.py index 9a2c403..fb3d2da 100644 --- a/app/db.py +++ b/app/db.py @@ -1,22 +1,33 @@ import os -import pymysql + +from sqlalchemy import create_engine, text +from sqlalchemy.orm import Session, sessionmaker -def _params() -> dict: - return { - "host": os.environ["MYSQL_HOST"], - "port": int(os.environ.get("MYSQL_PORT", 3306)), - "user": os.environ["MYSQL_USER"], - "password": os.environ["MYSQL_PASSWORD"], - "database": os.environ["MYSQL_DATABASE"], - "connect_timeout": 3, - } +def _db_url() -> str: + return ( + f"mysql+pymysql://{os.environ['MYSQL_USER']}:{os.environ['MYSQL_PASSWORD']}" + f"@{os.environ['MYSQL_HOST']}:{os.environ.get('MYSQL_PORT', 3306)}" + f"/{os.environ['MYSQL_DATABASE']}" + ) + + +engine = create_engine(_db_url(), pool_pre_ping=True) +SessionLocal = sessionmaker(bind=engine) + + +def get_db(): + db: Session = SessionLocal() + try: + yield db + finally: + db.close() def check_db() -> tuple[bool, str]: try: - conn = pymysql.connect(**_params()) - conn.close() + with engine.connect() as conn: + conn.execute(text("SELECT 1")) return True, "ok" except Exception as e: return False, str(e) diff --git a/app/main.py b/app/main.py index d24e6e2..f07b789 100644 --- a/app/main.py +++ b/app/main.py @@ -4,6 +4,7 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from app.db import check_db +from app.routers import filaments, pricing, print_jobs, printers, todos logger = logging.getLogger("pops") @@ -20,6 +21,12 @@ async def lifespan(app: FastAPI): app = FastAPI(title="POPS", lifespan=lifespan) +app.include_router(printers.router) +app.include_router(filaments.router) +app.include_router(print_jobs.router) +app.include_router(todos.router) +app.include_router(pricing.router) + @app.get("/") def root(): diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..c76a13d --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,8 @@ +from app.models.base import Base +from app.models.filament import Filament +from app.models.pricing import PricingConfig +from app.models.print_job import PrintJob +from app.models.printer import Printer +from app.models.todo import Todo + +__all__ = ["Base", "Printer", "Filament", "PrintJob", "Todo", "PricingConfig"] diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 0000000..0839bcf --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,6 @@ +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + def to_dict(self) -> dict: + return {c.name: getattr(self, c.name) for c in self.__table__.columns} diff --git a/app/models/filament.py b/app/models/filament.py new file mode 100644 index 0000000..9621c5e --- /dev/null +++ b/app/models/filament.py @@ -0,0 +1,38 @@ +import enum +from datetime import datetime +from decimal import Decimal + +from sqlalchemy import DateTime, Enum, Numeric, String, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base + + +class Material(enum.Enum): + PLA = "PLA" + PETG = "PETG" + ABS = "ABS" + ASA = "ASA" + TPU = "TPU" + Nylon = "Nylon" + PC = "PC" + Other = "Other" + + +class Filament(Base): + __tablename__ = "filaments" + + id: Mapped[int] = mapped_column(primary_key=True) + brand: Mapped[str | None] = mapped_column(String(100)) + material: Mapped[Material] = mapped_column(Enum(Material), default=Material.PLA) + color: Mapped[str | None] = mapped_column(String(100)) + color_hex: Mapped[str | None] = mapped_column(String(7)) + weight_total_g: Mapped[Decimal] = mapped_column(Numeric(8, 2)) + weight_remaining_g: Mapped[Decimal] = mapped_column(Numeric(8, 2)) + price_per_kg_eur: Mapped[Decimal | None] = mapped_column(Numeric(8, 2)) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime, server_default=func.now(), onupdate=func.now() + ) + + jobs: Mapped[list["PrintJob"]] = relationship(back_populates="filament") diff --git a/app/models/pricing.py b/app/models/pricing.py new file mode 100644 index 0000000..2897947 --- /dev/null +++ b/app/models/pricing.py @@ -0,0 +1,20 @@ +from datetime import date, datetime +from decimal import Decimal + +from sqlalchemy import Date, DateTime, Numeric, String, func +from sqlalchemy.orm import Mapped, mapped_column + +from app.models.base import Base + + +class PricingConfig(Base): + __tablename__ = "pricing_config" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(100), default="default") + electricity_kwh_eur: Mapped[Decimal] = mapped_column(Numeric(6, 4), default=Decimal("0.30")) + printer_watt: Mapped[Decimal] = mapped_column(Numeric(8, 2), default=Decimal("200.00")) + hourly_rate_eur: Mapped[Decimal] = mapped_column(Numeric(8, 2), default=Decimal("0.00")) + markup_percent: Mapped[Decimal] = mapped_column(Numeric(5, 2), default=Decimal("20.00")) + valid_from: Mapped[date] = mapped_column(Date, server_default=func.current_date()) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) diff --git a/app/models/print_job.py b/app/models/print_job.py new file mode 100644 index 0000000..a880793 --- /dev/null +++ b/app/models/print_job.py @@ -0,0 +1,41 @@ +import enum +from datetime import datetime +from decimal import Decimal + +from sqlalchemy import DateTime, Enum, ForeignKey, Integer, Numeric, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base + + +class JobStatus(enum.Enum): + queued = "queued" + printing = "printing" + done = "done" + failed = "failed" + cancelled = "cancelled" + + +class PrintJob(Base): + __tablename__ = "print_jobs" + + id: Mapped[int] = mapped_column(primary_key=True) + printer_id: Mapped[int] = mapped_column(ForeignKey("printers.id")) + filament_id: Mapped[int | None] = mapped_column(ForeignKey("filaments.id")) + name: Mapped[str] = mapped_column(String(200)) + file_name: Mapped[str | None] = mapped_column(String(255)) + status: Mapped[JobStatus] = mapped_column(Enum(JobStatus), default=JobStatus.queued) + queue_position: Mapped[int | None] = mapped_column(Integer) + started_at: Mapped[datetime | None] = mapped_column(DateTime) + finished_at: Mapped[datetime | None] = mapped_column(DateTime) + duration_minutes: Mapped[int | None] = mapped_column(Integer) + filament_used_g: Mapped[Decimal | None] = mapped_column(Numeric(8, 2)) + cost_eur: Mapped[Decimal | None] = mapped_column(Numeric(8, 2)) + notes: Mapped[str | None] = mapped_column(Text) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime, server_default=func.now(), onupdate=func.now() + ) + + printer: Mapped["Printer"] = relationship(back_populates="jobs") + filament: Mapped["Filament | None"] = relationship(back_populates="jobs") diff --git a/app/models/printer.py b/app/models/printer.py new file mode 100644 index 0000000..d0a9192 --- /dev/null +++ b/app/models/printer.py @@ -0,0 +1,33 @@ +import enum +from datetime import datetime + +from sqlalchemy import DateTime, Enum, String, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base + + +class PrinterStatus(enum.Enum): + idle = "idle" + printing = "printing" + error = "error" + offline = "offline" + + +class Printer(Base): + __tablename__ = "printers" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(100)) + model: Mapped[str | None] = mapped_column(String(100)) + location: Mapped[str | None] = mapped_column(String(100)) + status: Mapped[PrinterStatus] = mapped_column( + Enum(PrinterStatus), default=PrinterStatus.offline + ) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime, server_default=func.now(), onupdate=func.now() + ) + + jobs: Mapped[list["PrintJob"]] = relationship(back_populates="printer") + todos: Mapped[list["Todo"]] = relationship(back_populates="printer") diff --git a/app/models/todo.py b/app/models/todo.py new file mode 100644 index 0000000..0766246 --- /dev/null +++ b/app/models/todo.py @@ -0,0 +1,21 @@ +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, String, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base + + +class Todo(Base): + __tablename__ = "todos" + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(255)) + done: Mapped[bool] = mapped_column(Boolean, default=False) + printer_id: Mapped[int | None] = mapped_column(ForeignKey("printers.id")) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime, server_default=func.now(), onupdate=func.now() + ) + + printer: Mapped["Printer | None"] = relationship(back_populates="todos") diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/filaments.py b/app/routers/filaments.py new file mode 100644 index 0000000..8eee61c --- /dev/null +++ b/app/routers/filaments.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db import get_db +from app.models.filament import Filament + +router = APIRouter(prefix="/filaments", tags=["filaments"]) + + +@router.get("/") +def list_filaments(db: Session = Depends(get_db)): + return [f.to_dict() for f in db.query(Filament).all()] + + +@router.get("/{filament_id}") +def get_filament(filament_id: int, db: Session = Depends(get_db)): + filament = db.query(Filament).filter(Filament.id == filament_id).first() + if not filament: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="Filament not found") + return filament.to_dict() diff --git a/app/routers/pricing.py b/app/routers/pricing.py new file mode 100644 index 0000000..bb4fa91 --- /dev/null +++ b/app/routers/pricing.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db import get_db +from app.models.pricing import PricingConfig + +router = APIRouter(prefix="/pricing", tags=["pricing"]) + + +@router.get("/") +def list_pricing_configs(db: Session = Depends(get_db)): + return [p.to_dict() for p in db.query(PricingConfig).all()] + + +@router.get("/{config_id}") +def get_pricing_config(config_id: int, db: Session = Depends(get_db)): + config = db.query(PricingConfig).filter(PricingConfig.id == config_id).first() + if not config: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="Pricing config not found") + return config.to_dict() diff --git a/app/routers/print_jobs.py b/app/routers/print_jobs.py new file mode 100644 index 0000000..9ae3758 --- /dev/null +++ b/app/routers/print_jobs.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db import get_db +from app.models.print_job import PrintJob + +router = APIRouter(prefix="/jobs", tags=["jobs"]) + + +@router.get("/") +def list_jobs(db: Session = Depends(get_db)): + return [j.to_dict() for j in db.query(PrintJob).all()] + + +@router.get("/{job_id}") +def get_job(job_id: int, db: Session = Depends(get_db)): + job = db.query(PrintJob).filter(PrintJob.id == job_id).first() + if not job: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="Job not found") + return job.to_dict() diff --git a/app/routers/printers.py b/app/routers/printers.py new file mode 100644 index 0000000..74f12a0 --- /dev/null +++ b/app/routers/printers.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db import get_db +from app.models.printer import Printer + +router = APIRouter(prefix="/printers", tags=["printers"]) + + +@router.get("/") +def list_printers(db: Session = Depends(get_db)): + return [p.to_dict() for p in db.query(Printer).all()] + + +@router.get("/{printer_id}") +def get_printer(printer_id: int, db: Session = Depends(get_db)): + printer = db.query(Printer).filter(Printer.id == printer_id).first() + if not printer: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="Printer not found") + return printer.to_dict() diff --git a/app/routers/todos.py b/app/routers/todos.py new file mode 100644 index 0000000..dcff07c --- /dev/null +++ b/app/routers/todos.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db import get_db +from app.models.todo import Todo + +router = APIRouter(prefix="/todos", tags=["todos"]) + + +@router.get("/") +def list_todos(db: Session = Depends(get_db)): + return [t.to_dict() for t in db.query(Todo).all()] + + +@router.get("/{todo_id}") +def get_todo(todo_id: int, db: Session = Depends(get_db)): + todo = db.query(Todo).filter(Todo.id == todo_id).first() + if not todo: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="Todo not found") + return todo.to_dict() diff --git a/requirements.txt b/requirements.txt index 440e212..310ac42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ fastapi==0.115.6 uvicorn[standard]==0.34.0 +sqlalchemy==2.0.36 +alembic==1.14.0 pymysql==1.1.1 cryptography==44.0.3