inventory/crudkit/api/flask_api.py
2025-09-24 09:53:25 -05:00

127 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from flask import Blueprint, jsonify, request, abort
from urllib.parse import urlencode
from crudkit.api._cursor import encode_cursor, decode_cursor
from crudkit.core.service import _is_truthy
def _bool_param(d: dict[str, str], key: str, default: bool) -> bool:
return _is_truthy(d.get(key, "1" if default else "0"))
def _safe_int(value: str | None, default: int) -> int:
try:
return int(value) if value is not None else default
except Exception:
return default
def _link_with_params(base_url: str, **params) -> str:
# Filter out None, encode safely
q = {k: v for k, v in params.items() if v is not None}
return f"{base_url}?{urlencode(q)}"
def generate_crud_blueprint(model, service):
bp = Blueprint(model.__name__.lower(), __name__)
@bp.get("/")
def list_items():
# Work from a copy so we don't mutate request.args
args = request.args.to_dict(flat=True)
legacy_offset = "offset" in args or "page" in args
limit = _safe_int(args.get("limit"), 50)
args["limit"] = limit
if legacy_offset:
# Old behavior: honor limit/offset, same CRUDSpec goodies
items = service.list(args)
return jsonify([obj.as_dict() for obj in items])
# New behavior: keyset pagination with cursors
cursor_token = args.get("cursor")
key, desc_from_cursor, backward = decode_cursor(cursor_token)
window = service.seek_window(
args,
key=key,
backward=backward,
include_total=_bool_param(args, "include_total", True),
)
# Prefer the order actually used by the window; fall back to desc_from_cursor if needed.
try:
desc_flags = list(window.order.desc)
except Exception:
desc_flags = desc_from_cursor or []
body = {
"items": [obj.as_dict() for obj in window.items],
"limit": window.limit,
"next_cursor": encode_cursor(window.last_key, desc_flags, backward=False),
"prev_cursor": encode_cursor(window.first_key, desc_flags, backward=True),
"total": window.total,
}
resp = jsonify(body)
# Preserve users other query params like include_total, filters, sorts, etc.
base_url = request.base_url
base_params = {k: v for k, v in args.items() if k not in {"cursor"}}
link_parts = []
if body["next_cursor"]:
link_parts.append(
f'<{_link_with_params(base_url, **base_params, cursor=body["next_cursor"])}>; rel="next"'
)
if body["prev_cursor"]:
link_parts.append(
f'<{_link_with_params(base_url, **base_params, cursor=body["prev_cursor"])}>; rel="prev"'
)
if link_parts:
resp.headers["Link"] = ", ".join(link_parts)
return resp
@bp.get("/<int:id>")
def get_item(id):
try:
item = service.get(id, request.args)
if item is None:
abort(404)
return jsonify(item.as_dict())
except Exception as e:
# Could be validation, auth, or just you forgetting an index again
return jsonify({"status": "error", "error": str(e)}), 400
@bp.post("/")
def create_item():
payload = request.get_json(silent=True) or {}
try:
obj = service.create(payload)
return jsonify(obj.as_dict()), 201
except Exception as e:
return jsonify({"status": "error", "error": str(e)}), 400
@bp.patch("/<int:id>")
def update_item(id):
payload = request.get_json(silent=True) or {}
try:
obj = service.update(id, payload)
return jsonify(obj.as_dict())
except Exception as e:
# 404 if not found, 400 if validation. Your service can throw specific exceptions if you ever feel like being professional.
return jsonify({"status": "error", "error": str(e)}), 400
@bp.delete("/<int:id>")
def delete_item(id):
try:
service.delete(id)
# 204 means "no content" so don't send any.
return ("", 204)
except Exception as e:
return jsonify({"status": "error", "error": str(e)}), 400
return bp