Refactor backend structure: update Dockerfile, improve database connection, and enhance dev_seed functionality
This commit is contained in:
parent
cb213afdf4
commit
e9bc26e1ed
13 changed files with 150 additions and 128 deletions
|
|
@ -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" ]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}
|
||||
|
|
@ -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"])
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from datetime import date
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue