diff --git a/frappe/api/__init__.py b/frappe/api/__init__.py index 9dc4ba63ac..e4f6056004 100644 --- a/frappe/api/__init__.py +++ b/frappe/api/__init__.py @@ -1,5 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +from enum import Enum + from werkzeug.exceptions import NotFound from werkzeug.routing import Map, Submount from werkzeug.wrappers import Request, Response @@ -10,6 +12,11 @@ from frappe import _ from frappe.utils.response import build_response +class ApiVersion(str, Enum): + V1 = "v1" + V2 = "v2" + + def handle(request: Request): """ Entry point for `/api` methods. @@ -56,9 +63,18 @@ API_URL_MAP = Map( [ # V1 routes Submount("/api", v1_rules), - Submount("/api/v1", v1_rules), - Submount("/api/v2", v2_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 or not frappe.request.path.startswith("/api"): + return + + if frappe.request.path.startswith(f"/api/{ApiVersion.V2.value}"): + return ApiVersion.V2 + return ApiVersion.V1 diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index 7458ae5d09..91eef7fb1c 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -455,21 +455,23 @@ class TestMethodAPIV2(FrappeAPITestCase): def test_logs(self): method = "frappe.tests.test_api.test" - def get_message(resp, msg_type): - return frappe.parse_json(frappe.parse_json(frappe.parse_json(resp.json)[msg_type])[0]) + expected_message = "Failed v2" + response = self.get( + self.method_path(method), {"sid": self.sid, "message": expected_message} + ).json - expected_message = "Failed" - response = self.get(self.method_path(method), {"sid": self.sid, "message": expected_message}) - self.assertEqual(get_message(response, "_server_messages").message, expected_message) + self.assertIsInstance(response["messages"], list) + self.assertEqual(response["messages"][0]["message"], expected_message) # Cause handled failured with suppress_stdout(): response = self.get( self.method_path(method), {"sid": self.sid, "message": expected_message, "fail": True} - ) - self.assertEqual(get_message(response, "_server_messages").message, expected_message) - self.assertEqual(response.json["exc_type"], "ValidationError") - self.assertIn("Traceback", response.json["exc"]) + ).json + self.assertIsInstance(response["errors"], list) + self.assertEqual(response["errors"][0]["message"], expected_message) + self.assertEqual(response["errors"][0]["type"], "ValidationError") + self.assertIn("Traceback", response["errors"][0]["exception"]) # Cause handled failured with suppress_stdout(): @@ -477,9 +479,10 @@ class TestMethodAPIV2(FrappeAPITestCase): self.method_path(method), {"sid": self.sid, "message": expected_message, "fail": True, "handled": False}, ) - self.assertNotIn("_server_messages", response.json) - self.assertIn("ZeroDivisionError", response.json["exception"]) # WHY? - self.assertIn("Traceback", response.json["exc"]) + + self.assertIsInstance(response["errors"], list) + self.assertEqual(response["errors"][0]["type"], "ZeroDivisionError") + self.assertIn("Traceback", response["errors"][0]["exception"]) class TestDocTypeAPIV2(FrappeAPITestCase): diff --git a/frappe/utils/response.py b/frappe/utils/response.py index d6cea3775f..4abefbafb4 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -6,7 +6,7 @@ import decimal import json import mimetypes import os -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from urllib.parse import quote import werkzeug.utils @@ -21,7 +21,7 @@ import frappe.sessions import frappe.utils from frappe import _ from frappe.core.doctype.access_log.access_log import make_access_log -from frappe.utils import cint, format_timedelta +from frappe.utils import format_timedelta if TYPE_CHECKING: from frappe.core.doctype.file.file import File @@ -99,6 +99,7 @@ def as_raw(): def as_json(): make_logs() + response = Response() if frappe.local.response.http_status_code: response.status_code = frappe.local.response["http_status_code"] @@ -125,13 +126,22 @@ def as_binary(): return response -def make_logs(response=None): - # TODO: v2 API +def make_logs(): """make strings for msgprint and errprint""" + + from frappe.api import ApiVersion, get_api_version + + match get_api_version(): + case ApiVersion.V1: + _make_logs_v1() + case ApiVersion.V2: + _make_logs_v2() + + +def _make_logs_v1(): from frappe.utils.error import guess_exception_source - if not response: - response = frappe.local.response + response = frappe.local.response if frappe.error_log: if source := guess_exception_source(frappe.local.error_log and frappe.local.error_log[0]["exc"]): @@ -143,13 +153,20 @@ def make_logs(response=None): [frappe.utils.cstr(d) for d in frappe.local.message_log] ) - if frappe.debug_log and frappe.conf.get("logging") or False: + if frappe.debug_log and frappe.conf.get("logging"): response["_debug_messages"] = json.dumps(frappe.local.debug_log) if frappe.flags.error_message: response["_error_message"] = frappe.flags.error_message +def _make_logs_v2(): + response = frappe.local.response + + if frappe.local.message_log: + response["messages"] = [frappe.parse_json(l) for l in frappe.local.message_log] + + def json_handler(obj): """serialize non-serializable data for json""" from collections.abc import Iterable