From 6096c156de58352e2b25b55a1df60d44c46f857e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:42:22 +0000 Subject: [PATCH 1/2] Initial plan From 55ffbb035aa49a53fc0ebd9a79c794d3624915d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:46:18 +0000 Subject: [PATCH 2/2] Print human-readable error messages with suggestions when API calls fail Co-authored-by: VsevolodX <79542055+VsevolodX@users.noreply.github.com> --- src/py/mat3ra/api_client/settings.py | 41 +++++++++++++++++ src/py/mat3ra/api_client/utils/http.py | 12 ++++- tests/py/unit/test_httpBase.py | 61 ++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 src/py/mat3ra/api_client/settings.py diff --git a/src/py/mat3ra/api_client/settings.py b/src/py/mat3ra/api_client/settings.py new file mode 100644 index 0000000..fe4562a --- /dev/null +++ b/src/py/mat3ra/api_client/settings.py @@ -0,0 +1,41 @@ +from typing import Dict, Tuple + +# Mapping of HTTP status codes to (display_text, suggestion) +HTTP_ERROR_MAP: Dict[int, Tuple[str, str]] = { + 400: ( + "Bad Request", + "Check your request parameters and data.", + ), + 401: ( + "Unauthorized", + "Check your authentication token or log in again.", + ), + 403: ( + "Forbidden", + "You do not have permission to access this resource. Set the correct project or check your account permissions.", + ), + 404: ( + "Not Found", + "The requested resource does not exist. Check the ID or path.", + ), + 409: ( + "Conflict", + "A resource with this identifier already exists.", + ), + 422: ( + "Unprocessable Entity", + "The request data is invalid. Check your input.", + ), + 429: ( + "Too Many Requests", + "You have exceeded your quota. Update your quota or try again later.", + ), + 500: ( + "Internal Server Error", + "An error occurred on the server. Contact support if the problem persists.", + ), + 503: ( + "Service Unavailable", + "The service is temporarily unavailable. Try again later.", + ), +} diff --git a/src/py/mat3ra/api_client/utils/http.py b/src/py/mat3ra/api_client/utils/http.py index 73b80de..076431d 100644 --- a/src/py/mat3ra/api_client/utils/http.py +++ b/src/py/mat3ra/api_client/utils/http.py @@ -1,6 +1,8 @@ import requests import urllib.parse +from mat3ra.api_client.settings import HTTP_ERROR_MAP + class BaseConnection(object): """ @@ -32,7 +34,15 @@ def request(self, method, url, params=None, data=None, headers=None): params (dict): URL parameters to append to the URL. """ self.response = self.session.request(method=method.lower(), url=url, params=params, data=data, headers=headers) - self.response.raise_for_status() + try: + self.response.raise_for_status() + except requests.HTTPError as e: + status_code = self.response.status_code + display_text, suggestion = HTTP_ERROR_MAP.get(status_code, ("HTTP Error", "")) + message = f"Error {status_code}: {display_text}." + if suggestion: + message += f" {suggestion}" + raise requests.HTTPError(message, response=self.response) from e def get_response(self): """ diff --git a/tests/py/unit/test_httpBase.py b/tests/py/unit/test_httpBase.py index be3a8dc..84fb1fd 100644 --- a/tests/py/unit/test_httpBase.py +++ b/tests/py/unit/test_httpBase.py @@ -8,6 +8,14 @@ API_VERSION_2 = "2018-10-2" HTTP_STATUS_UNAUTHORIZED = 401 HTTP_REASON_UNAUTHORIZED = "Unauthorized" +HTTP_STATUS_INTERNAL_SERVER_ERROR = 500 +HTTP_REASON_INTERNAL_SERVER_ERROR = "Internal Server Error" +HTTP_STATUS_TOO_MANY_REQUESTS = 429 +HTTP_REASON_TOO_MANY_REQUESTS = "Too Many Requests" +HTTP_STATUS_FORBIDDEN = 403 +HTTP_REASON_FORBIDDEN = "Forbidden" +HTTP_STATUS_UNKNOWN = 418 +HTTP_REASON_UNKNOWN = "I'm a Teapot" EMPTY_CONTENT = "" TEST_ENTITY_ID = "28FMvD5knJZZx452H" EMPTY_USERNAME = "" @@ -41,3 +49,56 @@ def test_raise_http_error(self, mock_request): with self.assertRaises(HTTPError): conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) conn.request("POST", "login", data={"username": EMPTY_USERNAME, "password": EMPTY_PASSWORD}) + + @mock.patch("requests.sessions.Session.request") + def test_http_error_message_known_status(self, mock_request): + mock_request.return_value = self.mock_response(EMPTY_CONTENT, HTTP_STATUS_UNAUTHORIZED, + reason=HTTP_REASON_UNAUTHORIZED) + with self.assertRaises(HTTPError) as ctx: + conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) + conn.request("POST", "login", data={"username": EMPTY_USERNAME, "password": EMPTY_PASSWORD}) + self.assertIn("Error 401", str(ctx.exception)) + self.assertIn("Unauthorized", str(ctx.exception)) + self.assertIn("authentication token", str(ctx.exception)) + + @mock.patch("requests.sessions.Session.request") + def test_http_error_message_500(self, mock_request): + mock_request.return_value = self.mock_response(EMPTY_CONTENT, HTTP_STATUS_INTERNAL_SERVER_ERROR, + reason=HTTP_REASON_INTERNAL_SERVER_ERROR) + with self.assertRaises(HTTPError) as ctx: + conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) + conn.request("POST", "jobs/id/submit") + self.assertIn("Error 500", str(ctx.exception)) + self.assertIn("Internal Server Error", str(ctx.exception)) + self.assertIn("Contact support", str(ctx.exception)) + + @mock.patch("requests.sessions.Session.request") + def test_http_error_message_429_quota(self, mock_request): + mock_request.return_value = self.mock_response(EMPTY_CONTENT, HTTP_STATUS_TOO_MANY_REQUESTS, + reason=HTTP_REASON_TOO_MANY_REQUESTS) + with self.assertRaises(HTTPError) as ctx: + conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) + conn.request("POST", "materials") + self.assertIn("Error 429", str(ctx.exception)) + self.assertIn("quota", str(ctx.exception)) + + @mock.patch("requests.sessions.Session.request") + def test_http_error_message_403_project(self, mock_request): + mock_request.return_value = self.mock_response(EMPTY_CONTENT, HTTP_STATUS_FORBIDDEN, + reason=HTTP_REASON_FORBIDDEN) + with self.assertRaises(HTTPError) as ctx: + conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) + conn.request("GET", "workflows") + self.assertIn("Error 403", str(ctx.exception)) + self.assertIn("project", str(ctx.exception)) + + @mock.patch("requests.sessions.Session.request") + def test_http_error_message_unknown_status(self, mock_request): + mock_request.return_value = self.mock_response(EMPTY_CONTENT, HTTP_STATUS_UNKNOWN, + reason=HTTP_REASON_UNKNOWN) + with self.assertRaises(HTTPError) as ctx: + conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) + conn.request("GET", "materials") + self.assertIn("Error 418", str(ctx.exception)) + self.assertIn("HTTP Error", str(ctx.exception)) +