Komplett neu mit GPT

This commit is contained in:
Felix Zett 2025-08-13 21:10:04 +02:00
parent 269bfaab2c
commit e702418221
12 changed files with 498 additions and 165 deletions

View file

@ -7,5 +7,5 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

118
backend/crud.py Normal file
View file

@ -0,0 +1,118 @@
from __future__ import annotations
from typing import Iterable, List, Optional, Tuple
from uuid import UUID as UUID_t
from datetime import date
import re, ast, operator
from sqlalchemy.orm import Session, joinedload
from backend import models
ALLOWED_NAMES = {"days", "nights"}
ALLOWED_NODES = (
ast.Expression, ast.BinOp, ast.UnaryOp, ast.Num, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv,
ast.Mod, ast.Pow, ast.USub, ast.Load, ast.Name, ast.Constant, ast.Call
)
# Very small safe-eval for placeholders like {days * 10}
def _safe_eval(expr: str, ctx: dict) -> str:
node = ast.parse(expr, mode="eval")
for n in ast.walk(node):
if not isinstance(n, ALLOWED_NODES):
raise ValueError("disallowed expression")
if isinstance(n, ast.Name) and n.id not in ALLOWED_NAMES:
raise ValueError("unknown name")
if isinstance(n, ast.Call):
raise ValueError("calls not allowed")
val = eval(compile(node, "<expr>", "eval"), {"__builtins__": {}}, ctx)
return str(int(val)) if isinstance(val, (int, float)) and float(val).is_integer() else str(val)
_placeholder_re = re.compile(r"\{([^{}]+)\}")
def render_name(name_template: str, start: Optional[date], end: Optional[date]) -> str:
if not name_template:
return ""
days = nights = 0
if start and end:
days = (end - start).days + 1
nights = max(days - 1, 0)
ctx = {"days": days, "nights": nights}
def repl(m):
expr = m.group(1).strip()
try:
return _safe_eval(expr, ctx)
except Exception:
# if not evaluable, leave placeholder as-is
return m.group(0)
return _placeholder_re.sub(repl, name_template)
def items_for_trip(db: Session, user_id: UUID_t, selected_tag_ids: List[UUID_t]) -> List[models.Item]:
# Items ohne Tags (immer) + Items, die irgendeinen der selected_tags besitzen
q = (
db.query(models.Item)
.options(joinedload(models.Item.tags).joinedload(models.ItemTag.tag))
.filter(models.Item.user_id == user_id)
)
items = q.all()
selected_set = set(selected_tag_ids)
result: List[models.Item] = []
for it in items:
item_tag_ids = {link.tag_id for link in it.tags}
if not item_tag_ids:
result.append(it)
elif selected_set & item_tag_ids:
result.append(it)
return result
def generate_trip_items(
db: Session,
*,
trip: models.Trip,
selected_tag_ids: List[UUID_t],
marked_tag_ids: List[UUID_t],
) -> Tuple[List[models.TripItem], 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
deleted_checked: List[UUID_t] = []
# Lösche alle existierenden TripItems und merke checked, die wegfallen
for ti in list(trip.trip_items):
if ti.checked:
deleted_checked.append(ti.id)
db.delete(ti)
db.flush()
items = items_for_trip(db, trip.user_id, selected_tag_ids)
created_ids: List[UUID_t] = []
marked_set = set(marked_tag_ids)
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)
for tag_id in per_tags:
calc = render_name(it.name, trip.start_date, trip.end_date)
ti = models.TripItem(
trip_id=trip.id,
item_id=it.id,
name_calculated=calc,
checked=False,
tag_id=tag_id,
)
db.add(ti)
db.flush()
created_ids.append(ti.id)
db.flush()
return created_ids, deleted_checked

View file

@ -1,14 +1,18 @@
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@db:5432/packlist"
DATABASE_URL = "postgresql+psycopg2://postgres:postgres@db:5432/postgres"
engine = create_async_engine(DATABASE_URL)
SessionLocal = sessionmaker(
bind=engine, class_=AsyncSession, expire_on_commit=False
)
engine = create_engine(DATABASE_URL, future=True)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
Base = declarative_base()
async def get_db():
async with SessionLocal() as db:
# FastAPI dependency
from contextlib import contextmanager
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

View file

@ -1,17 +1,22 @@
from fastapi import FastAPI
from routes import items, trips
from models import Base
from database import engine
from fastapi.middleware.cors import CORSMiddleware
from backend.database import Base, engine
from backend.routes import items, tags, trips, trip_items
app = FastAPI()
# Create tables (for MVP without Alembic)
Base.metadata.create_all(bind=engine)
app = FastAPI(title="Packlist API (MVP)")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(tags.router)
app.include_router(items.router)
app.include_router(trips.router)
@app.on_event("startup")
async def startup():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
@app.get("/")
def read_root():
return {"status": "running"}
app.include_router(trip_items.router)

View file

@ -1,51 +1,105 @@
from sqlalchemy import Column, String, Boolean, Date, ForeignKey, Table, Text
from sqlalchemy.dialects.postgresql import UUID, ARRAY
from sqlalchemy.orm import relationship, declarative_base
import uuid
Base = declarative_base()
item_tags = Table(
"item_tags",
Base.metadata,
Column("item_id", UUID(as_uuid=True), ForeignKey("items.id")),
Column("tag_id", UUID(as_uuid=True), ForeignKey("tags.id")),
from sqlalchemy import (
Column, String, Boolean, Date, ForeignKey, UniqueConstraint
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from .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)
items = relationship("Item", back_populates="user", cascade="all, delete")
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), nullable=False)
name = Column(Text, nullable=False)
tags = relationship("Tag", secondary=item_tags, back_populates="items")
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"
user = relationship("User", back_populates="items")
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"),)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False)
name = Column(String, nullable=False, unique=False)
items = relationship("Item", secondary=item_tags, back_populates="tags")
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
name = Column(String, nullable=False)
user = relationship("User", back_populates="tags")
items = relationship("ItemTag", back_populates="tag", cascade="all, delete")
trip_selected_tags = relationship("TripTagSelected", back_populates="tag", cascade="all, delete")
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), nullable=False)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
name = Column(String)
start_date = Column(Date)
end_date = Column(Date)
selected_tags = Column(ARRAY(String))
marked_tags = Column(ARRAY(String))
user = relationship("User", back_populates="trips")
selected_tags = relationship("TripTagSelected", 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")
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"))
item_id = Column(UUID(as_uuid=True), ForeignKey("items.id"), nullable=True)
tag = Column(String, nullable=True) # e.g. "#kristin"
name = Column(Text)
calculated_label = Column(Text)
checked = Column(Boolean, default=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)
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)
trip = relationship("Trip", back_populates="trip_items")
item = relationship("Item", back_populates="trip_items")
tag = relationship("Tag", back_populates="trip_items")

View file

View file

@ -1,87 +1,123 @@
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from schemas import ItemCreate
from models import Item, Tag
from database import get_db
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from config import FIXED_USER_ID
from sqlalchemy.orm import Session, joinedload
from uuid import UUID
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="/items", tags=["Items"])
router = APIRouter(prefix="/trips", tags=["trips"])
@router.post("/")
async def create_item(item: ItemCreate, db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID
db_item = Item(id=uuid4(), name=item.name, user_id=user_id)
tags = []
for tag_name in item.tag_names:
result = await db.execute(select(Tag).where(Tag.name == tag_name, Tag.user_id == user_id))
tag = result.scalar_one_or_none()
if not tag:
tag = Tag(id=uuid4(), name=tag_name, user_id=user_id)
db.add(tag)
tags.append(tag)
db_item.tags = tags
db.add(db_item)
await db.commit()
return {"status": "item created", "item_id": str(db_item.id)}
@router.get("/")
async def read_items(db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID
result = await db.execute(select(Item).where(Item.user_id == user_id).options(selectinload(Item.tags)))
items = result.scalars().all()
return {
"items": [
{
"id": str(item.id),
"name": item.name,
"tags": [tag.name for tag in item.tags]
}
for item in items
]
}
@router.put("/{item_id}")
async def update_item(item_id: str, item: ItemCreate, db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID
result = await db.execute(select(Item).where(Item.id == item_id, Item.user_id == user_id).options(selectinload(Item.tags)))
db_item = result.scalar_one_or_none()
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
db_item.name = item.name
# Update tags
tags = []
for tag_name in item.tag_names:
result = await db.execute(select(Tag).where(Tag.name == tag_name, Tag.user_id == user_id))
tag = result.scalar_one_or_none()
if not tag:
tag = Tag(id=uuid4(), name=tag_name, user_id=user_id)
db.add(tag)
tags.append(tag)
db_item.tags = tags
await db.commit()
return {"status": "item updated", "item_id": str(db_item.id)}
@router.get("/{item_id}")
async def get_item(item_id: str, db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID
result = await db.execute(
select(Item).where(Item.id == item_id, Item.user_id == user_id).options(selectinload(Item.tags))
@router.get("/", response_model=list[TripOut])
def list_trips(db: Session = Depends(get_db)):
trips = (
db.query(models.Trip)
.options(
joinedload(models.Trip.selected_tags).joinedload(models.TripTagSelected.tag),
joinedload(models.Trip.marked_tags).joinedload(models.TripTagMarked.tag),
)
db_item = result.scalar_one_or_none()
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
.all()
)
return [
TripOut(
id=t.id,
name=t.name,
start_date=t.start_date,
end_date=t.end_date,
selected_tags=[st.tag for st in t.selected_tags],
marked_tags=[mt.tag for mt in t.marked_tags],
)
for t in trips
]
@router.post("/", response_model=TripOut)
def create_trip(payload: TripCreate, db: Session = Depends(get_db)):
user = db.query(models.User).first()
if not user:
from uuid import uuid4
user = models.User(id=uuid4(), name="Demo")
db.add(user)
db.flush()
trip = models.Trip(user_id=user.id, name=payload.name, start_date=payload.start_date, end_date=payload.end_date)
db.add(trip)
db.flush()
# attach selected & marked
if payload.selected_tag_ids:
for tid in payload.selected_tag_ids:
db.add(models.TripTagSelected(trip_id=trip.id, tag_id=tid))
if payload.marked_tag_ids:
for tid in payload.marked_tag_ids:
db.add(models.TripTagMarked(trip_id=trip.id, tag_id=tid))
db.flush()
# generate items per rules
created_ids, _ = generate_trip_items(
db,
trip=trip,
selected_tag_ids=payload.selected_tag_ids,
marked_tag_ids=payload.marked_tag_ids,
)
db.commit()
# reload with relationships
trip = (
db.query(models.Trip)
.options(
joinedload(models.Trip.selected_tags).joinedload(models.TripTagSelected.tag),
joinedload(models.Trip.marked_tags).joinedload(models.TripTagMarked.tag),
)
.get(trip.id)
)
return TripOut(
id=trip.id,
name=trip.name,
start_date=trip.start_date,
end_date=trip.end_date,
selected_tags=[st.tag for st in trip.selected_tags],
marked_tags=[mt.tag for mt in trip.marked_tags],
)
@router.put("/{trip_id}/reconfigure", response_model=TripRegenerationResult)
def reconfigure_trip(trip_id: UUID, payload: TripUpdate, db: Session = Depends(get_db)):
trip = db.get(models.Trip, trip_id)
if not trip:
raise HTTPException(status_code=404, detail="Trip not found")
# update base fields
if payload.name is not None:
trip.name = payload.name
if payload.start_date is not None:
trip.start_date = payload.start_date
if payload.end_date is not None:
trip.end_date = payload.end_date
db.flush()
# update selected/marked join tables if provided
if payload.selected_tag_ids is not None:
# replace all
db.query(models.TripTagSelected).filter_by(trip_id=trip.id).delete()
for tid in payload.selected_tag_ids:
db.add(models.TripTagSelected(trip_id=trip.id, tag_id=tid))
if payload.marked_tag_ids is not None:
db.query(models.TripTagMarked).filter_by(trip_id=trip.id).delete()
for tid in payload.marked_tag_ids:
db.add(models.TripTagMarked(trip_id=trip.id, tag_id=tid))
db.flush()
# read back lists
sel_ids = [row.tag_id for row in db.query(models.TripTagSelected).filter_by(trip_id=trip.id).all()]
mrk_ids = [row.tag_id for row in db.query(models.TripTagMarked).filter_by(trip_id=trip.id).all()]
created_ids, deleted_checked = generate_trip_items(
db, trip=trip, selected_tag_ids=sel_ids, marked_tag_ids=mrk_ids
)
db.commit()
return {
"id": str(db_item.id),
"name": db_item.name,
"tags": [tag.name for tag in db_item.tags]
"trip_id": trip.id,
"deleted_checked_trip_item_ids": deleted_checked,
"created_trip_item_ids": created_ids,
}

33
backend/routes/tags.py Normal file
View file

@ -0,0 +1,33 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from uuid import UUID
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()
return tags
@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
user = models.User(id=uuid4(), name="Demo")
db.add(user)
db.flush()
existing = db.query(models.Tag).filter(models.Tag.user_id == user.id, models.Tag.name == payload.name).first()
if existing:
raise HTTPException(status_code=400, detail="Tag already exists")
tag = models.Tag(user_id=user.id, name=payload.name)
db.add(tag)
db.commit()
db.refresh(tag)
return tag

View file

@ -0,0 +1,51 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload
from uuid import UUID
from backend.database import get_db
from backend import models
from backend.schemas import TripItemOut
router = APIRouter(prefix="/trip-items", tags=["trip-items"])
@router.get("/by-trip/{trip_id}", response_model=list[TripItemOut])
def list_trip_items(trip_id: UUID, db: Session = Depends(get_db)):
trip = db.get(models.Trip, trip_id)
if not trip:
raise HTTPException(status_code=404, detail="Trip not found")
items = (
db.query(models.TripItem)
.options(joinedload(models.TripItem.tag))
.filter(models.TripItem.trip_id == trip_id)
.all()
)
return [
TripItemOut(
id=ti.id,
trip_id=ti.trip_id,
item_id=ti.item_id,
name_calculated=ti.name_calculated,
checked=ti.checked,
tag=ti.tag,
) for ti in items
]
@router.post("/{trip_item_id}/toggle", response_model=TripItemOut)
def toggle_trip_item(trip_item_id: UUID, db: Session = Depends(get_db)):
ti = (
db.query(models.TripItem)
.options(joinedload(models.TripItem.tag))
.get(trip_item_id)
)
if not ti:
raise HTTPException(status_code=404, detail="TripItem not found")
ti.checked = not ti.checked
db.commit()
db.refresh(ti)
return TripItemOut(
id=ti.id,
trip_id=ti.trip_id,
item_id=ti.item_id,
name_calculated=ti.name_calculated,
checked=ti.checked,
tag=ti.tag,
)

View file

@ -2,7 +2,7 @@ import re
from uuid import UUID, uuid4
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from schemas import TripCreate
from schemas import TripCreate, TripItemOut, TripOut
from models import Item, Trip, TripItem
from database import get_db
from sqlalchemy import select
@ -11,7 +11,7 @@ from config import FIXED_USER_ID
router = APIRouter(prefix="/trips", tags=["Trips"])
@router.post("/")
@router.post("/", response_model=TripOut)
async def create_trip(trip: TripCreate, db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID
@ -33,7 +33,7 @@ async def create_trip(trip: TripCreate, db: AsyncSession = Depends(get_db)):
nights = days - 1
# relevante Items
result = await db.execute(select(Item).where(Item.user_id == user_id))
result = await db.execute(select(Item).where(Item.user_id == user_id).options(selectinload(Item.tags)))
all_items = result.scalars().all()
trip_items = []
@ -73,36 +73,22 @@ async def create_trip(trip: TripCreate, db: AsyncSession = Depends(get_db)):
db.add_all(trip_items)
await db.commit()
return {"status": "trip created", "trip_id": str(db_trip.id)}
# return {"status": "trip created", "trip_id": str(db_trip.id)}
return db_trip
@router.get("/{trip_id}/items")
@router.get("/{trip_id}/items", response_model=list[TripItemOut])
async def get_trip_items(trip_id: UUID, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(TripItem).where(TripItem.trip_id == trip_id))
result = await db.execute(select(TripItem).where(TripItem.trip_id == trip_id).options(selectinload(TripItem.item)))
items = result.scalars().all()
return [
{
"label": item.calculated_label,
"tag": item.tag,
"checked": item.checked
} for item in items
]
return [TripItemOut.model_validate(item) for item in items]
@router.get("/")
@router.get("/", response_model=list[TripOut])
async def get_trips(db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID
result = await db.execute(select(Trip).where(Trip.user_id == user_id))
trips = result.scalars().all()
return [
{
"id": str(trip.id),
"name": trip.name,
"start_date": trip.start_date,
"end_date": trip.end_date,
"selected_tags": trip.selected_tags,
"marked_tags": trip.marked_tags
} for trip in trips
]
return trips
def replace_placeholders(text: str, days: int, nights: int) -> str:
def replacer(match):

View file

@ -1,21 +1,65 @@
from typing import List, Optional
from uuid import UUID
from datetime import date
from typing import List
from pydantic import BaseModel
class TagCreate(BaseModel):
class TagBase(BaseModel):
name: str
class TagCreate(TagBase):
pass
class ItemCreate(BaseModel):
class TagOut(TagBase):
id: UUID
class Config:
orm_mode = True
class ItemBase(BaseModel):
name: str
tag_names: List[str]
class ItemCreate(ItemBase):
tag_ids: List[UUID] = []
class TripCreate(BaseModel):
name: str
start_date: date
end_date: date
selected_tags: List[str]
marked_tags: List[str]
class ItemOut(ItemBase):
id: UUID
tags: List[TagOut] = []
class Config:
orm_mode = True
class TripBase(BaseModel):
name: Optional[str] = None
start_date: Optional[date] = None
end_date: Optional[date] = None
class TripCreate(TripBase):
selected_tag_ids: List[UUID] = []
marked_tag_ids: List[UUID] = []
class TripUpdate(BaseModel):
name: Optional[str] = None
start_date: Optional[date] = None
end_date: Optional[date] = None
selected_tag_ids: Optional[List[UUID]] = None
marked_tag_ids: Optional[List[UUID]] = None
class TripOut(TripBase):
id: UUID
selected_tags: List[TagOut]
marked_tags: List[TagOut]
class Config:
orm_mode = True
class TripItemOut(BaseModel):
id: UUID
trip_id: UUID
item_id: UUID
name_calculated: str
checked: bool
tag: Optional[TagOut] = None
class Config:
orm_mode = True
class TripRegenerationResult(BaseModel):
trip_id: UUID
deleted_checked_trip_item_ids: List[UUID] = []
created_trip_item_ids: List[UUID] = []

View file

@ -9,6 +9,8 @@ services:
- ./backend:/app
environment:
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/packlist
depends_on:
- db
db:
image: postgres:15