Skip to content

Commit 66a0639

Browse files
authored
Merge pull request #221 from reportportal/develop
Release
2 parents 34c7000 + 117ca91 commit 66a0639

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+6085
-2342
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ jobs:
5050
pip install tox tox-gh-actions
5151
5252
- name: Test with tox
53+
timeout-minutes: 10
5354
run: tox
5455

5556
- name: Upload coverage to Codecov
56-
if: matrix.python-version == 3.7 && success()
57+
if: matrix.python-version == 3.8 && success()
5758
uses: codecov/codecov-action@v3
5859
with:
5960
files: coverage.xml

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
# Changelog
22

33
## [Unreleased]
4+
### Added
5+
- `RP` class in `reportportal_client.client` module as common interface for all ReportPortal clients, by @HardNorth
6+
- `reportportal_client.aio` with asynchronous clients and auxiliary classes, by @HardNorth
7+
- Dependency on `aiohttp` and `certifi`, by @HardNorth
8+
### Changed
9+
- RPClient class does not use separate Thread for log processing anymore, by @HardNorth
10+
- Use `importlib.metadata` package for distribution data extraction for Python versions starting 3.8, by @HardNorth
11+
- `helpers.verify_value_length` function updated to truncate attribute keys also and reveal attributes were truncated, by @HardNorth
12+
### Removed
13+
- Dependency on `six`, by @HardNorth
14+
15+
## [5.4.1]
416
### Changed
517
- Unified ReportPortal product naming, by @HardNorth
618
- `RPClient` internal item stack implementation changed to `LifoQueue` to maintain concurrency better, by @HardNorth

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
include MANIFEST.in
22
include README.md CONTRIBUTING.rst requirements.txt
3+
exclude test_res/*

reportportal_client/__init__.py

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,124 @@
1-
"""
2-
Copyright (c) 2022 https://reportportal.io .
1+
# Copyright (c) 2023 EPAM Systems
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# https://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License
313

4-
Licensed under the Apache License, Version 2.0 (the "License");
5-
you may not use this file except in compliance with the License.
6-
You may obtain a copy of the License at
14+
"""This package is the base package for ReportPortal client."""
15+
import typing
16+
import warnings
717

8-
https://www.apache.org/licenses/LICENSE-2.0
18+
import aenum
919

10-
Unless required by applicable law or agreed to in writing, software
11-
distributed under the License is distributed on an "AS IS" BASIS,
12-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
See the License for the specific language governing permissions and
14-
limitations under the License.
15-
"""
20+
# noinspection PyProtectedMember
21+
from reportportal_client._internal.local import current, set_current
22+
from reportportal_client.aio.client import AsyncRPClient, BatchedRPClient, ThreadedRPClient
23+
from reportportal_client.client import RP, RPClient, OutputType
24+
from reportportal_client.logs import RPLogger, RPLogHandler
25+
from reportportal_client.steps import step
26+
27+
28+
class ClientType(aenum.Enum):
29+
"""Enum of possible type of ReportPortal clients."""
30+
31+
SYNC = aenum.auto()
32+
ASYNC = aenum.auto()
33+
ASYNC_THREAD = aenum.auto()
34+
ASYNC_BATCHED = aenum.auto()
35+
36+
37+
# noinspection PyIncorrectDocstring
38+
def create_client(
39+
client_type: ClientType,
40+
endpoint: str,
41+
project: str,
42+
*,
43+
api_key: str = None,
44+
**kwargs: typing.Any
45+
) -> typing.Optional[RP]:
46+
"""Create and ReportPortal Client based on the type and arguments provided.
47+
48+
:param client_type: Type of the Client to create.
49+
:type client_type: ClientType
50+
:param endpoint: Endpoint of the ReportPortal service.
51+
:type endpoint: str
52+
:param project: Project name to report to.
53+
:type project: str
54+
:param api_key: Authorization API key.
55+
:type api_key: str
56+
:param launch_uuid: A launch UUID to use instead of starting own one.
57+
:type launch_uuid: str
58+
:param is_skipped_an_issue: Option to mark skipped tests as not 'To Investigate' items on the server
59+
side.
60+
:type is_skipped_an_issue: bool
61+
:param verify_ssl: Option to skip ssl verification.
62+
:type verify_ssl: typing.Union[bool, str]
63+
:param retries: Number of retry attempts to make in case of connection / server
64+
errors.
65+
:type retries: int
66+
:param max_pool_size: Option to set the maximum number of connections to save the pool.
67+
:type max_pool_size: int
68+
:param http_timeout : A float in seconds for connect and read timeout. Use a Tuple to
69+
specific connect and read separately.
70+
:type http_timeout: Tuple[float, float]
71+
:param mode: Launch mode, all Launches started by the client will be in that mode.
72+
:type mode: str
73+
:param launch_uuid_print: Print Launch UUID into passed TextIO or by default to stdout.
74+
:type launch_uuid_print: bool
75+
:param print_output: Set output stream for Launch UUID printing.
76+
:type print_output: OutputType
77+
:param log_batch_size: Option to set the maximum number of logs that can be processed in one
78+
batch.
79+
:type log_batch_size: int
80+
:param log_batch_payload_limit: Maximum size in bytes of logs that can be processed in one batch.
81+
:type log_batch_payload_limit: int
82+
:param keepalive_timeout: For Async Clients only. Maximum amount of idle time in seconds before
83+
force connection closing.
84+
:type keepalive_timeout: int
85+
:param task_timeout: For Async Threaded and Batched Clients only. Time limit in seconds for a
86+
Task processing.
87+
:type task_timeout: float
88+
:param shutdown_timeout: For Async Threaded and Batched Clients only. Time limit in seconds for
89+
shutting down internal Tasks.
90+
:type shutdown_timeout: float
91+
:param trigger_num: For Async Batched Client only. Number of tasks which triggers Task batch
92+
execution.
93+
:type trigger_num: int
94+
:param trigger_interval: For Async Batched Client only. Time limit which triggers Task batch
95+
execution.
96+
:type trigger_interval: float
97+
:return: ReportPortal Client instance.
98+
"""
99+
if client_type is ClientType.SYNC:
100+
return RPClient(endpoint, project, api_key=api_key, **kwargs)
101+
if client_type is ClientType.ASYNC:
102+
return AsyncRPClient(endpoint, project, api_key=api_key, **kwargs)
103+
if client_type is ClientType.ASYNC_THREAD:
104+
return ThreadedRPClient(endpoint, project, api_key=api_key, **kwargs)
105+
if client_type is ClientType.ASYNC_BATCHED:
106+
return BatchedRPClient(endpoint, project, api_key=api_key, **kwargs)
107+
warnings.warn(f'Unknown ReportPortal Client type requested: {client_type}', RuntimeWarning, stacklevel=2)
16108

17-
from ._local import current
18-
from .logs import RPLogger, RPLogHandler
19-
from .client import RPClient
20-
from .steps import step
21109

22110
__all__ = [
111+
'ClientType',
112+
'create_client',
23113
'current',
114+
'set_current',
115+
'RP',
116+
'RPClient',
117+
'AsyncRPClient',
118+
'BatchedRPClient',
119+
'ThreadedRPClient',
120+
'OutputType',
24121
'RPLogger',
25122
'RPLogHandler',
26-
'RPClient',
27123
'step',
28124
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright (c) 2023 EPAM Systems
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# https://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License
13+
14+
"""ReportPortal client internal API. No warnings before breaking changes. No backward compatibility."""

tests/logs/__init__.py renamed to reportportal_client/_internal/aio/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
"""This package contains unit tests for logging."""
2-
3-
# Copyright (c) 2022 EPAM Systems
1+
# Copyright (c) 2023 EPAM Systems
42
# Licensed under the Apache License, Version 2.0 (the "License");
53
# you may not use this file except in compliance with the License.
64
# You may obtain a copy of the License at
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Copyright (c) 2023 EPAM Systems
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# https://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License
13+
#
14+
# https://www.apache.org/licenses/LICENSE-2.0
15+
#
16+
# Unless required by applicable law or agreed to in writing, software
17+
# distributed under the License is distributed on an "AS IS" BASIS,
18+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19+
# See the License for the specific language governing permissions and
20+
# limitations under the License
21+
22+
"""This module designed to help with asynchronous HTTP request/response handling."""
23+
24+
import asyncio
25+
import sys
26+
from types import TracebackType
27+
from typing import Coroutine, Any, Optional, Type, Callable
28+
29+
from aenum import Enum
30+
from aiohttp import ClientSession, ClientResponse, ServerConnectionError, \
31+
ClientResponseError
32+
33+
DEFAULT_RETRY_NUMBER: int = 5
34+
DEFAULT_RETRY_DELAY: float = 0.005
35+
THROTTLING_STATUSES: set = {425, 429}
36+
RETRY_STATUSES: set = {408, 500, 502, 503, 504, 507}.union(THROTTLING_STATUSES)
37+
38+
39+
class RetryClass(int, Enum):
40+
"""Enum contains error types and their retry delay multiply factor as values."""
41+
42+
SERVER_ERROR = 1
43+
CONNECTION_ERROR = 2
44+
THROTTLING = 3
45+
46+
47+
class RetryingClientSession:
48+
"""Class uses aiohttp.ClientSession.request method and adds request retry logic."""
49+
50+
_client: ClientSession
51+
__retry_number: int
52+
__retry_delay: float
53+
54+
def __init__(
55+
self,
56+
*args,
57+
max_retry_number: int = DEFAULT_RETRY_NUMBER,
58+
base_retry_delay: float = DEFAULT_RETRY_DELAY,
59+
**kwargs
60+
):
61+
"""Initialize an instance of the session with arguments.
62+
63+
To obtain the full list of arguments please see aiohttp.ClientSession.__init__() method. This class
64+
just bypass everything to the base method, except two local arguments 'max_retry_number' and
65+
'base_retry_delay'.
66+
67+
:param max_retry_number: the maximum number of the request retries if it was unsuccessful
68+
:param base_retry_delay: base value for retry delay, determine how much time the class will wait after
69+
an error. Real value highly depends on Retry Class and Retry attempt number,
70+
since retries are performed in exponential delay manner
71+
"""
72+
self._client = ClientSession(*args, **kwargs)
73+
self.__retry_number = max_retry_number
74+
self.__retry_delay = base_retry_delay
75+
76+
async def __nothing(self):
77+
pass
78+
79+
def __sleep(self, retry_num: int, retry_factor: int) -> Coroutine:
80+
if retry_num > 0: # don't wait at the first retry attempt
81+
delay = (((retry_factor * self.__retry_delay) * 1000) ** retry_num) / 1000
82+
return asyncio.sleep(delay)
83+
else:
84+
return self.__nothing()
85+
86+
async def __request(
87+
self, method: Callable, url, **kwargs: Any
88+
) -> ClientResponse:
89+
"""Make a request and retry if necessary.
90+
91+
The method retries requests depending on error class and retry number. For no-retry errors, such as
92+
400 Bad Request it just returns result, for cases where it's reasonable to retry it does it in
93+
exponential manner.
94+
"""
95+
result = None
96+
exceptions = []
97+
for i in range(self.__retry_number + 1): # add one for the first attempt, which is not a retry
98+
retry_factor = None
99+
try:
100+
result = await method(url, **kwargs)
101+
except Exception as exc:
102+
exceptions.append(exc)
103+
if isinstance(exc, ServerConnectionError) or isinstance(exc, ClientResponseError):
104+
retry_factor = RetryClass.CONNECTION_ERROR
105+
106+
if not retry_factor:
107+
raise exc
108+
109+
if result:
110+
if result.ok or result.status not in RETRY_STATUSES:
111+
return result
112+
113+
if result.status in THROTTLING_STATUSES:
114+
retry_factor = RetryClass.THROTTLING
115+
else:
116+
retry_factor = RetryClass.SERVER_ERROR
117+
118+
if i + 1 < self.__retry_number:
119+
# don't wait at the last attempt
120+
await self.__sleep(i, retry_factor)
121+
122+
if exceptions:
123+
if len(exceptions) > 1:
124+
if sys.version_info > (3, 10):
125+
# noinspection PyCompatibility
126+
raise ExceptionGroup( # noqa: F821
127+
'During retry attempts the following exceptions happened',
128+
exceptions
129+
)
130+
else:
131+
raise exceptions[-1]
132+
else:
133+
raise exceptions[0]
134+
return result
135+
136+
def get(self, url: str, *, allow_redirects: bool = True,
137+
**kwargs: Any) -> Coroutine[Any, Any, ClientResponse]:
138+
"""Perform HTTP GET request."""
139+
return self.__request(self._client.get, url, allow_redirects=allow_redirects, **kwargs)
140+
141+
def post(self, url: str, *, data: Any = None, **kwargs: Any) -> Coroutine[Any, Any, ClientResponse]:
142+
"""Perform HTTP POST request."""
143+
return self.__request(self._client.post, url, data=data, **kwargs)
144+
145+
def put(self, url: str, *, data: Any = None, **kwargs: Any) -> Coroutine[Any, Any, ClientResponse]:
146+
"""Perform HTTP PUT request."""
147+
return self.__request(self._client.put, url, data=data, **kwargs)
148+
149+
def close(self) -> Coroutine:
150+
"""Gracefully close internal aiohttp.ClientSession class instance."""
151+
return self._client.close()
152+
153+
async def __aenter__(self) -> "RetryingClientSession":
154+
"""Auxiliary method which controls what `async with` construction does on block enter."""
155+
return self
156+
157+
async def __aexit__(
158+
self,
159+
exc_type: Optional[Type[BaseException]],
160+
exc_val: Optional[BaseException],
161+
exc_tb: Optional[TracebackType],
162+
) -> None:
163+
"""Auxiliary method which controls what `async with` construction does on block exit."""
164+
await self.close()

0 commit comments

Comments
 (0)