# 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// -> list (filters via ?q=..., sort=..., limit=..., cursor=...) GET /api// -> get POST /api// -> create PATCH /api// -> update (partial) DELETE /api//[?hard=1] -> delete RPC (legacy): GET /api//get?id=123 GET /api//list GET /api//seek_window GET /api//page POST /api//create PATCH /api//update?id=123 DELETE /api//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("/") 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("/") 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("/") 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