165 lines
6.1 KiB
Python
165 lines
6.1 KiB
Python
# crudkit/api/flask_api.py
|
|
|
|
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:
|
|
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, *, base_prefix: str | None = None, rest: bool = True, rpc: bool = True):
|
|
"""
|
|
REST:
|
|
GET /api/<models>/ -> list (filters via ?q=..., sort=..., limit=..., cursor=...)
|
|
GET /api/<models>/<id> -> get
|
|
POST /api/<models>/ -> create
|
|
PATCH /api/<models>/<id> -> update (partial)
|
|
DELETE /api/<models>/<id>[?hard=1] -> delete
|
|
|
|
RPC (legacy):
|
|
GET /api/<model>/get?id=123
|
|
GET /api/<model>/list
|
|
GET /api/<model>/seek_window
|
|
GET /api/<model>/page
|
|
POST /api/<model>/create
|
|
PATCH /api/<model>/update?id=123
|
|
DELETE /api/<model>/delete?id=123[&hard=1]
|
|
"""
|
|
model_name = model.__name__.lower()
|
|
# bikeshed if you want pluralization; this is the least-annoying default
|
|
collection = (base_prefix or model_name).lower()
|
|
plural = collection if collection.endswith('s') else f"{collection}s"
|
|
|
|
bp = Blueprint(plural, __name__, url_prefix=f"/api/{plural}")
|
|
|
|
# ---------- REST ----------
|
|
if rest:
|
|
@bp.get("/")
|
|
def rest_list():
|
|
args = request.args.to_dict(flat=True)
|
|
# support cursor pagination transparently; fall back to limit/offset
|
|
try:
|
|
items = service.list(args)
|
|
return jsonify([o.as_dict() for o in items])
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "error": str(e)}), 400
|
|
|
|
@bp.get("/<int:obj_id>")
|
|
def rest_get(obj_id: int):
|
|
try:
|
|
item = service.get(obj_id, request.args)
|
|
if item is None:
|
|
abort(404)
|
|
return jsonify(item.as_dict())
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "error": str(e)}), 400
|
|
|
|
@bp.post("/")
|
|
def rest_create():
|
|
payload = request.get_json(silent=True) or {}
|
|
try:
|
|
obj = service.create(payload)
|
|
resp = jsonify(obj.as_dict())
|
|
resp.status_code = 201
|
|
resp.headers["Location"] = f"{request.base_url.rstrip('/')}/{obj.id}"
|
|
return resp
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "error": str(e)}), 400
|
|
|
|
@bp.patch("/<int:obj_id>")
|
|
def rest_update(obj_id: int):
|
|
payload = request.get_json(silent=True) or {}
|
|
try:
|
|
obj = service.update(obj_id, payload)
|
|
return jsonify(obj.as_dict())
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "error": str(e)}), 400
|
|
|
|
@bp.delete("/<int:obj_id>")
|
|
def rest_delete(obj_id: int):
|
|
hard = (request.args.get("hard") in ("1", "true", "yes"))
|
|
try:
|
|
obj = service.delete(obj_id, hard=hard)
|
|
if obj is None:
|
|
abort(404)
|
|
return ("", 204)
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "error": str(e)}), 400
|
|
|
|
# ---------- RPC (your existing routes) ----------
|
|
if rpc:
|
|
# your original functions verbatim, shortened here for sanity
|
|
@bp.get("/get")
|
|
def rpc_get():
|
|
id_ = int(request.args.get("id", 0))
|
|
if not id_:
|
|
return jsonify({"status": "error", "error": "missing required param: id"}), 400
|
|
try:
|
|
item = service.get(id_, request.args)
|
|
if item is None:
|
|
abort(404)
|
|
return jsonify(item.as_dict())
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "error": str(e)}), 400
|
|
|
|
@bp.get("/list")
|
|
def rpc_list():
|
|
args = request.args.to_dict(flat=True)
|
|
try:
|
|
items = service.list(args)
|
|
return jsonify([obj.as_dict() for obj in items])
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "error": str(e)}), 400
|
|
|
|
@bp.post("/create")
|
|
def rpc_create():
|
|
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("/update")
|
|
def rpc_update():
|
|
id_ = int(request.args.get("id", 0))
|
|
if not id_:
|
|
return jsonify({"status": "error", "error": "missing required param: id"}), 400
|
|
payload = request.get_json(silent=True) or {}
|
|
try:
|
|
obj = service.update(id_, payload)
|
|
return jsonify(obj.as_dict())
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "error": str(e)}), 400
|
|
|
|
@bp.delete("/delete")
|
|
def rpc_delete():
|
|
id_ = int(request.args.get("id", 0))
|
|
if not id_:
|
|
return jsonify({"status": "error", "error": "missing required param: id"}), 400
|
|
hard = (request.args.get("hard") in ("1", "true", "yes"))
|
|
try:
|
|
obj = service.delete(id_, hard=hard)
|
|
return ("", 204) if obj is not None else abort(404)
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "error": str(e)}), 400
|
|
|
|
return bp
|