From edaca07da1f42f8cdc5a9654316bbca0927b993e Mon Sep 17 00:00:00 2001 From: "reportportal.io" Date: Fri, 28 Mar 2025 15:26:06 +0000 Subject: [PATCH 1/3] Changelog update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 900a4ad..333be20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] + +## [5.6.3] ### Added - All Requests now have their names, by @HardNorth ### Removed From 3ea53598ecc65880ab699f9dbba3ddef63d1a7db Mon Sep 17 00:00:00 2001 From: "reportportal.io" Date: Fri, 28 Mar 2025 15:26:07 +0000 Subject: [PATCH 2/3] Version update --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dcd57ab..c089873 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup -__version__ = "5.6.3" +__version__ = "5.6.4" TYPE_STUBS = ["*.pyi"] From 2226ebfd2b7064f8119c3ff0371d3a1223780b5b Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Mon, 31 Mar 2025 15:56:25 +0300 Subject: [PATCH 3/3] Fix isort --- CHANGELOG.md | 4 ++ reportportal_client/aio/client.py | 3 +- reportportal_client/client.py | 3 +- reportportal_client/core/rp_requests.py | 85 ++++++++++++++++++++++-- reportportal_client/core/rp_responses.py | 10 +-- 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 333be20..bb7415b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Added +- `ErrorPrintingHttpRequest` and `ErrorPrintingAsyncHttpRequest` classes to avoid recursion on ReportPortal logging, by @HardNorth +### Removed +- Any logging on log requests to ReportPortal, by @HardNorth ## [5.6.3] ### Added diff --git a/reportportal_client/aio/client.py b/reportportal_client/aio/client.py index 403a807..467bfa7 100644 --- a/reportportal_client/aio/client.py +++ b/reportportal_client/aio/client.py @@ -61,6 +61,7 @@ AsyncItemStartRequest, AsyncRPLogBatch, AsyncRPRequestLog, + ErrorPrintingAsyncHttpRequest, LaunchFinishRequest, LaunchStartRequest, RPFile, @@ -568,7 +569,7 @@ async def log_batch(self, log_batch: Optional[List[AsyncRPRequestLog]]) -> Optio """ url = root_uri_join(self.base_url_v2, "log") if log_batch: - response = await AsyncHttpRequest( + response = await ErrorPrintingAsyncHttpRequest( (await self.session()).post, url=url, data=AsyncRPLogBatch(log_batch).payload, name="log" ).make() if not response: diff --git a/reportportal_client/client.py b/reportportal_client/client.py index 92026b7..69d45a3 100644 --- a/reportportal_client/client.py +++ b/reportportal_client/client.py @@ -40,6 +40,7 @@ # noinspection PyProtectedMember from reportportal_client.core.rp_issues import Issue from reportportal_client.core.rp_requests import ( + ErrorPrintingHttpRequest, HttpRequest, ItemFinishRequest, ItemStartRequest, @@ -809,7 +810,7 @@ def update_test_item( def _log(self, batch: Optional[List[RPRequestLog]]) -> Optional[Tuple[str, ...]]: if batch: url = uri_join(self.base_url_v2, "log") - response = HttpRequest( + response = ErrorPrintingHttpRequest( self.session.post, url, files=RPLogBatch(batch).payload, diff --git a/reportportal_client/core/rp_requests.py b/reportportal_client/core/rp_requests.py index 4b29a5c..8c5499e 100644 --- a/reportportal_client/core/rp_requests.py +++ b/reportportal_client/core/rp_requests.py @@ -20,7 +20,10 @@ import asyncio import logging +import sys +import traceback from dataclasses import dataclass +from datetime import datetime from typing import Any, Callable, List, Optional, Tuple, TypeVar, Union import aiohttp @@ -48,7 +51,7 @@ class HttpRequest: - """This model stores attributes related to ReportPortal HTTP requests.""" + """This object stores attributes related to ReportPortal HTTP requests and makes them.""" session_method: Callable url: Any @@ -121,8 +124,8 @@ def priority(self, value: Priority) -> None: def make(self) -> Optional[RPResponse]: """Make HTTP request to the ReportPortal API. - The method catches any request preparation error to not fail reporting. Since we are reporting tool - and should not fail tests. + The method catches any request error to not fail reporting. Since we are reporting tool and should not fail + tests. :return: wrapped HTTP response or None in case of failure """ @@ -141,8 +144,45 @@ def make(self) -> Optional[RPResponse]: logger.warning("ReportPortal %s request failed", self.name, exc_info=exc) +class ErrorPrintingHttpRequest(HttpRequest): + """This is specific request object which catches any request error and prints it to the "std.err". + + The object is supposed to be used in logging methods only to prevent infinite recursion of logging, when logging + framework configured to log everything to ReportPortal. In this case if a request to ReportPortal fails, the + failure will be logged to ReportPortal once again and, for example, in case of endpoint configuration error, it + will also fail and will be logged again. So, the recursion will never end. + + This class is used to prevent this situation. It catches any request error and prints it to the "std.err". + """ + + def make(self) -> Optional[RPResponse]: + """Make HTTP request to the ReportPortal API. + + The method catches any request error and prints it to the "std.err". + + :return: wrapped HTTP response or None in case of failure + """ + # noinspection PyBroadException + try: + return RPResponse( + self.session_method( + self.url, + data=self.data, + json=self.json, + files=self.files, + verify=self.verify_ssl, + timeout=self.http_timeout, + ) + ) + except Exception: + print( + f"{datetime.now().isoformat()} - [ERROR] - ReportPortal request error:\n{traceback.format_exc()}", + file=sys.stderr, + ) + + class AsyncHttpRequest(HttpRequest): - """This model stores attributes related to asynchronous ReportPortal HTTP requests.""" + """This object stores attributes related to asynchronous ReportPortal HTTP requests and make them.""" def __init__( self, @@ -166,8 +206,8 @@ def __init__( async def make(self) -> Optional[AsyncRPResponse]: """Asynchronously make HTTP request to the ReportPortal API. - The method catches any request preparation error to not fail reporting. Since we are reporting tool - and should not fail tests. + The method catches any request error to not fail reporting. Since we are reporting tool and should not fail + tests. :return: wrapped HTTP response or None in case of failure """ @@ -182,6 +222,39 @@ async def make(self) -> Optional[AsyncRPResponse]: logger.warning("ReportPortal %s request failed", self.name, exc_info=exc) +class ErrorPrintingAsyncHttpRequest(AsyncHttpRequest): + """This is specific request object which catches any request error and prints it to the "std.err". + + The object is supposed to be used in logging methods only to prevent infinite recursion of logging, when logging + framework configured to log everything to ReportPortal. In this case if a request to ReportPortal fails, the + failure will be logged to ReportPortal once again and, for example, in case of endpoint configuration error, it + will also fail and will be logged again. So, the recursion will never end. + + This class is used to prevent this situation. It catches any request error and prints it to the "std.err". + """ + + async def make(self) -> Optional[AsyncRPResponse]: + """Asynchronously make HTTP request to the ReportPortal API. + + The method catches any request error and prints it to the "std.err". + + :return: wrapped HTTP response or None in case of failure + """ + url = await await_if_necessary(self.url) + if not url: + return None + data = await await_if_necessary(self.data) + json = await await_if_necessary(self.json) + # noinspection PyBroadException + try: + return AsyncRPResponse(await self.session_method(url, data=data, json=json)) + except Exception: + print( + f"{datetime.now().isoformat()} - [ERROR] - ReportPortal request error:\n{traceback.format_exc()}", + file=sys.stderr, + ) + + class RPRequestBase(metaclass=AbstractBaseClass): """Base class for specific ReportPortal request models. diff --git a/reportportal_client/core/rp_responses.py b/reportportal_client/core/rp_responses.py index 2c8902d..0c3b0cf 100644 --- a/reportportal_client/core/rp_responses.py +++ b/reportportal_client/core/rp_responses.py @@ -36,7 +36,6 @@ def _iter_json_messages(json: Any) -> Generator[str, None, None]: data = json.get("responses", [json]) for chunk in data: if "message" not in chunk: - logger.warning(f"Response chunk does not contain 'message' field: {str(chunk)}") continue message = chunk["message"] if message: @@ -115,9 +114,7 @@ def message(self) -> Optional[str]: :return: message as string or NOT_FOUND, or None if the response is not JSON """ - if self.json is None: - return None - return self.json.get("message") + return _get_field("message", self.json) @property def messages(self) -> Optional[Tuple[str, ...]]: @@ -181,10 +178,7 @@ async def message(self) -> Optional[str]: :return: message as string or NOT_FOUND, or None if the response is not JSON """ - json = await self.json - if json is None: - return None - return _get_field("message", json) + return _get_field("message", await self.json) @property async def messages(self) -> Optional[Tuple[str, ...]]: