style: format backend with black

Also drops the session-sliding comment in login_required.
This commit is contained in:
2026-10-02 14:46:16 -05:00
parent 2512c67893
commit 63c9991421
14 changed files with 320 additions and 82 deletions
+43 -10
View File
@@ -1,4 +1,5 @@
"""Books slice: CRUD, progress, search/filter, and the genre list."""
import re
import psycopg2.errors
@@ -60,7 +61,9 @@ def escape_like(term: str) -> str:
def owned_book(book_id: int) -> dict:
row = query_one(BOOK_SELECT + " WHERE b.id = %s AND b.user_id = %s", (book_id, g.user_id))
row = query_one(
BOOK_SELECT + " WHERE b.id = %s AND b.user_id = %s", (book_id, g.user_id)
)
if row is None:
raise ApiError(404, "Book not found")
return row
@@ -80,7 +83,9 @@ def _save(sql: str, params: tuple) -> dict | None:
try:
return query_one(sql, params)
except psycopg2.errors.ForeignKeyViolation:
raise ApiError(400, "Unknown genre_id; see GET /api/genres", "genre_id") from None
raise ApiError(
400, "Unknown genre_id; see GET /api/genres", "genre_id"
) from None
@bp.get("/genres")
@@ -97,14 +102,18 @@ def list_books():
search = request.args.get("q", "").strip()
if search:
if len(search) > MAX_TEXT or "\x00" in search:
raise ApiError(400, f"q must be plain text of at most {MAX_TEXT} characters", "q")
raise ApiError(
400, f"q must be plain text of at most {MAX_TEXT} characters", "q"
)
pattern = f"%{escape_like(search)}%"
sql += " AND (b.title ILIKE %s OR b.author ILIKE %s)"
params += [pattern, pattern]
genre_id = request.args.get("genre_id", "")
if genre_id:
if not GENRE_ID_PATTERN.fullmatch(genre_id) or int(genre_id) > MAX_GENRE_ID:
raise ApiError(400, "genre_id must be an id from GET /api/genres", "genre_id")
raise ApiError(
400, "genre_id must be an id from GET /api/genres", "genre_id"
)
sql += " AND b.genre_id = %s"
params.append(int(genre_id))
sql += " ORDER BY b.updated_at DESC, b.id DESC"
@@ -116,7 +125,9 @@ def list_books():
def create_book():
body = json_body(FIELDS)
book = {field: VALIDATORS[field](body) for field in REQUIRED_FIELDS}
book["current_page"] = VALIDATORS["current_page"](body) if "current_page" in body else 0
book["current_page"] = (
VALIDATORS["current_page"](body) if "current_page" in body else 0
)
_check_progress(book, "current_page")
row = _save(
"""
@@ -124,7 +135,14 @@ def create_book():
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id
""",
(g.user_id, book["title"], book["author"], book["genre_id"], book["total_pages"], book["current_page"]),
(
g.user_id,
book["title"],
book["author"],
book["genre_id"],
book["total_pages"],
book["current_page"],
),
)
return jsonify(to_json(owned_book(row["id"]))), 201
@@ -143,7 +161,9 @@ def update_book(book_id: int):
if not body:
raise ApiError(400, f"Provide at least one of: {', '.join(sorted(FIELDS))}")
current = owned_book(book_id)
book = {field: current[field] for field in FIELDS} | {field: VALIDATORS[field](body) for field in body}
book = {field: current[field] for field in FIELDS} | {
field: VALIDATORS[field](body) for field in body
}
_check_progress(book, "total_pages" if "total_pages" in body else "current_page")
_save(
"""
@@ -151,8 +171,15 @@ def update_book(book_id: int):
SET title = %s, author = %s, genre_id = %s, total_pages = %s, current_page = %s, updated_at = now()
WHERE id = %s AND user_id = %s
""",
(book["title"], book["author"], book["genre_id"], book["total_pages"], book["current_page"],
book_id, g.user_id),
(
book["title"],
book["author"],
book["genre_id"],
book["total_pages"],
book["current_page"],
book_id,
g.user_id,
),
)
return jsonify(to_json(owned_book(book_id)))
@@ -160,6 +187,12 @@ def update_book(book_id: int):
@bp.delete("/books/<int(max=2147483647):book_id>")
@login_required
def delete_book(book_id: int):
if query_one("DELETE FROM books WHERE id = %s AND user_id = %s RETURNING id", (book_id, g.user_id)) is None:
if (
query_one(
"DELETE FROM books WHERE id = %s AND user_id = %s RETURNING id",
(book_id, g.user_id),
)
is None
):
raise ApiError(404, "Book not found")
return "", 204