Skip to content

Commit 1659187

Browse files
authored
Merge pull request #265 from reportportal/develop
Release
2 parents 43111f7 + a2eac78 commit 1659187

35 files changed

+325
-753
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
runs-on: ubuntu-latest
3535
strategy:
3636
matrix:
37-
python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13' ]
37+
python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ]
3838
steps:
3939
- name: Checkout repository
4040
uses: actions/checkout@v4

CHANGELOG.md

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

33
## [Unreleased]
4+
### Added
5+
- Official `Python 3.14` support, by @HardNorth
6+
- Custom log level support in `RPLogHandler` class, by @HardNorth
7+
### Removed
8+
- `Python 3.7` support, by @HardNorth
9+
- Deprecated `log_manager.py` module, by @HardNorth
10+
11+
## [5.6.7]
412
### Changed
513
- `log_batch_payload_size` was renamed to `log_batch_payload_limit` as it was originally supposed, by @HardNorth
614

reportportal_client/__init__.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515

1616
import sys
1717
import warnings
18-
from typing import Optional, Tuple, TypedDict, Union
18+
from typing import Optional, TypedDict, Union
1919

2020
# noinspection PyUnreachableCode
2121
if sys.version_info >= (3, 11):
2222
from typing import Unpack
2323
else:
2424
from typing_extensions import Unpack
2525

26-
import aenum
26+
import aenum # type: ignore
2727

2828
# noinspection PyProtectedMember
2929
from reportportal_client._internal.local import current, set_current
@@ -43,9 +43,6 @@ class ClientType(aenum.Enum):
4343

4444

4545
class _ClientOptions(TypedDict, total=False):
46-
client_type: ClientType
47-
endpoint: str
48-
project: str
4946
api_key: Optional[str]
5047
# OAuth 2.0 parameters
5148
oauth_uri: Optional[str]
@@ -60,7 +57,7 @@ class _ClientOptions(TypedDict, total=False):
6057
verify_ssl: Union[bool, str]
6158
retries: int
6259
max_pool_size: int
63-
http_timeout: Union[float, Tuple[float, float]]
60+
http_timeout: Union[float, tuple[float, float]]
6461
mode: str
6562
launch_uuid_print: bool
6663
print_output: OutputType
@@ -122,15 +119,16 @@ def create_client(
122119
:return: ReportPortal Client instance.
123120
"""
124121
my_kwargs = kwargs.copy()
125-
if "log_batch_payload_size" in my_kwargs:
122+
if "log_batch_payload_size" in my_kwargs: # type: ignore
126123
warnings.warn(
127124
message="Your agent is using `log_batch_payload_size` property which was introduced by mistake. "
128125
"The real property name is `log_batch_payload_limit`. Please consider Agent version update.",
129126
category=DeprecationWarning,
130127
stacklevel=2,
131128
)
129+
payload_size = my_kwargs.pop("log_batch_payload_size") # type: ignore
132130
if "log_batch_payload_limit" not in my_kwargs:
133-
my_kwargs["log_batch_payload_limit"] = my_kwargs.pop("log_batch_payload_size")
131+
my_kwargs["log_batch_payload_limit"] = payload_size
134132

135133
if client_type is ClientType.SYNC:
136134
return RPClient(endpoint, project, **my_kwargs)

reportportal_client/_internal/aio/http.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
import asyncio
2525
import sys
2626
from types import TracebackType
27-
from typing import Any, Callable, Coroutine, Optional, Type, Union
27+
from typing import Any, Callable, Coroutine, Optional, Union
2828

29-
from aenum import Enum
29+
from aenum import Enum # type: ignore
3030
from aiohttp import ClientResponse, ClientResponseError
3131
from aiohttp import ClientSession as AioHttpClientSession
3232
from aiohttp import ServerConnectionError
@@ -77,31 +77,33 @@ def __init__(
7777
self.__retry_number = max_retry_number
7878
self.__retry_delay = base_retry_delay
7979

80-
async def __nothing(self):
80+
async def __nothing(self) -> None:
8181
pass
8282

83-
def __sleep(self, retry_num: int, retry_factor: int) -> Coroutine:
84-
if retry_num > 0: # don't wait at the first retry attempt
83+
def __sleep(self, retry_num: int, retry_factor: Optional[int]) -> Coroutine:
84+
if retry_num > 0 and retry_factor is not None: # don't wait at the first retry attempt
8585
delay = (((retry_factor * self.__retry_delay) * 1000) ** retry_num) / 1000
8686
return asyncio.sleep(delay)
8787
else:
8888
return self.__nothing()
8989

90-
async def __request(self, method: Callable, url, **kwargs: Any) -> ClientResponse:
90+
async def __request(
91+
self, method: Callable[..., Coroutine[Any, Any, ClientResponse]], url: str, **kwargs: Any
92+
) -> ClientResponse:
9193
"""Make a request and retry if necessary.
9294
9395
The method retries requests depending on error class and retry number. For no-retry errors, such as
9496
400 Bad Request it just returns result, for cases where it's reasonable to retry it does it in
9597
exponential manner.
9698
"""
97-
result = None
99+
result: Optional[ClientResponse] = None
98100
exceptions = []
99101

100102
for i in range(self.__retry_number + 1): # add one for the first attempt, which is not a retry
101-
retry_factor = None
103+
retry_factor: Optional[int] = None
102104
if result is not None:
103105
# Release previous result to return connection to pool
104-
await result.release()
106+
result.release()
105107
try:
106108
result = await method(url, **kwargs)
107109
except Exception as exc:
@@ -136,6 +138,8 @@ async def __request(self, method: Callable, url, **kwargs: Any) -> ClientRespons
136138
raise exceptions[-1]
137139
else:
138140
raise exceptions[0]
141+
if result is None:
142+
raise IOError("Request failed without exceptions")
139143
return result
140144

141145
def get(self, url: str, *, allow_redirects: bool = True, **kwargs: Any) -> Coroutine[Any, Any, ClientResponse]:
@@ -150,7 +154,7 @@ def put(self, url: str, *, data: Any = None, **kwargs: Any) -> Coroutine[Any, An
150154
"""Perform HTTP PUT request."""
151155
return self.__request(self._client.put, url, data=data, **kwargs)
152156

153-
def close(self) -> Coroutine:
157+
def close(self) -> Coroutine[None, None, None]:
154158
"""Gracefully close internal aiohttp.ClientSession class instance."""
155159
return self._client.close()
156160

@@ -160,7 +164,7 @@ async def __aenter__(self) -> "RetryingClientSession":
160164

161165
async def __aexit__(
162166
self,
163-
exc_type: Optional[Type[BaseException]],
167+
exc_type: Optional[type[BaseException]],
164168
exc_val: Optional[BaseException],
165169
exc_tb: Optional[TracebackType],
166170
) -> None:
@@ -241,7 +245,7 @@ async def __aenter__(self) -> "ClientSession":
241245

242246
async def __aexit__(
243247
self,
244-
exc_type: Optional[Type[BaseException]],
248+
exc_type: Optional[type[BaseException]],
245249
exc_val: Optional[BaseException],
246250
exc_tb: Optional[TracebackType],
247251
) -> None:

reportportal_client/_internal/aio/tasks.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import sys
1818
import time
1919
from asyncio import Future
20-
from typing import Any, Awaitable, Coroutine, Generator, Generic, List, Optional, TypeVar, Union
20+
from typing import Any, Awaitable, Coroutine, Generator, Generic, Optional, TypeVar, Union
2121

2222
from reportportal_client.aio.tasks import BlockingOperationError, Task
2323

@@ -142,7 +142,7 @@ def __call__(
142142
class TriggerTaskBatcher(Generic[_T]):
143143
"""Batching class which compile its batches by object number or by passed time."""
144144

145-
__task_list: List[_T]
145+
__task_list: list[_T]
146146
__last_run_time: float
147147
__trigger_num: int
148148
__trigger_interval: float
@@ -170,7 +170,7 @@ def __ready_to_run(self) -> bool:
170170
return True
171171
return False
172172

173-
def append(self, value: _T) -> Optional[List[_T]]:
173+
def append(self, value: _T) -> Optional[list[_T]]:
174174
"""Add an object to internal batch and return the batch if it's triggered.
175175
176176
:param value: an object to add to the batch
@@ -184,7 +184,7 @@ def append(self, value: _T) -> Optional[List[_T]]:
184184
self.__task_list = []
185185
return tasks
186186

187-
def flush(self) -> Optional[List[_T]]:
187+
def flush(self) -> Optional[list[_T]]:
188188
"""Immediately return everything what's left in the internal batch.
189189
190190
:return: a batch or None
@@ -200,7 +200,7 @@ def flush(self) -> Optional[List[_T]]:
200200
class BackgroundTaskList(Generic[_T]):
201201
"""Task list class which collects Tasks into internal batch and removes when they complete."""
202202

203-
__task_list: List[_T]
203+
__task_list: list[_T]
204204

205205
def __init__(self):
206206
"""Initialize an instance of the Batcher."""
@@ -222,7 +222,7 @@ def append(self, value: _T) -> None:
222222
self.__remove_finished()
223223
self.__task_list.append(value)
224224

225-
def flush(self) -> Optional[List[_T]]:
225+
def flush(self) -> Optional[list[_T]]:
226226
"""Immediately return everything what's left unfinished in the internal batch.
227227
228228
:return: a batch or None

reportportal_client/_internal/http.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"""This module designed to help with synchronous HTTP request/response handling."""
1616

1717
from types import TracebackType
18-
from typing import Any, Callable, Optional, Type, Union
18+
from typing import Any, Callable, Optional, Union
1919

2020
from requests import Response, Session
2121
from requests.adapters import BaseAdapter
@@ -104,7 +104,7 @@ def __enter__(self) -> "ClientSession":
104104

105105
def __exit__(
106106
self,
107-
exc_type: Optional[Type[BaseException]],
107+
exc_type: Optional[type[BaseException]],
108108
exc_val: Optional[BaseException],
109109
exc_tb: Optional[TracebackType],
110110
) -> None:

reportportal_client/_internal/logs/batcher.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import logging
1717
import threading
18-
from typing import Any, Dict, Generic, List, Optional, TypeVar
18+
from typing import Any, Generic, Optional, TypeVar
1919

2020
from reportportal_client.core.rp_requests import AsyncRPRequestLog, RPRequestLog
2121
from reportportal_client.logs import MAX_LOG_BATCH_PAYLOAD_SIZE, MAX_LOG_BATCH_SIZE
@@ -35,7 +35,7 @@ class LogBatcher(Generic[T_co]):
3535
entry_num: int
3636
payload_limit: int
3737
_lock: threading.Lock
38-
_batch: List[T_co]
38+
_batch: list[T_co]
3939
_payload_size: int
4040

4141
def __init__(self, entry_num=MAX_LOG_BATCH_SIZE, payload_limit=MAX_LOG_BATCH_PAYLOAD_SIZE) -> None:
@@ -50,7 +50,7 @@ def __init__(self, entry_num=MAX_LOG_BATCH_SIZE, payload_limit=MAX_LOG_BATCH_PAY
5050
self._batch = []
5151
self._payload_size = 0
5252

53-
def _append(self, size: int, log_req: RPRequestLog) -> Optional[List[RPRequestLog]]:
53+
def _append(self, size: int, log_req: RPRequestLog) -> Optional[list[RPRequestLog]]:
5454
with self._lock:
5555
if self._payload_size + size >= self.payload_limit:
5656
if len(self._batch) > 0:
@@ -68,23 +68,23 @@ def _append(self, size: int, log_req: RPRequestLog) -> Optional[List[RPRequestLo
6868
self._payload_size = 0
6969
return batch
7070

71-
def append(self, log_req: RPRequestLog) -> Optional[List[RPRequestLog]]:
71+
def append(self, log_req: RPRequestLog) -> Optional[list[RPRequestLog]]:
7272
"""Add a log request object to internal batch and return the batch if it's full.
7373
7474
:param log_req: log request object
7575
:return: a batch or None
7676
"""
7777
return self._append(log_req.multipart_size, log_req)
7878

79-
async def append_async(self, log_req: AsyncRPRequestLog) -> Optional[List[AsyncRPRequestLog]]:
79+
async def append_async(self, log_req: AsyncRPRequestLog) -> Optional[list[AsyncRPRequestLog]]:
8080
"""Add a log request object to internal batch and return the batch if it's full.
8181
8282
:param log_req: log request object
8383
:return: a batch or None
8484
"""
8585
return self._append(await log_req.multipart_size, log_req)
8686

87-
def flush(self) -> Optional[List[T_co]]:
87+
def flush(self) -> Optional[list[T_co]]:
8888
"""Immediately return everything what's left in the internal batch.
8989
9090
:return: a batch or None
@@ -99,7 +99,7 @@ def flush(self) -> Optional[List[T_co]]:
9999
self._payload_size = 0
100100
return batch
101101

102-
def __getstate__(self) -> Dict[str, Any]:
102+
def __getstate__(self) -> dict[str, Any]:
103103
"""Control object pickling and return object fields as Dictionary.
104104
105105
:return: object state dictionary
@@ -110,7 +110,7 @@ def __getstate__(self) -> Dict[str, Any]:
110110
del state["_lock"]
111111
return state
112112

113-
def __setstate__(self, state: Dict[str, Any]) -> None:
113+
def __setstate__(self, state: dict[str, Any]) -> None:
114114
"""Control object pickling, receives object state as Dictionary.
115115
116116
:param dict state: object state dictionary

reportportal_client/_internal/services/client_id.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io
1818
import logging
1919
import os
20+
from typing import Iterable, Optional
2021
from uuid import uuid4
2122

2223
from .constants import CLIENT_ID_PROPERTY, RP_FOLDER_PATH, RP_PROPERTIES_FILE_PATH
@@ -32,35 +33,35 @@ def __preprocess_file(self, fp):
3233
content = "[" + self.DEFAULT_SECTION + "]\n" + fp.read()
3334
return io.StringIO(content)
3435

35-
def read(self, filenames, encoding=None):
36+
def read_file(self, filenames: Iterable[str], source: Optional[str] = None) -> None:
3637
if isinstance(filenames, str):
3738
filenames = [filenames]
3839
for filename in filenames:
3940
with open(filename, "r") as fp:
4041
preprocessed_fp = self.__preprocess_file(fp)
41-
self.read_file(preprocessed_fp, filename)
42+
super().read_file(preprocessed_fp, filename)
4243

43-
def write(self, fp, space_around_delimiters=True):
44+
def write(self, fp, space_around_delimiters: bool = True) -> None:
4445
for key, value in self.items(self.DEFAULT_SECTION):
4546
delimiter = " = " if space_around_delimiters else "="
4647
fp.write("{}{}{}\n".format(key, delimiter, value))
4748

4849

49-
def __read_config():
50+
def __read_config() -> configparser.ConfigParser:
5051
config = __NoSectionConfigParser()
5152
if os.path.exists(RP_PROPERTIES_FILE_PATH):
52-
config.read(RP_PROPERTIES_FILE_PATH)
53+
config.read_file(RP_PROPERTIES_FILE_PATH)
5354
return config
5455

5556

56-
def _read_client_id():
57+
def _read_client_id() -> Optional[str]:
5758
config = __read_config()
5859
if config.has_option(__NoSectionConfigParser.DEFAULT_SECTION, CLIENT_ID_PROPERTY):
5960
return config.get(__NoSectionConfigParser.DEFAULT_SECTION, CLIENT_ID_PROPERTY)
6061
return None
6162

6263

63-
def _store_client_id(client_id):
64+
def _store_client_id(client_id: str) -> None:
6465
config = __read_config()
6566
if not os.path.exists(RP_FOLDER_PATH):
6667
os.makedirs(RP_FOLDER_PATH)
@@ -69,7 +70,7 @@ def _store_client_id(client_id):
6970
config.write(fp)
7071

7172

72-
def get_client_id():
73+
def get_client_id() -> str:
7374
"""Return unique client ID of the instance, generate new if not exists."""
7475
client_id = None
7576
try:

reportportal_client/_internal/services/client_id.pyi

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)