diff --git a/CHANGELOG.md b/CHANGELOG.md index 12d917a..900a4ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,16 @@ # Changelog ## [Unreleased] +### Added +- All Requests now have their names, by @HardNorth +### Removed +- `NOT_FOUND` constant, as it only causes infinite issues, by @HardNorth + +## [5.6.2] ### Fixed - Issue [#246](https://github.com/reportportal/client-Python/issues/246): Invalid return type, by @HardNorth ### Changed -- `helpers.common_helpers.gen_attributes` function now accepts refactored, by @HardNorth +- `helpers.common_helpers.gen_attributes` function was refactored, by @HardNorth ## [5.6.1] ### Added diff --git a/reportportal_client/_internal/aio/tasks.py b/reportportal_client/_internal/aio/tasks.py index aae2486..fea52c0 100644 --- a/reportportal_client/_internal/aio/tasks.py +++ b/reportportal_client/_internal/aio/tasks.py @@ -19,7 +19,6 @@ from asyncio import Future from typing import Any, Awaitable, Coroutine, Generator, Generic, List, Optional, TypeVar, Union -from reportportal_client._internal.static.defines import NOT_FOUND from reportportal_client.aio.tasks import BlockingOperationError, Task _T = TypeVar("_T") @@ -58,7 +57,7 @@ def blocking_result(self) -> _T: result = self.result() else: result = self.__loop.run_until_complete(self) - return result if result is not NOT_FOUND else None + return result class ThreadedTask(Generic[_T], Task[_T]): @@ -91,8 +90,7 @@ def blocking_result(self) -> _T: :return: execution result or raise an error, or return immediately if already executed """ if self.done(): - result = self.result() - return result if result is not NOT_FOUND else None + return self.result() if not self.__loop.is_running() or self.__loop.is_closed(): raise BlockingOperationError("Running loop is not alive") start_time = time.time() @@ -101,8 +99,7 @@ def blocking_result(self) -> _T: time.sleep(sleep_time) if not self.done(): raise BlockingOperationError("Timed out waiting for the task execution") - result = self.result() - return result if result is not NOT_FOUND else None + return self.result() class BatchedTaskFactory: diff --git a/reportportal_client/_internal/static/defines.py b/reportportal_client/_internal/static/defines.py index cc31ebd..20c6f0f 100644 --- a/reportportal_client/_internal/static/defines.py +++ b/reportportal_client/_internal/static/defines.py @@ -80,5 +80,4 @@ class Priority(enum.IntEnum): ATTRIBUTE_LENGTH_LIMIT = ATTRIBUTE_LIMIT DEFAULT_PRIORITY = Priority.PRIORITY_MEDIUM LOW_PRIORITY = Priority.PRIORITY_LOW -NOT_FOUND = _PresenceSentinel() NOT_SET = _PresenceSentinel() diff --git a/reportportal_client/aio/client.py b/reportportal_client/aio/client.py index 3c247df..403a807 100644 --- a/reportportal_client/aio/client.py +++ b/reportportal_client/aio/client.py @@ -51,7 +51,7 @@ from reportportal_client._internal.static.abstract import AbstractBaseClass, abstractmethod # noinspection PyProtectedMember -from reportportal_client._internal.static.defines import NOT_FOUND, NOT_SET +from reportportal_client._internal.static.defines import NOT_SET from reportportal_client.aio.tasks import Task from reportportal_client.client import RP, OutputType from reportportal_client.core.rp_issues import Issue @@ -225,14 +225,14 @@ async def close(self) -> None: async def __get_item_url(self, item_id_future: Union[Optional[str], Task[Optional[str]]]) -> Optional[str]: item_id = await await_if_necessary(item_id_future) - if item_id is NOT_FOUND or item_id is None: + if not item_id: logger.warning("Attempt to make request for non-existent id.") return None return root_uri_join(self.base_url_v2, "item", item_id) async def __get_launch_url(self, launch_uuid_future: Union[Optional[str], Task[Optional[str]]]) -> Optional[str]: launch_uuid = await await_if_necessary(launch_uuid_future) - if launch_uuid is NOT_FOUND or launch_uuid is None: + if not launch_uuid: logger.warning("Attempt to make request for non-existent launch.") return None return root_uri_join(self.base_url_v2, "launch", launch_uuid, "finish") @@ -270,7 +270,9 @@ async def start_launch( rerun_of=rerun_of, ).payload - response = await AsyncHttpRequest((await self.session()).post, url=url, json=request_payload).make() + response = await AsyncHttpRequest( + (await self.session()).post, url=url, json=request_payload, name="start_launch" + ).make() if not response: return None @@ -345,11 +347,13 @@ async def start_test_item( uuid=uuid, ).payload - response = await AsyncHttpRequest((await self.session()).post, url=url, json=request_payload).make() + response = await AsyncHttpRequest( + (await self.session()).post, url=url, json=request_payload, name="start_test_item" + ).make() if not response: return None item_id = await response.id - if item_id is NOT_FOUND or item_id is None: + if not item_id: logger.warning("start_test_item - invalid response: %s", str(await response.json)) else: logger.debug("start_test_item - ID: %s", item_id) @@ -400,7 +404,9 @@ async def finish_test_item( retry=retry, retry_of=retry_of, ).payload - response = await AsyncHttpRequest((await self.session()).put, url=url, json=request_payload).make() + response = await AsyncHttpRequest( + (await self.session()).put, url=url, json=request_payload, name="finish_test_item" + ).make() if not response: return None message = await response.message @@ -434,7 +440,7 @@ async def finish_launch( description=kwargs.get("description"), ).payload response = await AsyncHttpRequest( - (await self.session()).put, url=url, json=request_payload, name="Finish Launch" + (await self.session()).put, url=url, json=request_payload, name="finish_launch" ).make() if not response: return None @@ -463,7 +469,9 @@ async def update_test_item( } item_id = await self.get_item_id_by_uuid(item_uuid) url = root_uri_join(self.base_url_v1, "item", item_id, "update") - response = await AsyncHttpRequest((await self.session()).put, url=url, json=data).make() + response = await AsyncHttpRequest( + (await self.session()).put, url=url, json=data, name="update_test_item" + ).make() if not response: return None logger.debug("update_test_item - Item: %s", item_id) @@ -471,7 +479,7 @@ async def update_test_item( async def __get_launch_uuid_url(self, launch_uuid_future: Union[str, Task[str]]) -> Optional[str]: launch_uuid = await await_if_necessary(launch_uuid_future) - if launch_uuid is NOT_FOUND or launch_uuid is None: + if not launch_uuid: logger.warning("Attempt to make request for non-existent Launch UUID.") return None logger.debug("get_launch_info - ID: %s", launch_uuid) @@ -484,7 +492,7 @@ async def get_launch_info(self, launch_uuid_future: Union[str, Task[str]]) -> Op :return: Launch information in dictionary. """ url = self.__get_launch_uuid_url(launch_uuid_future) - response = await AsyncHttpRequest((await self.session()).get, url=url).make() + response = await AsyncHttpRequest((await self.session()).get, url=url, name="get_launch_info").make() if not response: return None launch_info = None @@ -497,7 +505,7 @@ async def get_launch_info(self, launch_uuid_future: Union[str, Task[str]]) -> Op async def __get_item_uuid_url(self, item_uuid_future: Union[Optional[str], Task[Optional[str]]]) -> Optional[str]: item_uuid = await await_if_necessary(item_uuid_future) - if item_uuid is NOT_FOUND or item_uuid is None: + if not item_uuid: logger.warning("Attempt to make request for non-existent UUID.") return None return root_uri_join(self.base_url_v1, "item", "uuid", item_uuid) @@ -509,7 +517,7 @@ async def get_item_id_by_uuid(self, item_uuid_future: Union[str, Task[str]]) -> :return: Test Item ID. """ url = self.__get_item_uuid_url(item_uuid_future) - response = await AsyncHttpRequest((await self.session()).get, url=url).make() + response = await AsyncHttpRequest((await self.session()).get, url=url, name="get_item_id").make() return await response.id if response else None async def get_launch_ui_id(self, launch_uuid_future: Union[str, Task[str]]) -> Optional[int]: @@ -549,7 +557,7 @@ async def get_project_settings(self) -> Optional[dict]: :return: Settings response in Dictionary. """ url = root_uri_join(self.base_url_v1, "settings") - response = await AsyncHttpRequest((await self.session()).get, url=url).make() + response = await AsyncHttpRequest((await self.session()).get, url=url, name="get_project_settings").make() return await response.json if response else None async def log_batch(self, log_batch: Optional[List[AsyncRPRequestLog]]) -> Optional[Tuple[str, ...]]: @@ -561,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( - (await self.session()).post, url=url, data=AsyncRPLogBatch(log_batch).payload + (await self.session()).post, url=url, data=AsyncRPLogBatch(log_batch).payload, name="log" ).make() if not response: return None @@ -639,8 +647,6 @@ def launch_uuid(self) -> Optional[str]: :return: UUID string. """ - if self.__launch_uuid is NOT_FOUND: - return None return self.__launch_uuid @property @@ -809,9 +815,10 @@ async def start_test_item( uuid=uuid, **kwargs, ) - if item_id and item_id is not NOT_FOUND: - logger.debug("start_test_item - ID: %s", item_id) - self._add_current_item(item_id) + if not item_id: + return None + logger.debug("start_test_item - ID: %s", item_id) + self._add_current_item(item_id) return item_id async def finish_test_item( @@ -972,9 +979,6 @@ async def log( :param item_id: UUID of the ReportPortal Item the message belongs to. :return: Response message Tuple if Log message batch was sent or None. """ - if item_id is NOT_FOUND: - logger.warning("Attempt to log to non-existent item") - return None rp_file = RPFile(**attachment) if attachment else None rp_log = AsyncRPRequestLog(self.__launch_uuid, time, rp_file, item_id, level, message) return await self.__client.log_batch(await self._log_batcher.append_async(rp_log)) diff --git a/reportportal_client/client.py b/reportportal_client/client.py index f95b4b0..92026b7 100644 --- a/reportportal_client/client.py +++ b/reportportal_client/client.py @@ -38,7 +38,6 @@ from reportportal_client._internal.static.abstract import AbstractBaseClass # noinspection PyProtectedMember -from reportportal_client._internal.static.defines import NOT_FOUND from reportportal_client.core.rp_issues import Issue from reportportal_client.core.rp_requests import ( HttpRequest, @@ -408,8 +407,6 @@ def launch_uuid(self) -> Optional[str]: :return: UUID string """ - if self.__launch_uuid is NOT_FOUND: - return None return self.__launch_uuid @property @@ -587,6 +584,7 @@ def start_launch( json=request_payload, verify_ssl=self.verify_ssl, http_timeout=self.http_timeout, + name="start_launch", ).make() if not response: return None @@ -638,9 +636,6 @@ def start_test_item( :param uuid: Test Item UUID to use on start (overrides server one, should be globally unique). :return: Test Item UUID if successfully started or None. """ - if parent_item_id is NOT_FOUND: - logger.warning("Attempt to start item for non-existent parent item.") - return None if parent_item_id: url = uri_join(self.base_url_v2, "item", parent_item_id) else: @@ -667,15 +662,16 @@ def start_test_item( json=request_payload, verify_ssl=self.verify_ssl, http_timeout=self.http_timeout, + name="start_test_item", ).make() if not response: return None item_id = response.id - if item_id is not NOT_FOUND: - logger.debug("start_test_item - ID: %s", item_id) - self._add_current_item(item_id) - else: + if not item_id: logger.warning("start_test_item - invalid response: %s", str(response.json)) + return None + logger.debug("start_test_item - ID: %s", item_id) + self._add_current_item(item_id) return item_id def finish_test_item( @@ -707,7 +703,7 @@ def finish_test_item( with the 'retry' parameter. :return: Response message. """ - if item_id is NOT_FOUND or not item_id: + if not item_id: logger.warning("Attempt to finish non-existent item") return None url = uri_join(self.base_url_v2, "item", item_id) @@ -724,7 +720,12 @@ def finish_test_item( retry_of=retry_of, ).payload response = HttpRequest( - self.session.put, url=url, json=request_payload, verify_ssl=self.verify_ssl, http_timeout=self.http_timeout + self.session.put, + url=url, + json=request_payload, + verify_ssl=self.verify_ssl, + http_timeout=self.http_timeout, + name="finish_test_item", ).make() if not response: return None @@ -748,7 +749,7 @@ def finish_launch( :param attributes: Launch attributes """ if self.use_own_launch: - if self.__launch_uuid is NOT_FOUND or not self.__launch_uuid: + if not self.__launch_uuid: logger.warning("Attempt to finish non-existent launch") return None url = uri_join(self.base_url_v2, "launch", self.__launch_uuid, "finish") @@ -763,8 +764,8 @@ def finish_launch( url=url, json=request_payload, verify_ssl=self.verify_ssl, - name="Finish Launch", http_timeout=self.http_timeout, + name="finish_launch", ).make() if not response: return None @@ -793,7 +794,12 @@ def update_test_item( item_id = self.get_item_id_by_uuid(item_uuid) url = uri_join(self.base_url_v1, "item", item_id, "update") response = HttpRequest( - self.session.put, url=url, json=data, verify_ssl=self.verify_ssl, http_timeout=self.http_timeout + self.session.put, + url=url, + json=data, + verify_ssl=self.verify_ssl, + http_timeout=self.http_timeout, + name="update_test_item", ).make() if not response: return None @@ -809,6 +815,7 @@ def _log(self, batch: Optional[List[RPRequestLog]]) -> Optional[Tuple[str, ...]] files=RPLogBatch(batch).payload, verify_ssl=self.verify_ssl, http_timeout=self.http_timeout, + name="log", ).make() if response: return response.messages @@ -833,9 +840,6 @@ def log( :param item_id: UUID of the ReportPortal Item the message belongs to. :return: Response message Tuple if Log message batch was sent or None. """ - if item_id is NOT_FOUND: - logger.warning("Attempt to log to non-existent item") - return None rp_file = RPFile(**attachment) if attachment else None rp_log = RPRequestLog(self.__launch_uuid, time, rp_file, item_id, level, message) return self._log(self._log_batcher.append(rp_log)) @@ -848,7 +852,7 @@ def get_item_id_by_uuid(self, item_uuid: str) -> Optional[str]: """ url = uri_join(self.base_url_v1, "item", "uuid", item_uuid) response = HttpRequest( - self.session.get, url=url, verify_ssl=self.verify_ssl, http_timeout=self.http_timeout + self.session.get, url=url, verify_ssl=self.verify_ssl, http_timeout=self.http_timeout, name="get_item_id" ).make() return response.id if response else None @@ -862,7 +866,11 @@ def get_launch_info(self) -> Optional[dict]: url = uri_join(self.base_url_v1, "launch", "uuid", self.__launch_uuid) logger.debug("get_launch_info - ID: %s", self.__launch_uuid) response = HttpRequest( - self.session.get, url=url, verify_ssl=self.verify_ssl, http_timeout=self.http_timeout + self.session.get, + url=url, + verify_ssl=self.verify_ssl, + http_timeout=self.http_timeout, + name="get_launch_info", ).make() if not response: return None @@ -911,7 +919,11 @@ def get_project_settings(self) -> Optional[dict]: """ url = uri_join(self.base_url_v1, "settings") response = HttpRequest( - self.session.get, url=url, verify_ssl=self.verify_ssl, http_timeout=self.http_timeout + self.session.get, + url=url, + verify_ssl=self.verify_ssl, + http_timeout=self.http_timeout, + name="get_project_settings", ).make() return response.json if response else None diff --git a/reportportal_client/core/rp_responses.py b/reportportal_client/core/rp_responses.py index 7f0c788..2c8902d 100644 --- a/reportportal_client/core/rp_responses.py +++ b/reportportal_client/core/rp_responses.py @@ -25,7 +25,7 @@ from requests import Response # noinspection PyProtectedMember -from reportportal_client._internal.static.defines import NOT_FOUND, NOT_SET +from reportportal_client._internal.static.defines import NOT_SET logger = logging.getLogger(__name__) @@ -35,7 +35,10 @@ def _iter_json_messages(json: Any) -> Generator[str, None, None]: return data = json.get("responses", [json]) for chunk in data: - message = chunk.get("message", chunk.get("error_code", NOT_FOUND)) + if "message" not in chunk: + logger.warning(f"Response chunk does not contain 'message' field: {str(chunk)}") + continue + message = chunk["message"] if message: yield message @@ -49,6 +52,18 @@ def _get_json_decode_error_message(response: Union[Response, ClientResponse]) -> ) +def _get_field(name: str, json: Optional[Any]) -> Optional[str]: + if json is None: + return None + if name not in json: + logger.warning(f"Unable to get '{name}' from json: {str(json)}") + return None + result_id = json[name] + if result_id is None: + logger.warning(f"Unable to get '{name}' from json: {str(json)}") + return result_id + + class RPResponse: """Class representing ReportPortal API response.""" @@ -69,9 +84,8 @@ def id(self) -> Optional[str]: :return: ID as string or NOT_FOUND, or None if the response is not JSON """ - if self.json is None: - return None - return self.json.get("id", NOT_FOUND) + json = self.json + return _get_field("id", json) @property def is_success(self) -> bool: @@ -137,9 +151,7 @@ async def id(self) -> Optional[str]: :return: ID as string or NOT_FOUND, or None if the response is not JSON """ json = await self.json - if json is None: - return None - return json.get("id", NOT_FOUND) + return _get_field("id", json) @property def is_success(self) -> bool: @@ -172,7 +184,7 @@ async def message(self) -> Optional[str]: json = await self.json if json is None: return None - return json.get("message", NOT_FOUND) + return _get_field("message", json) @property async def messages(self) -> Optional[Tuple[str, ...]]: diff --git a/reportportal_client/logs/log_manager.py b/reportportal_client/logs/log_manager.py index 4ed2297..5589e69 100644 --- a/reportportal_client/logs/log_manager.py +++ b/reportportal_client/logs/log_manager.py @@ -19,9 +19,6 @@ from threading import Lock from reportportal_client import helpers - -# noinspection PyProtectedMember -from reportportal_client._internal.static.defines import NOT_FOUND from reportportal_client.core.rp_requests import HttpRequest, RPFile, RPLogBatch, RPRequestLog from reportportal_client.core.worker import APIWorker from reportportal_client.logs import MAX_LOG_BATCH_PAYLOAD_SIZE, MAX_LOG_BATCH_SIZE @@ -115,7 +112,7 @@ def log(self, time, message=None, level=None, attachment=None, item_id=None): :param attachment: Attachments(images,files,etc.) :param item_id: parent item UUID """ - if item_id is NOT_FOUND: + if not item_id: logger.warning("Attempt to log to non-existent item") return rp_file = RPFile(**attachment) if attachment else None diff --git a/reportportal_client/steps/__init__.py b/reportportal_client/steps/__init__.py index 3eb535f..542b5bf 100644 --- a/reportportal_client/steps/__init__.py +++ b/reportportal_client/steps/__init__.py @@ -98,7 +98,7 @@ def start_nested_step( """ parent_id = self.client.current_item() if not parent_id: - return + return None return self.client.start_test_item( name, start_time, "step", has_stats=False, parameters=parameters, parent_item_id=parent_id ) diff --git a/setup.py b/setup.py index a6fa852..dcd57ab 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup -__version__ = "5.6.2" +__version__ = "5.6.3" TYPE_STUBS = ["*.pyi"] diff --git a/tests/conftest.py b/tests/conftest.py index 17b65a6..5000882 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,18 @@ from reportportal_client.client import RPClient +class DummyResponse: + # noinspection PyMethodMayBeStatic + def json(self): + return { + "id": "321-321-4321-321", + "message": "test", + } + + def ok(self): + return True + + @fixture def response(): """Cook up a mock for the Response with specific arguments.""" @@ -46,6 +58,8 @@ def rp_client(): """Prepare instance of the RPClient for testing.""" client = RPClient("http://endpoint", "project", "api_key") client.session = mock.Mock() + client.session.post.return_value = DummyResponse() + client.session.put.return_value = DummyResponse() client._skip_analytics = True return client diff --git a/tests/steps/conftest.py b/tests/steps/conftest.py index 83700ae..b8d393e 100644 --- a/tests/steps/conftest.py +++ b/tests/steps/conftest.py @@ -17,10 +17,13 @@ from pytest import fixture from reportportal_client.client import RPClient +from tests.conftest import DummyResponse @fixture def rp_client(): client = RPClient("http://endpoint", "project", "api_key") client.session = mock.Mock() + client.session.post.return_value = DummyResponse() + client.session.put.return_value = DummyResponse() return client diff --git a/tests/test_client.py b/tests/test_client.py index 55796e3..c78e125 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -17,6 +17,7 @@ # noinspection PyPackageRequirements import pytest +from conftest import DummyResponse from requests import Response from requests.exceptions import ReadTimeout @@ -38,7 +39,7 @@ def response_error(*args, **kwargs): def invalid_response(*args, **kwargs): result = Response() - result._content = "