# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE from contextlib import suppress from enum import Enum from werkzeug.exceptions import NotFound from werkzeug.routing import Map, Submount from werkzeug.wrappers import Request, Response import frappe from frappe import _ from frappe.modules.utils import get_doctype_app_map from frappe.monitor import add_data_to_monitor from frappe.pulse.app_heartbeat_event import capture_app_heartbeat from frappe.utils.response import build_response class ApiVersion(str, Enum): V1 = "v1" V2 = "v2" def handle(request: Request): """ Entry point for `/api` methods. APIs are versioned using second part of path. v1 -> `/api/v1/*` v2 -> `/api/v2/*` Different versions have different specification but broadly following things are supported: - `/api/method/{methodname}` will call a whitelisted method - `/api/resource/{doctype}` will query a table examples: - `?fields=["name", "owner"]` - `?filters=[["Task", "name", "like", "%005"]]` - `?limit_start=0` - `?limit_page_length=20` - `/api/resource/{doctype}/{name}` will point to a resource `GET` will return document `POST` will insert `PUT` will update `DELETE` will delete """ if frappe.get_system_settings("log_api_requests"): doc = frappe.get_doc( { "doctype": "API Request Log", "path": request.path, "user": frappe.session.user, "method": request.method, } ) doc.deferred_insert() try: endpoint, arguments = API_URL_MAP.bind_to_environ(request.environ).match() except NotFound: # Wrap 404 - backward compatiblity raise frappe.DoesNotExistError data = endpoint(**arguments) if isinstance(data, Response): return data if data is not None: frappe.response["data"] = data data = build_response("json") with suppress(Exception): method = arguments.get("method") or frappe.form_dict.get("method") doctype = arguments.get("doctype") or frappe.form_dict.get("doctype") if method or doctype: app_name = None if doctype: app_name = get_doctype_app_map().get(doctype) elif method and "." in method: app_name = method.split(".", 1)[0] if app_name: add_data_to_monitor(app=app_name) capture_app_heartbeat(app_name) return data # Merge all API version routing rules from frappe.api.v1 import url_rules as v1_rules from frappe.api.v2 import url_rules as v2_rules API_URL_MAP = Map( [ # V1 routes Submount("/api", v1_rules), Submount(f"/api/{ApiVersion.V1.value}", v1_rules), Submount(f"/api/{ApiVersion.V2.value}", v2_rules), ], strict_slashes=False, # Allows skipping trailing slashes merge_slashes=False, ) def get_api_version() -> ApiVersion | None: if not frappe.request: return if frappe.request.path.startswith(f"/api/{ApiVersion.V2.value}"): return ApiVersion.V2 return ApiVersion.V1