goon/app/api/saved_searches.py
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

108 lines
3.3 KiB
Python

"""Saved searches, zapisane słowa kluczowe per urządzenie.
User-report mobilism: sceny słabo opisane → user chce zapisywać często używane
zapytania i szybko je odpalać. Scope po device_id (X-Device-Id), jak reszta stanu.
Endpointy:
GET /saved-searches lista zapisów tego urządzenia (najnowsze pierwsze)
POST /saved-searches dodaj {query} (idempotent na device+query)
DELETE /saved-searches/{id} usuń
"""
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, field_validator
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.api.device import get_device_id
from app.auth import require_api_key
from app.db import get_session
from app.models.saved_search import SavedSearch
router = APIRouter(
prefix="/saved-searches",
tags=["saved-searches"],
dependencies=[Depends(require_api_key)],
)
# Limit zapisów per urządzenie, to wygodny skrót, nie nieograniczona kolekcja.
MAX_PER_DEVICE = 50
class SavedSearchOut(BaseModel):
id: uuid.UUID
query: str
created_at: datetime
class SavedSearchIn(BaseModel):
query: str
@field_validator("query")
@classmethod
def _clean(cls, v: str) -> str:
v = (v or "").strip()
if not v:
raise ValueError("query empty")
return v[:256]
@router.get("", response_model=list[SavedSearchOut])
def list_saved_searches(
session: Annotated[Session, Depends(get_session)],
device_id: Annotated[str, Depends(get_device_id)],
) -> list[SavedSearch]:
rows = session.execute(
select(SavedSearch)
.where(SavedSearch.device_id == device_id)
.order_by(SavedSearch.created_at.desc())
).scalars().all()
return list(rows)
@router.post("", response_model=SavedSearchOut, status_code=status.HTTP_200_OK)
def add_saved_search(
payload: SavedSearchIn,
session: Annotated[Session, Depends(get_session)],
device_id: Annotated[str, Depends(get_device_id)],
) -> SavedSearch:
# Idempotent, ten sam (device, query) zwraca istniejący wiersz.
existing = session.execute(
select(SavedSearch).where(
SavedSearch.device_id == device_id, SavedSearch.query == payload.query
)
).scalar_one_or_none()
if existing is not None:
return existing
count = session.execute(
select(SavedSearch.id).where(SavedSearch.device_id == device_id)
).scalars().all()
if len(count) >= MAX_PER_DEVICE:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"saved-search limit ({MAX_PER_DEVICE}) reached",
)
row = SavedSearch(device_id=device_id, query=payload.query)
session.add(row)
session.commit()
session.refresh(row)
return row
@router.delete("/{search_id}", status_code=status.HTTP_204_NO_CONTENT)
def remove_saved_search(
search_id: uuid.UUID,
session: Annotated[Session, Depends(get_session)],
device_id: Annotated[str, Depends(get_device_id)],
) -> None:
row = session.get(SavedSearch, search_id)
# Tylko własne urządzenie może usuwać; cudze/nieistniejące → idempotentny no-op.
if row is None or row.device_id != device_id:
return
session.delete(row)
session.commit()