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
WORKDIR /app
COPY requirements.txt .
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
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 typing import Iterable, List, Optional, Tuple
from typing import List, Optional, Tuple
from uuid import UUID as UUID_t
from datetime import date
import re, ast, operator
import re, ast
from sqlalchemy.orm import Session, joinedload
import models
from backend import models
ALLOWED_NAMES = {"days", "nights"}
ALLOWED_NODES = (
@ -74,7 +75,7 @@ def generate_trip_items(
trip: models.Trip,
selected_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.
Gibt (created_ids, deleted_checked_ids) zurück."""
# Sammle bestehende checked Items, falls sie verschwinden
@ -94,12 +95,8 @@ def generate_trip_items(
for it in items:
item_tag_ids = {link.tag_id for link in it.tags}
# bestimmen, ob dupliziert werden soll
intersection = item_tag_ids & marked_set
if marked_set and intersection:
per_tags = sorted(list(intersection))
else:
per_tags = [None] # gemeinsames Item (oder keine marked match)
per_tags = sorted(list(intersection)) if (marked_set and intersection) else [None]
for tag_id in per_tags:
calc = render_name(it.name, trip.start_date, trip.end_date)

View file

@ -1,3 +1,4 @@
from sqlalchemy import create_engine
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)
Base = declarative_base()
# FastAPI dependency
from contextlib import contextmanager
def get_db():
db = SessionLocal()
try:

View file

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

View file

@ -1,15 +1,12 @@
import uuid
from sqlalchemy import (
Column, String, Boolean, Date, ForeignKey, UniqueConstraint
)
from sqlalchemy import Column, String, Boolean, Date, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from database import Base
from backend.database import Base
class User(Base):
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False)
@ -17,10 +14,8 @@ class User(Base):
tags = relationship("Tag", back_populates="user", cascade="all, delete")
trips = relationship("Trip", back_populates="user", cascade="all, delete")
class Item(Base):
__tablename__ = "items"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
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"
@ -29,10 +24,9 @@ class Item(Base):
tags = relationship("ItemTag", back_populates="item", cascade="all, delete")
trip_items = relationship("TripItem", back_populates="item", cascade="all, delete")
class Tag(Base):
__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)
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_items = relationship("TripItem", back_populates="tag")
class ItemTag(Base):
__tablename__ = "item_tags"
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)
item = relationship("Item", back_populates="tags")
tag = relationship("Tag", back_populates="items")
class Trip(Base):
__tablename__ = "trips"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
name = Column(String)
@ -69,36 +59,30 @@ class Trip(Base):
marked_tags = relationship("TripTagMarked", back_populates="trip", cascade="all, delete")
trip_items = relationship("TripItem", back_populates="trip", cascade="all, delete")
class TripTagSelected(Base):
__tablename__ = "trip_tag_selected"
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)
trip = relationship("Trip", back_populates="selected_tags")
tag = relationship("Tag", back_populates="trip_selected_tags")
class TripTagMarked(Base):
__tablename__ = "trip_tag_marked"
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)
trip = relationship("Trip", back_populates="marked_tags")
tag = relationship("Tag", back_populates="trip_marked_tags")
class TripItem(Base):
__tablename__ = "trip_items"
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)
item_id = Column(UUID(as_uuid=True), ForeignKey("items.id"), nullable=False)
name_calculated = Column(String, nullable=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")
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 sqlalchemy.orm import Session
import uuid
from database import get_db
import models
FIXED_USER_ID = uuid.UUID("00000000-0000-0000-0000-000000000001") # Fixed UUID for demo user
from uuid import uuid4
from datetime import date, timedelta
from backend.database import get_db
from backend import models
from backend.crud import generate_trip_items
router = APIRouter(tags=["dev"])
@router.get("/dev/seed")
def seed_data(db: Session = Depends(get_db)):
user_id = FIXED_USER_ID
# Create demo user if not exists
user = db.query(models.User).filter(models.User.id == user_id).first()
def dev_seed(db: Session = Depends(get_db)):
# Create demo user
user = db.query(models.User).first()
if not user:
user = models.User(id=user_id, name="Demo User")
user = models.User(id=uuid4(), name="Demo")
db.add(user)
db.flush()
# Tags
tags = ["jari", "kristin", "felix", "auto", "sommer"]
tag_objs = []
for t in tags:
tag_obj = models.Tag(id=uuid.uuid4(), user_id=user_id, name=t)
db.add(tag_obj)
tag_objs.append(tag_obj)
tag_names = ["jari", "kristin", "felix", "auto", "sommer"]
name_to_tag = {}
for name in tag_names:
existing = db.query(models.Tag).filter(models.Tag.user_id==user.id, models.Tag.name==name).first()
if existing:
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()
# Items
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"]]
# Demo trip
trip = models.Trip(
id=uuid.uuid4(),
user_id=user_id,
id=uuid4(),
user_id=user.id,
name="Ostsee August 2025",
start_date="2025-08-01",
end_date="2025-08-14",
selected_tags=selected_tags,
marked_tags=marked_tags,
start_date=date(2025, 8, 18),
end_date=date(2025, 8, 20),
)
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()
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 sqlalchemy.orm import Session, joinedload
from uuid import UUID
from database import get_db
import models
from schemas import ItemCreate, ItemOut
from backend.database import get_db
from backend import models
from backend.schemas import ItemCreate, ItemOut
router = APIRouter(prefix="/items", tags=["items"])

View file

@ -1,14 +1,13 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from uuid import UUID
from database import get_db
import models
from schemas import TagCreate, TagOut
from backend.database import get_db
from backend import models
from backend.schemas import TagCreate, TagOut
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])
def list_tags(db: Session = Depends(get_db)):
tags = db.query(models.Tag).all()
@ -16,7 +15,6 @@ def list_tags(db: Session = Depends(get_db)):
@router.post("/", response_model=TagOut)
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()
if not user:
from uuid import uuid4

View file

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

View file

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

View file

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

View file

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