Refactor backend structure: update Dockerfile, improve database connection, and enhance dev_seed functionality

This commit is contained in:
Felix Zett 2025-08-14 20:40:12 +02:00
parent cb213afdf4
commit e9bc26e1ed
13 changed files with 150 additions and 128 deletions

View file

@ -1,11 +1,13 @@
FROM python:3.11-slim FROM python:3.11-slim
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] EXPOSE 8000
CMD [ "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload" ]

View file

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Iterable, List, Optional, Tuple from typing import List, Optional, Tuple
from uuid import UUID as UUID_t from uuid import UUID as UUID_t
from datetime import date from datetime import date
import re, ast, operator import re, ast
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
import models from backend import models
ALLOWED_NAMES = {"days", "nights"} ALLOWED_NAMES = {"days", "nights"}
ALLOWED_NODES = ( ALLOWED_NODES = (
@ -74,7 +75,7 @@ def generate_trip_items(
trip: models.Trip, trip: models.Trip,
selected_tag_ids: List[UUID_t], selected_tag_ids: List[UUID_t],
marked_tag_ids: List[UUID_t], marked_tag_ids: List[UUID_t],
) -> Tuple[List[models.TripItem], List[UUID_t]]: ) -> Tuple[List[UUID_t], List[UUID_t]]:
"""Regeneriert TripItems für einen Trip. Löscht alte, legt neue an. """Regeneriert TripItems für einen Trip. Löscht alte, legt neue an.
Gibt (created_ids, deleted_checked_ids) zurück.""" Gibt (created_ids, deleted_checked_ids) zurück."""
# Sammle bestehende checked Items, falls sie verschwinden # Sammle bestehende checked Items, falls sie verschwinden
@ -94,12 +95,8 @@ def generate_trip_items(
for it in items: for it in items:
item_tag_ids = {link.tag_id for link in it.tags} item_tag_ids = {link.tag_id for link in it.tags}
# bestimmen, ob dupliziert werden soll
intersection = item_tag_ids & marked_set intersection = item_tag_ids & marked_set
if marked_set and intersection: per_tags = sorted(list(intersection)) if (marked_set and intersection) else [None]
per_tags = sorted(list(intersection))
else:
per_tags = [None] # gemeinsames Item (oder keine marked match)
for tag_id in per_tags: for tag_id in per_tags:
calc = render_name(it.name, trip.start_date, trip.end_date) calc = render_name(it.name, trip.start_date, trip.end_date)

View file

@ -1,3 +1,4 @@
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base from sqlalchemy.orm import sessionmaker, declarative_base
@ -7,9 +8,6 @@ engine = create_engine(DATABASE_URL, future=True)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True) SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
Base = declarative_base() Base = declarative_base()
# FastAPI dependency
from contextlib import contextmanager
def get_db(): def get_db():
db = SessionLocal() db = SessionLocal()
try: try:

View file

@ -1,9 +1,8 @@
from uuid import UUID
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from database import Base, engine from backend.database import Base, engine
from routes import items, tags, trips, trip_items, dev_seed from backend.routes import items, tags, trips, trip_items, dev_seed
# Create tables (for MVP without Alembic) # Create tables (for MVP without Alembic)
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)

View file

@ -1,15 +1,12 @@
import uuid import uuid
from sqlalchemy import ( from sqlalchemy import Column, String, Boolean, Date, ForeignKey, UniqueConstraint
Column, String, Boolean, Date, ForeignKey, UniqueConstraint
)
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from database import Base from backend.database import Base
class User(Base): class User(Base):
__tablename__ = "users" __tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False) name = Column(String, nullable=False)
@ -17,10 +14,8 @@ class User(Base):
tags = relationship("Tag", back_populates="user", cascade="all, delete") tags = relationship("Tag", back_populates="user", cascade="all, delete")
trips = relationship("Trip", back_populates="user", cascade="all, delete") trips = relationship("Trip", back_populates="user", cascade="all, delete")
class Item(Base): class Item(Base):
__tablename__ = "items" __tablename__ = "items"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
name = Column(String, nullable=False) # z.B. "Zahnbürste" oder "{days} x Vitamin D3" name = Column(String, nullable=False) # z.B. "Zahnbürste" oder "{days} x Vitamin D3"
@ -29,10 +24,9 @@ class Item(Base):
tags = relationship("ItemTag", back_populates="item", cascade="all, delete") tags = relationship("ItemTag", back_populates="item", cascade="all, delete")
trip_items = relationship("TripItem", back_populates="item", cascade="all, delete") trip_items = relationship("TripItem", back_populates="item", cascade="all, delete")
class Tag(Base): class Tag(Base):
__tablename__ = "tags" __tablename__ = "tags"
__table_args__ = (UniqueConstraint("user_id", "name"),) __table_args__ = (UniqueConstraint("user_id", "name", name="uq_tag_user_name"),)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
@ -44,20 +38,16 @@ class Tag(Base):
trip_marked_tags = relationship("TripTagMarked", back_populates="tag", cascade="all, delete") trip_marked_tags = relationship("TripTagMarked", back_populates="tag", cascade="all, delete")
trip_items = relationship("TripItem", back_populates="tag") trip_items = relationship("TripItem", back_populates="tag")
class ItemTag(Base): class ItemTag(Base):
__tablename__ = "item_tags" __tablename__ = "item_tags"
item_id = Column(UUID(as_uuid=True), ForeignKey("items.id", ondelete="CASCADE"), primary_key=True) item_id = Column(UUID(as_uuid=True), ForeignKey("items.id", ondelete="CASCADE"), primary_key=True)
tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True) tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True)
item = relationship("Item", back_populates="tags") item = relationship("Item", back_populates="tags")
tag = relationship("Tag", back_populates="items") tag = relationship("Tag", back_populates="items")
class Trip(Base): class Trip(Base):
__tablename__ = "trips" __tablename__ = "trips"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
name = Column(String) name = Column(String)
@ -69,36 +59,30 @@ class Trip(Base):
marked_tags = relationship("TripTagMarked", back_populates="trip", cascade="all, delete") marked_tags = relationship("TripTagMarked", back_populates="trip", cascade="all, delete")
trip_items = relationship("TripItem", back_populates="trip", cascade="all, delete") trip_items = relationship("TripItem", back_populates="trip", cascade="all, delete")
class TripTagSelected(Base): class TripTagSelected(Base):
__tablename__ = "trip_tag_selected" __tablename__ = "trip_tag_selected"
trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id", ondelete="CASCADE"), primary_key=True) trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id", ondelete="CASCADE"), primary_key=True)
tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id"), primary_key=True) tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id"), primary_key=True)
trip = relationship("Trip", back_populates="selected_tags") trip = relationship("Trip", back_populates="selected_tags")
tag = relationship("Tag", back_populates="trip_selected_tags") tag = relationship("Tag", back_populates="trip_selected_tags")
class TripTagMarked(Base): class TripTagMarked(Base):
__tablename__ = "trip_tag_marked" __tablename__ = "trip_tag_marked"
trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id", ondelete="CASCADE"), primary_key=True) trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id", ondelete="CASCADE"), primary_key=True)
tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id"), primary_key=True) tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id"), primary_key=True)
trip = relationship("Trip", back_populates="marked_tags") trip = relationship("Trip", back_populates="marked_tags")
tag = relationship("Tag", back_populates="trip_marked_tags") tag = relationship("Tag", back_populates="trip_marked_tags")
class TripItem(Base): class TripItem(Base):
__tablename__ = "trip_items" __tablename__ = "trip_items"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id", ondelete="CASCADE"), nullable=False) trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id", ondelete="CASCADE"), nullable=False)
item_id = Column(UUID(as_uuid=True), ForeignKey("items.id"), nullable=False) item_id = Column(UUID(as_uuid=True), ForeignKey("items.id"), nullable=False)
name_calculated = Column(String, nullable=False) name_calculated = Column(String, nullable=False)
checked = Column(Boolean, nullable=False, default=False) checked = Column(Boolean, nullable=False, default=False)
tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id"), nullable=True) tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id"), nullable=True) # null = gemeinsames Item
trip = relationship("Trip", back_populates="trip_items") trip = relationship("Trip", back_populates="trip_items")
item = relationship("Item", back_populates="trip_items") item = relationship("Item", back_populates="trip_items")

View file

@ -1,8 +1,6 @@
fastapi
uvicorn[standard]
sqlalchemy
asyncpg
psycopg2-binary
pydantic
python-dotenv
fastapi==0.115.0
uvicorn==0.30.6
SQLAlchemy==2.0.34
psycopg2-binary==2.9.9
pydantic==1.10.17

View file

@ -1,63 +1,103 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import uuid from uuid import uuid4
from database import get_db from datetime import date, timedelta
import models from backend.database import get_db
from backend import models
FIXED_USER_ID = uuid.UUID("00000000-0000-0000-0000-000000000001") # Fixed UUID for demo user from backend.crud import generate_trip_items
router = APIRouter(tags=["dev"]) router = APIRouter(tags=["dev"])
@router.get("/dev/seed") @router.get("/dev/seed")
def seed_data(db: Session = Depends(get_db)):
user_id = FIXED_USER_ID
# Create demo user if not exists def dev_seed(db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.id == user_id).first()
# Create demo user
user = db.query(models.User).first()
if not user: if not user:
user = models.User(id=user_id, name="Demo User") user = models.User(id=uuid4(), name="Demo")
db.add(user) db.add(user)
db.flush() db.flush()
# Tags # Tags
tags = ["jari", "kristin", "felix", "auto", "sommer"] tag_names = ["jari", "kristin", "felix", "auto", "sommer"]
tag_objs = [] name_to_tag = {}
for t in tags: for name in tag_names:
tag_obj = models.Tag(id=uuid.uuid4(), user_id=user_id, name=t) existing = db.query(models.Tag).filter(models.Tag.user_id==user.id, models.Tag.name==name).first()
db.add(tag_obj) if existing:
tag_objs.append(tag_obj) name_to_tag[name] = existing
else:
t = models.Tag(id=uuid4(), user_id=user.id, name=name)
db.add(t)
db.flush()
name_to_tag[name] = t
# Items (based on your original example)
items = [
("Kinderwagen", ["jari"]),
("Babyschale mit Sonnenschutz", ["jari", "auto"]),
("Alle Schnuller", ["jari"]),
("Sonnencreme", ["sommer"]),
("Ladekabel Handy", []),
("Ladekabel Mac", ["kristin"]),
("Sonnenbrillen", ["sommer"]),
("Schlafsack für {nights} Nächte", ["jari"]),
("{days} x Vitamin D3", ["jari"]),
("{days * 10} Windeln", ["jari"]),
("{days} Unterhosen", ["felix", "kristin"]),
("Badesachen", ["felix", "kristin"]),
]
for name, tags in items:
existing = db.query(models.Item).filter(models.Item.user_id==user.id, models.Item.name==name).first()
if existing:
item = existing
else:
item = models.Item(id=uuid4(), user_id=user.id, name=name)
db.add(item)
db.flush()
# link tags
for tag_name in tags:
tag = name_to_tag[tag_name]
link = db.query(models.ItemTag).filter_by(item_id=item.id, tag_id=tag.id).first()
if not link:
db.add(models.ItemTag(item_id=item.id, tag_id=tag.id))
db.flush() db.flush()
# Items # Demo trip
items_data = [
("Badesachen", ["sommer", "auto"]),
("Sonnencreme", ["sommer"]),
("Ladegerät", []),
("Zelt", ["auto"]),
("Schlafsack", []),
("Strandspielzeug", ["sommer", "jari"]),
]
for name, tag_names in items_data:
item = models.Item(id=uuid.uuid4(), user_id=user_id, name=name)
db.add(item)
db.flush()
for tag_name in tag_names:
tag_id = next(t.id for t in tag_objs if t.name == tag_name)
db.add(models.ItemTag(item_id=item.id, tag_id=tag_id))
selected_tags = [t for t in tag_objs if t.name in ["jari", "kristin", "auto", "sommer"]]
marked_tags = [t for t in tag_objs if t.name in ["kristin", "felix"]]
trip = models.Trip( trip = models.Trip(
id=uuid.uuid4(), id=uuid4(),
user_id=user_id, user_id=user.id,
name="Ostsee August 2025", name="Ostsee August 2025",
start_date="2025-08-01", start_date=date(2025, 8, 18),
end_date="2025-08-14", end_date=date(2025, 8, 20),
selected_tags=selected_tags,
marked_tags=marked_tags,
) )
db.add(trip) db.add(trip)
db.flush()
selected = [name_to_tag[n].id for n in ["jari", "felix", "kristin", "auto"]]
marked = [name_to_tag[n].id for n in ["kristin", "felix"]]
for tid in selected:
db.add(models.TripTagSelected(trip_id=trip.id, tag_id=tid))
for tid in marked:
db.add(models.TripTagMarked(trip_id=trip.id, tag_id=tid))
db.flush()
created_ids, _ = generate_trip_items(db, trip=trip, selected_tag_ids=selected, marked_tag_ids=marked)
db.commit() db.commit()
return {"message": "Seed data created", "user_id": str(user_id)}
return {
"user_id": str(user.id),
"trip_id": str(trip.id),
"selected_tag_ids": [str(x) for x in selected],
"marked_tag_ids": [str(x) for x in marked],
"created_trip_item_ids": [str(x) for x in created_ids],
}

View file

@ -1,9 +1,10 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from uuid import UUID from uuid import UUID
from database import get_db from backend.database import get_db
import models from backend import models
from schemas import ItemCreate, ItemOut from backend.schemas import ItemCreate, ItemOut
router = APIRouter(prefix="/items", tags=["items"]) router = APIRouter(prefix="/items", tags=["items"])

View file

@ -1,14 +1,13 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from uuid import UUID from uuid import UUID
from database import get_db from backend.database import get_db
import models from backend import models
from schemas import TagCreate, TagOut from backend.schemas import TagCreate, TagOut
router = APIRouter(prefix="/tags", tags=["tags"]) router = APIRouter(prefix="/tags", tags=["tags"])
FIXED_USER_ID = None # will be created on seed; or set in your auth layer
@router.get("/", response_model=list[TagOut]) @router.get("/", response_model=list[TagOut])
def list_tags(db: Session = Depends(get_db)): def list_tags(db: Session = Depends(get_db)):
tags = db.query(models.Tag).all() tags = db.query(models.Tag).all()
@ -16,7 +15,6 @@ def list_tags(db: Session = Depends(get_db)):
@router.post("/", response_model=TagOut) @router.post("/", response_model=TagOut)
def create_tag(payload: TagCreate, db: Session = Depends(get_db)): def create_tag(payload: TagCreate, db: Session = Depends(get_db)):
# For MVP: attach to first user (or create one)
user = db.query(models.User).first() user = db.query(models.User).first()
if not user: if not user:
from uuid import uuid4 from uuid import uuid4

View file

@ -1,9 +1,10 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from uuid import UUID from uuid import UUID
from database import get_db from backend.database import get_db
import models from backend import models
from schemas import TripItemOut from backend.schemas import TripItemOut
router = APIRouter(prefix="/trip-items", tags=["trip-items"]) router = APIRouter(prefix="/trip-items", tags=["trip-items"])

View file

@ -1,10 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from uuid import UUID from uuid import UUID
from database import get_db from backend.database import get_db
import models from backend import models
from schemas import TripCreate, TripOut, TripUpdate, TripRegenerationResult from backend.schemas import TripCreate, TripOut, TripUpdate, TripRegenerationResult
from crud import generate_trip_items from backend.crud import generate_trip_items
router = APIRouter(prefix="/trips", tags=["trips"]) router = APIRouter(prefix="/trips", tags=["trips"])

View file

@ -1,3 +1,4 @@
from typing import List, Optional from typing import List, Optional
from uuid import UUID from uuid import UUID
from datetime import date from datetime import date

View file

@ -1,25 +1,27 @@
version: "3.9"
version: "3.9"
services: services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data
backend: backend:
build: ./backend build:
context: ./backend
dockerfile: Dockerfile
depends_on:
- db
ports: ports:
- "8000:8000" - "8000:8000"
volumes: volumes:
- ./backend:/app - ./:/app
environment: environment:
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/packlist - PYTHONUNBUFFERED=1
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: packlist
volumes:
- db_data:/var/lib/postgresql/data
volumes: volumes:
db_data: db_data: