Skip to content

Release #258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## [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
- All Requests now have their names, by @HardNorth
### Removed
- `NOT_FOUND` constant, as it only causes infinite issues, by @HardNorth
Expand Down
3 changes: 2 additions & 1 deletion reportportal_client/aio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
AsyncItemStartRequest,
AsyncRPLogBatch,
AsyncRPRequestLog,
ErrorPrintingAsyncHttpRequest,
LaunchFinishRequest,
LaunchStartRequest,
RPFile,
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion reportportal_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
85 changes: 79 additions & 6 deletions reportportal_client/core/rp_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
"""
Expand All @@ -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,
Expand All @@ -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
"""
Expand All @@ -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.

Expand Down
10 changes: 2 additions & 8 deletions reportportal_client/core/rp_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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, ...]]:
Expand Down Expand Up @@ -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, ...]]:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from setuptools import find_packages, setup

__version__ = "5.6.3"
__version__ = "5.6.4"

TYPE_STUBS = ["*.pyi"]

Expand Down