trcli
Advanced tools
+1
-1
| Metadata-Version: 2.4 | ||
| Name: trcli | ||
| Version: 1.12.2 | ||
| Version: 1.12.3 | ||
| License-File: LICENSE.md | ||
@@ -5,0 +5,0 @@ Requires-Dist: click<8.2.2,>=8.1.0 |
+4
-4
@@ -36,3 +36,3 @@  | ||
| ``` | ||
| TestRail CLI v1.12.2 | ||
| TestRail CLI v1.12.3 | ||
| Copyright 2025 Gurock Software GmbH - www.gurock.com | ||
@@ -51,3 +51,3 @@ Supported and loaded modules: | ||
| $ trcli --help | ||
| TestRail CLI v1.12.1 | ||
| TestRail CLI v1.12.3 | ||
| Copyright 2025 Gurock Software GmbH - www.gurock.com | ||
@@ -1099,3 +1099,3 @@ Usage: trcli [OPTIONS] COMMAND [ARGS]... | ||
| $ trcli add_run --help | ||
| TestRail CLI v1.12.1 | ||
| TestRail CLI v1.12.3 | ||
| Copyright 2025 Gurock Software GmbH - www.gurock.com | ||
@@ -1224,3 +1224,3 @@ Usage: trcli add_run [OPTIONS] | ||
| $ trcli parse_openapi --help | ||
| TestRail CLI v1.12.1 | ||
| TestRail CLI v1.12.3 | ||
| Copyright 2025 Gurock Software GmbH - www.gurock.com | ||
@@ -1227,0 +1227,0 @@ Usage: trcli parse_openapi [OPTIONS] |
+55
-64
@@ -48,5 +48,3 @@ import pytest | ||
| Check that response was packed into APIClientResult, retry mechanism was not triggered.""" | ||
| requests_mock.get( | ||
| create_url("get_projects"), status_code=200, json=FAKE_PROJECT_DATA | ||
| ) | ||
| requests_mock.get(create_url("get_projects"), status_code=200, json=FAKE_PROJECT_DATA) | ||
| api_client = api_resources | ||
@@ -62,5 +60,3 @@ response = api_client.send_get("get_projects") | ||
| Check that response was packed into APIClientResult, retry mechanism was not triggered.""" | ||
| requests_mock.post( | ||
| create_url("add_project"), status_code=201, json=FAKE_PROJECT_DATA | ||
| ) | ||
| requests_mock.post(create_url("add_project"), status_code=201, json=FAKE_PROJECT_DATA) | ||
| api_client = api_resources | ||
@@ -87,5 +83,3 @@ response = api_client.send_post("add_project", FAKE_PROJECT_DATA) | ||
| check_calls_count(requests_mock) | ||
| check_response( | ||
| 400, INVALID_TEST_CASE_ERROR, INVALID_TEST_CASE_ERROR["error"], response | ||
| ) | ||
| check_response(400, INVALID_TEST_CASE_ERROR, INVALID_TEST_CASE_ERROR["error"], response) | ||
@@ -96,5 +90,3 @@ @pytest.mark.api_client | ||
| Check that response was packed into APIClientResult, retry mechanism was not triggered.""" | ||
| requests_mock.post( | ||
| create_url("add_project"), status_code=403, json=NO_PERMISSION_PROJECT_ERROR | ||
| ) | ||
| requests_mock.post(create_url("add_project"), status_code=403, json=NO_PERMISSION_PROJECT_ERROR) | ||
| api_client = api_resources | ||
@@ -113,5 +105,3 @@ response = api_client.send_post("add_project", {"fake_project_data": "data"}) | ||
| @pytest.mark.parametrize("retries, retry_after", [(4, "30"), (10, "60")]) | ||
| def test_retry_mechanism_too_many_requests( | ||
| self, retries, retry_after, api_resources_maker, requests_mock, mocker | ||
| ): | ||
| def test_retry_mechanism_too_many_requests(self, retries, retry_after, api_resources_maker, requests_mock, mocker): | ||
| """The purpose of this test is to check that retry mechanism will work as expected when | ||
@@ -169,2 +159,5 @@ 429 - too many requests will be received as an answer on get request.""" | ||
| f"url: https://FakeTestRail.io/index.php?/api/v2/get_projects\n" | ||
| f"headers:\n" | ||
| f" User-Agent: TRCLI\n" | ||
| f" Content-Type: application/json\n" | ||
| ), | ||
@@ -191,2 +184,5 @@ ] | ||
| f"url: https://FakeTestRail.io/index.php?/api/v2/get_projects\n" | ||
| f"headers:\n" | ||
| f" User-Agent: TRCLI\n" | ||
| f" Content-Type: application/json\n" | ||
| ) | ||
@@ -200,5 +196,3 @@ ] | ||
| "", | ||
| FAULT_MAPPING["unexpected_error_during_request_send"].format( | ||
| request=request | ||
| ), | ||
| FAULT_MAPPING["unexpected_error_during_request_send"].format(request=request), | ||
| response, | ||
@@ -247,3 +241,4 @@ ) | ||
| check_calls_count(requests_mock) | ||
| check_response(200, content, content, response) | ||
| expected_error_message = FAULT_MAPPING["invalid_json_response"].format(status_code=200, response_preview="Test") | ||
| check_response(200, str(content), expected_error_message, response) | ||
@@ -260,2 +255,5 @@ @pytest.mark.api_client | ||
| f"url: https://FakeTestRail.io/index.php?/api/v2/get_projects\n" | ||
| f"headers:\n" | ||
| f" User-Agent: TRCLI\n" | ||
| f" Content-Type: application/json\n" | ||
| f"response status code: 200\n" | ||
@@ -283,5 +281,3 @@ f"response body: ['test', 'list']\n" | ||
| ) | ||
| def test_timeout_is_parsed_and_validated( | ||
| self, timeout_value, expected_message, api_resources_maker, mocker | ||
| ): | ||
| def test_timeout_is_parsed_and_validated(self, timeout_value, expected_message, api_resources_maker, mocker): | ||
| environment = mocker.patch("trcli.cli.Environment") | ||
@@ -297,3 +293,3 @@ api_client = api_resources_maker(environment=environment, timeout=timeout_value) | ||
| @pytest.mark.api_client | ||
| @patch('requests.post') | ||
| @patch("requests.post") | ||
| def test_send_post_with_json_default(self, mock_post, api_resources_maker): | ||
@@ -317,18 +313,18 @@ """Test that send_post uses JSON by default""" | ||
| assert result.response_text == {"id": 1, "title": "Test"} | ||
| # Verify JSON was used | ||
| mock_post.assert_called_once() | ||
| call_args = mock_post.call_args | ||
| # Should use json parameter, not data | ||
| assert 'json' in call_args[1] | ||
| assert 'data' not in call_args[1] | ||
| assert call_args[1]['json'] == {":title": "Test Label"} | ||
| assert "json" in call_args[1] | ||
| assert "data" not in call_args[1] | ||
| assert call_args[1]["json"] == {":title": "Test Label"} | ||
| # Should have JSON content type header | ||
| headers = call_args[1]['headers'] | ||
| assert headers.get('Content-Type') == 'application/json' | ||
| headers = call_args[1]["headers"] | ||
| assert headers.get("Content-Type") == "application/json" | ||
| @pytest.mark.api_client | ||
| @patch('requests.post') | ||
| @patch("requests.post") | ||
| def test_send_post_with_form_data_true(self, mock_post, api_resources_maker): | ||
@@ -356,17 +352,17 @@ """Test that send_post uses form-data when as_form_data=True""" | ||
| call_args = mock_post.call_args | ||
| # Should use data parameter, not json | ||
| assert 'data' in call_args[1] | ||
| assert 'json' not in call_args[1] | ||
| assert call_args[1]['data'] == {":title": "Test Label"} | ||
| assert "data" in call_args[1] | ||
| assert "json" not in call_args[1] | ||
| assert call_args[1]["data"] == {":title": "Test Label"} | ||
| # Should NOT have files parameter (uses application/x-www-form-urlencoded) | ||
| assert 'files' not in call_args[1] or call_args[1]['files'] is None | ||
| assert "files" not in call_args[1] or call_args[1]["files"] is None | ||
| # Should NOT have JSON content type header when using form-data | ||
| headers = call_args[1]['headers'] | ||
| assert headers.get('Content-Type') != 'application/json' | ||
| headers = call_args[1]["headers"] | ||
| assert headers.get("Content-Type") != "application/json" | ||
| @pytest.mark.api_client | ||
| @patch('requests.post') | ||
| @patch("requests.post") | ||
| def test_send_post_with_form_data_false(self, mock_post, api_resources_maker): | ||
@@ -394,14 +390,14 @@ """Test that send_post uses JSON when as_form_data=False explicitly""" | ||
| call_args = mock_post.call_args | ||
| # Should use json parameter, not data | ||
| assert 'json' in call_args[1] | ||
| assert 'data' not in call_args[1] | ||
| assert call_args[1]['json'] == {":title": "Test Label"} | ||
| assert "json" in call_args[1] | ||
| assert "data" not in call_args[1] | ||
| assert call_args[1]["json"] == {":title": "Test Label"} | ||
| # Should have JSON content type header | ||
| headers = call_args[1]['headers'] | ||
| assert headers.get('Content-Type') == 'application/json' | ||
| headers = call_args[1]["headers"] | ||
| assert headers.get("Content-Type") == "application/json" | ||
| @pytest.mark.api_client | ||
| @patch('requests.post') | ||
| @patch("requests.post") | ||
| def test_send_post_with_files_and_form_data(self, mock_post, api_resources_maker): | ||
@@ -421,8 +417,3 @@ """Test that send_post handles files parameter with form-data""" | ||
| files = {"file1": "/path/to/file"} | ||
| result = api_client.send_post( | ||
| "test_endpoint", | ||
| {":title": "Test Label"}, | ||
| files=files, | ||
| as_form_data=True | ||
| ) | ||
| result = api_client.send_post("test_endpoint", {":title": "Test Label"}, files=files, as_form_data=True) | ||
@@ -436,12 +427,12 @@ # Verify the result | ||
| call_args = mock_post.call_args | ||
| # Should use data parameter, not json | ||
| assert 'data' in call_args[1] | ||
| assert 'json' not in call_args[1] | ||
| assert call_args[1]['data'] == {":title": "Test Label"} | ||
| assert "data" in call_args[1] | ||
| assert "json" not in call_args[1] | ||
| assert call_args[1]["data"] == {":title": "Test Label"} | ||
| # Files should be passed through as provided (not replaced with empty dict) | ||
| assert call_args[1]['files'] == files | ||
| assert call_args[1]["files"] == files | ||
| # Should NOT have JSON content type header when using files | ||
| headers = call_args[1]['headers'] | ||
| assert headers.get('Content-Type') != 'application/json' | ||
| headers = call_args[1]["headers"] | ||
| assert headers.get("Content-Type") != "application/json" |
| Metadata-Version: 2.4 | ||
| Name: trcli | ||
| Version: 1.12.2 | ||
| Version: 1.12.3 | ||
| License-File: LICENSE.md | ||
@@ -5,0 +5,0 @@ Requires-Dist: click<8.2.2,>=8.1.0 |
@@ -1,1 +0,1 @@ | ||
| __version__ = "1.12.2" | ||
| __version__ = "1.12.3" |
| import json | ||
| from pathlib import Path | ||
| import platform | ||
| import os | ||
| import base64 | ||
@@ -52,3 +55,4 @@ import requests | ||
| proxy_user: str = None, | ||
| noproxy: str = None, | ||
| noproxy: str = None, | ||
| uploader_metadata: str = None, | ||
| ): | ||
@@ -66,3 +70,4 @@ self.username = "" | ||
| self.proxy_user = proxy_user | ||
| self.noproxy = noproxy.split(',') if noproxy else [] | ||
| self.noproxy = noproxy.split(',') if noproxy else [] | ||
| self.uploader_metadata = uploader_metadata | ||
@@ -104,2 +109,3 @@ if not host_name.endswith("/"): | ||
| headers.update(self.__get_proxy_headers()) | ||
| headers.update(self.__get_uploader_metadata_headers()) | ||
| if files is None and not as_form_data: | ||
@@ -113,3 +119,3 @@ headers["Content-Type"] = "application/json" | ||
| verbose_log_message = APIClient.format_request_for_vlog( | ||
| method=method, url=url, payload=payload | ||
| method=method, url=url, payload=payload, headers=headers | ||
| ) | ||
@@ -184,4 +190,8 @@ if method == "POST": | ||
| except (JSONDecodeError, ValueError): | ||
| response_preview = response.content[:200].decode('utf-8', errors='ignore') | ||
| response_text = str(response.content) | ||
| error_message = response.content | ||
| error_message = FAULT_MAPPING["invalid_json_response"].format( | ||
| status_code=status_code, | ||
| response_preview=response_preview | ||
| ) | ||
| except AttributeError: | ||
@@ -217,2 +227,11 @@ error_message = "" | ||
| def __get_uploader_metadata_headers(self) -> Dict[str, str]: | ||
| """ | ||
| Returns headers for uploader metadata. | ||
| """ | ||
| headers = {} | ||
| if self.uploader_metadata: | ||
| headers["X-Uploader-Metadata"] = self.uploader_metadata | ||
| return headers | ||
| def _get_proxies_for_request(self, url: str) -> Dict[str, str]: | ||
@@ -286,8 +305,34 @@ """ | ||
| @staticmethod | ||
| def format_request_for_vlog(method: str, url: str, payload: dict): | ||
| return ( | ||
| def build_uploader_metadata(version: str) -> str: | ||
| """ | ||
| Build uploader metadata as base64-encoded JSON. | ||
| :param version: Application version | ||
| :returns: Base64-encoded metadata string | ||
| """ | ||
| data = { | ||
| "app_name": "trcli", | ||
| "app_version": version, | ||
| "os": platform.system().lower(), | ||
| "arch": platform.machine(), | ||
| "run_mode": "ci" if os.getenv("CI") else "other", | ||
| "container": os.path.exists("/.dockerenv"), | ||
| } | ||
| return base64.b64encode(json.dumps(data).encode()).decode() | ||
| @staticmethod | ||
| def format_request_for_vlog(method: str, url: str, payload: dict, headers: dict = None): | ||
| log_message = ( | ||
| f"\n**** API Call\n" | ||
| f"method: {method}\n" | ||
| f"url: {url}\n" + (f"payload: {payload}\n" if payload else "") | ||
| f"url: {url}\n" | ||
| ) | ||
| if headers: | ||
| log_message += "headers:\n" | ||
| for key, value in headers.items(): | ||
| log_message += f" {key}: {value}\n" | ||
| if payload: | ||
| log_message += f"payload: {payload}\n" | ||
| return log_message | ||
@@ -294,0 +339,0 @@ @staticmethod |
@@ -9,2 +9,3 @@ from beartype.typing import Callable, Optional, Tuple | ||
| from trcli.data_classes.dataclass_testrail import TestRailSuite | ||
| import trcli | ||
@@ -40,23 +41,21 @@ | ||
| proxy_user = self.environment.proxy_user | ||
| # Generate uploader metadata | ||
| uploader_metadata = APIClient.build_uploader_metadata(version=trcli.__version__) | ||
| # Build client configuration | ||
| client_kwargs = { | ||
| "verbose_logging_function": verbose_logging_function, | ||
| "logging_function": logging_function, | ||
| "verify": not self.environment.insecure, | ||
| "proxy": proxy, | ||
| "proxy_user": proxy_user, | ||
| "noproxy": noproxy, | ||
| "uploader_metadata": uploader_metadata | ||
| } | ||
| if self.environment.timeout: | ||
| api_client = APIClient( | ||
| self.environment.host, | ||
| verbose_logging_function=verbose_logging_function, | ||
| logging_function=logging_function, | ||
| timeout=self.environment.timeout, | ||
| verify=not self.environment.insecure, | ||
| proxy=proxy, | ||
| proxy_user=proxy_user, | ||
| noproxy=noproxy | ||
| ) | ||
| else: | ||
| api_client = APIClient( | ||
| self.environment.host, | ||
| logging_function=logging_function, | ||
| verbose_logging_function=verbose_logging_function, | ||
| verify=not self.environment.insecure, | ||
| proxy=proxy, | ||
| proxy_user=proxy_user, | ||
| noproxy=noproxy | ||
| ) | ||
| client_kwargs["timeout"] = self.environment.timeout | ||
| api_client = APIClient(self.environment.host, **client_kwargs) | ||
| api_client.username = self.environment.username | ||
@@ -63,0 +62,0 @@ api_client.password = self.environment.password |
@@ -68,3 +68,7 @@ import trcli | ||
| no_proxy_match_error= "The host {host} does not match any NO_PROXY rules. Ensure the correct domains or IP addresses are specified for bypassing the proxy.", | ||
| no_suites_found= "The project {project_id} does not have any suites." | ||
| no_suites_found= "The project {project_id} does not have any suites.", | ||
| invalid_json_response= "Received invalid response from TestRail server (HTTP {status_code}). " | ||
| "Please verify your TestRail host URL (-h) is correct and points to a valid TestRail instance. " | ||
| "Response preview: {response_preview}", | ||
| invalid_api_response= "Invalid response from TestRail API: {error_details}" | ||
| ) | ||
@@ -71,0 +75,0 @@ |
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
615347
0.38%11340
0.33%