From 19e4a353b7089bae4c2fc23ef6bae07fe660fb89 Mon Sep 17 00:00:00 2001 From: Josef Prochazka Date: Fri, 13 Dec 2024 15:07:22 +0100 Subject: [PATCH 1/4] Add error data to ApifyApiError Add tests. --- poetry.lock | 61 ++++++++++++++++++++------------ pyproject.toml | 5 ++- src/apify_client/_errors.py | 3 ++ tests/unit/test_client_errors.py | 49 +++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 tests/unit/test_client_errors.py diff --git a/poetry.lock b/poetry.lock index f2e7e975..37f4421d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -622,13 +622,13 @@ trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.28.1" +version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, ] [package.dependencies] @@ -636,6 +636,7 @@ anyio = "*" certifi = "*" httpcore = "==1.*" idna = "*" +sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] @@ -1399,6 +1400,20 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "respx" +version = "0.21.1" +description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." +optional = false +python-versions = ">=3.7" +files = [ + {file = "respx-0.21.1-py2.py3-none-any.whl", hash = "sha256:05f45de23f0c785862a2c92a3e173916e8ca88e4caad715dd5f68584d6053c20"}, + {file = "respx-0.21.1.tar.gz", hash = "sha256:0bd7fe21bfaa52106caa1223ce61224cf30786985f17c63c5d71eff0307ee8af"}, +] + +[package.dependencies] +httpx = ">=0.21.0" + [[package]] name = "rply" version = "0.7.8" @@ -1415,29 +1430,29 @@ appdirs = "*" [[package]] name = "ruff" -version = "0.8.2" +version = "0.8.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"}, - {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"}, - {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"}, - {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"}, - {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"}, - {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"}, - {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"}, + {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, + {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, + {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, + {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, + {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, + {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, + {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, ] [[package]] @@ -1783,4 +1798,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "cfd5f44f20520b0a1823e28cfb01de2191a3963d8bd27e8c2b85e063a7fe16a7" +content-hash = "00d32742a4023e022c1be322583c7ccc941d30366465e0b4bcf91d240a7db39f" diff --git a/pyproject.toml b/pyproject.toml index a684bdbb..9f30ebfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,12 +41,14 @@ keywords = [ [tool.poetry.dependencies] python = "^3.9" apify-shared = ">=1.1.2" -httpx = ">=0.25.0" more_itertools = ">=10.0.0" [tool.poetry.group.dev.dependencies] build = "~1.2.0" griffe = "~1.5.0" +# TODO: relax the upper bound once the issue is resolved: +# https://github.com/apify/apify-client-python/issues/313 +httpx = "~0.27.0" ipdb = "~0.13.0" mypy = "~1.13.0" pre-commit = "~4.0.0" @@ -58,6 +60,7 @@ pytest-only = "~2.1.0" pytest-timeout = "~2.3.0" pytest-xdist = "~3.6.0" redbaron = "~0.9.0" +respx = "^0.21.1" ruff = "~0.8.0" setuptools = "~75.6.0" # setuptools are used by pytest but not explicitly required diff --git a/src/apify_client/_errors.py b/src/apify_client/_errors.py index 8647e871..b7eee2cd 100644 --- a/src/apify_client/_errors.py +++ b/src/apify_client/_errors.py @@ -26,6 +26,7 @@ def __init__(self, response: httpx.Response, attempt: int) -> None: """ self.message: str | None = None self.type: str | None = None + self.data = dict[str, str]() self.message = f'Unexpected error: {response.text}' try: @@ -33,6 +34,8 @@ def __init__(self, response: httpx.Response, attempt: int) -> None: if 'error' in response_data: self.message = response_data['error']['message'] self.type = response_data['error']['type'] + if 'data' in response_data['error']: + self.data = response_data['error']['data'] except ValueError: pass diff --git a/tests/unit/test_client_errors.py b/tests/unit/test_client_errors.py new file mode 100644 index 00000000..e185cd6c --- /dev/null +++ b/tests/unit/test_client_errors.py @@ -0,0 +1,49 @@ +import json + +import httpx +import pytest +import respx +from respx import MockRouter + +from apify_client._errors import ApifyApiError +from apify_client._http_client import HTTPClient, HTTPClientAsync + +_TEST_URL = 'http://example.com' +_EXPECTED_MESSAGE = 'some_message' +_EXPECTED_TYPE = 'some_type' +_EXPECTED_DATA = { + 'invalidItems': {'0': ["should have required property 'name'"], '1': ["should have required property 'name'"]} +} + + +@respx.mock +@pytest.fixture +def mocked_response(respx_mock: MockRouter) -> None: + response_content = json.dumps( + {'error': {'message': _EXPECTED_MESSAGE, 'type': _EXPECTED_TYPE, 'data': _EXPECTED_DATA}} + ) + respx_mock.get(_TEST_URL).mock(return_value=httpx.Response(400, content=response_content)) + + +@pytest.mark.usefixtures('mocked_response') +def test_client_apify_api_error_with_data() -> None: + """Test that client correctly throws ApifyApiError with error data from response.""" + client = HTTPClient() + + with pytest.raises(ApifyApiError) as e: + client.call(method='GET', url=_TEST_URL) + + assert e.value.message == _EXPECTED_MESSAGE + assert e.value.type == _EXPECTED_TYPE + assert e.value.data == _EXPECTED_DATA + + +@pytest.mark.usefixtures('mocked_response') +async def test_async_client_apify_api_error_with_data() -> None: + """Test that async client correctly throws ApifyApiError with error data from response.""" + client = HTTPClientAsync() + with pytest.raises(ApifyApiError) as e: + await client.call(method='GET', url=_TEST_URL) + assert e.value.message == _EXPECTED_MESSAGE + assert e.value.type == _EXPECTED_TYPE + assert e.value.data == _EXPECTED_DATA From 6b811c073583fa5c7b66c81b345e07e2f6117e7e Mon Sep 17 00:00:00 2001 From: Josef Prochazka Date: Fri, 13 Dec 2024 15:10:46 +0100 Subject: [PATCH 2/4] Pyproject.toml --- poetry.lock | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 37f4421d..5d0f1299 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1798,4 +1798,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "00d32742a4023e022c1be322583c7ccc941d30366465e0b4bcf91d240a7db39f" +content-hash = "39c46c00ae2788e010f790e761090a3092208e243057638be2027835ef704a6f" diff --git a/pyproject.toml b/pyproject.toml index 9f30ebfc..d6be1ee0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,14 +41,14 @@ keywords = [ [tool.poetry.dependencies] python = "^3.9" apify-shared = ">=1.1.2" +# TODO: relax the upper bound once the issue is resolved: +# https://github.com/apify/apify-client-python/issues/313 +httpx = "~0.27.0" more_itertools = ">=10.0.0" [tool.poetry.group.dev.dependencies] build = "~1.2.0" griffe = "~1.5.0" -# TODO: relax the upper bound once the issue is resolved: -# https://github.com/apify/apify-client-python/issues/313 -httpx = "~0.27.0" ipdb = "~0.13.0" mypy = "~1.13.0" pre-commit = "~4.0.0" From 83e272f2d267efae6b495b630cce09af9371928b Mon Sep 17 00:00:00 2001 From: Josef Prochazka Date: Mon, 16 Dec 2024 07:31:52 +0100 Subject: [PATCH 3/4] Replace usefixtures by autouse --- tests/unit/test_client_errors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_client_errors.py b/tests/unit/test_client_errors.py index e185cd6c..76921e0e 100644 --- a/tests/unit/test_client_errors.py +++ b/tests/unit/test_client_errors.py @@ -17,7 +17,7 @@ @respx.mock -@pytest.fixture +@pytest.fixture(autouse=True) def mocked_response(respx_mock: MockRouter) -> None: response_content = json.dumps( {'error': {'message': _EXPECTED_MESSAGE, 'type': _EXPECTED_TYPE, 'data': _EXPECTED_DATA}} @@ -25,7 +25,6 @@ def mocked_response(respx_mock: MockRouter) -> None: respx_mock.get(_TEST_URL).mock(return_value=httpx.Response(400, content=response_content)) -@pytest.mark.usefixtures('mocked_response') def test_client_apify_api_error_with_data() -> None: """Test that client correctly throws ApifyApiError with error data from response.""" client = HTTPClient() @@ -38,12 +37,13 @@ def test_client_apify_api_error_with_data() -> None: assert e.value.data == _EXPECTED_DATA -@pytest.mark.usefixtures('mocked_response') async def test_async_client_apify_api_error_with_data() -> None: """Test that async client correctly throws ApifyApiError with error data from response.""" client = HTTPClientAsync() + with pytest.raises(ApifyApiError) as e: await client.call(method='GET', url=_TEST_URL) + assert e.value.message == _EXPECTED_MESSAGE assert e.value.type == _EXPECTED_TYPE assert e.value.data == _EXPECTED_DATA From 43161eba5edffbb974bef9c311b14fd3df487855 Mon Sep 17 00:00:00 2001 From: Josef Prochazka Date: Mon, 16 Dec 2024 15:27:25 +0100 Subject: [PATCH 4/4] Not so restrictive bounds for httpx --- poetry.lock | 18 +++++++++--------- pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5d0f1299..035ba1d3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -149,13 +149,13 @@ virtualenv = ["virtualenv (>=20.0.35)"] [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -1218,20 +1218,20 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.24.0" +version = "0.25.0" description = "Pytest support for asyncio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, - {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, + {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"}, + {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"}, ] [package.dependencies] pytest = ">=8.2,<9" [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] @@ -1798,4 +1798,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "39c46c00ae2788e010f790e761090a3092208e243057638be2027835ef704a6f" +content-hash = "897734209ab1277e46c39271e3d00992626214544da868dbadef133b07122566" diff --git a/pyproject.toml b/pyproject.toml index c02b7333..1eeb3c06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ python = "^3.9" apify-shared = ">=1.1.2" # TODO: relax the upper bound once the issue is resolved: # https://github.com/apify/apify-client-python/issues/313 -httpx = "~0.27.0" +httpx = ">=0.25 <0.28.0" more_itertools = ">=10.0.0" [tool.poetry.group.dev.dependencies]