diff --git a/pyproject.toml b/pyproject.toml index bd3bd492..a53c974c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,13 +71,15 @@ dev = [ "pydoc-markdown~=4.8.0", "pytest-asyncio~=1.1.0", "pytest-cov~=6.2.0", + "pytest-httpserver>=1.1.3", "pytest-timeout>=2.4.0", "pytest-xdist~=3.8.0", "pytest~=8.4.0", - "respx~=0.22.0", "ruff~=0.12.0", "setuptools", # setuptools are used by pytest but not explicitly required "uvicorn[standard]", + "werkzeug~=3.0.0", # Werkzeug is used by httpserver + "yarl~=1.20.0", # yarl is used by crawlee ] [tool.hatch.build.targets.wheel] diff --git a/tests/unit/actor/test_actor_create_proxy_configuration.py b/tests/unit/actor/test_actor_create_proxy_configuration.py index de98d047..a079ba38 100644 --- a/tests/unit/actor/test_actor_create_proxy_configuration.py +++ b/tests/unit/actor/test_actor_create_proxy_configuration.py @@ -1,8 +1,8 @@ from __future__ import annotations from typing import TYPE_CHECKING +from unittest.mock import Mock -import httpx import pytest from apify_client import ApifyClientAsync @@ -11,7 +11,8 @@ from apify import Actor if TYPE_CHECKING: - from respx import MockRouter + from pytest_httpserver import HTTPServer + from werkzeug import Request, Response from ..conftest import ApifyClientAsyncPatcher @@ -24,25 +25,29 @@ def patched_apify_client(apify_client_async_patcher: ApifyClientAsyncPatcher) -> return ApifyClientAsync() +@pytest.mark.usefixtures('patched_httpx_client') async def test_basic_proxy_configuration_creation( monkeypatch: pytest.MonkeyPatch, - respx_mock: MockRouter, + httpserver: HTTPServer, patched_apify_client: ApifyClientAsync, ) -> None: - dummy_proxy_status_url = 'http://dummy-proxy-status-url.com' + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') monkeypatch.setenv(ApifyEnvVars.TOKEN.value, 'DUMMY_TOKEN') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) - route = respx_mock.get(dummy_proxy_status_url) - route.mock( - httpx.Response( - 200, - json={ - 'connected': True, - 'connectionError': None, - 'isManInTheMiddle': True, - }, - ) + call_mock = Mock() + + def request_handler(request: Request, response: Response) -> Response: + call_mock(request.url) + return response + + httpserver.expect_oneshot_request('/').with_post_hook(request_handler).respond_with_json( + { + 'connected': True, + 'connectionError': None, + 'isManInTheMiddle': True, + }, + status=200, ) groups = ['GROUP1', 'GROUP2'] @@ -58,32 +63,36 @@ async def test_basic_proxy_configuration_creation( assert proxy_configuration._country_code == country_code assert len(patched_apify_client.calls['user']['get']) == 1 # type: ignore[attr-defined] - assert len(route.calls) == 1 + assert call_mock.call_count == 1 await Actor.exit() +@pytest.mark.usefixtures('patched_httpx_client') async def test_proxy_configuration_with_actor_proxy_input( monkeypatch: pytest.MonkeyPatch, - respx_mock: MockRouter, + httpserver: HTTPServer, patched_apify_client: ApifyClientAsync, ) -> None: - dummy_proxy_status_url = 'http://dummy-proxy-status-url.com' + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') dummy_proxy_url = 'http://dummy-proxy.com:8000' monkeypatch.setenv(ApifyEnvVars.TOKEN.value, 'DUMMY_TOKEN') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) - route = respx_mock.get(dummy_proxy_status_url) - route.mock( - httpx.Response( - 200, - json={ - 'connected': True, - 'connectionError': None, - 'isManInTheMiddle': True, - }, - ) + call_mock = Mock() + + def request_handler(request: Request, response: Response) -> Response: + call_mock(request.url) + return response + + httpserver.expect_request('/').with_post_hook(request_handler).respond_with_json( + { + 'connected': True, + 'connectionError': None, + 'isManInTheMiddle': True, + }, + status=200, ) await Actor.init() @@ -138,6 +147,6 @@ async def test_proxy_configuration_with_actor_proxy_input( ) assert len(patched_apify_client.calls['user']['get']) == 2 # type: ignore[attr-defined] - assert len(route.calls) == 2 + assert call_mock.call_count == 2 await Actor.exit() diff --git a/tests/unit/actor/test_request_list.py b/tests/unit/actor/test_request_list.py index 9efcdce7..bcc60578 100644 --- a/tests/unit/actor/test_request_list.py +++ b/tests/unit/actor/test_request_list.py @@ -2,17 +2,21 @@ import re from dataclasses import dataclass -from typing import Any, get_args +from typing import TYPE_CHECKING, Any, get_args +from unittest.mock import Mock import pytest -import respx -from httpx import Response +from yarl import URL from crawlee._request import UserData from crawlee._types import HttpMethod from apify.storages._request_list import URL_NO_COMMAS_REGEX, RequestList +if TYPE_CHECKING: + from pytest_httpserver import HTTPServer + from werkzeug import Request, Response + @pytest.mark.parametrize( argnames='request_method', @@ -67,20 +71,19 @@ async def test_request_list_open_request_types( assert request.headers.root == optional_input.get('headers', {}) -@respx.mock -async def test_request_list_open_from_url_correctly_send_requests() -> None: +async def test_request_list_open_from_url_correctly_send_requests(httpserver: HTTPServer) -> None: """Test that requests are sent to expected urls.""" request_list_sources_input: list[dict[str, Any]] = [ { - 'requestsFromUrl': 'https://abc.dev/file.txt', + 'requestsFromUrl': httpserver.url_for('/file.txt'), 'method': 'GET', }, { - 'requestsFromUrl': 'https://www.abc.dev/file2', + 'requestsFromUrl': httpserver.url_for('/file2'), 'method': 'PUT', }, { - 'requestsFromUrl': 'https://www.something.som', + 'requestsFromUrl': httpserver.url_for('/something'), 'method': 'POST', 'headers': {'key': 'value'}, 'payload': 'some_payload', @@ -88,16 +91,28 @@ async def test_request_list_open_from_url_correctly_send_requests() -> None: }, ] - routes = [respx.get(entry['requestsFromUrl']) for entry in request_list_sources_input] + routes: dict[str, Mock] = {} + + def request_handler(request: Request, response: Response) -> Response: + routes[request.url]() + return response + + for entry in request_list_sources_input: + path = str(URL(entry['requestsFromUrl']).path) + httpserver.expect_oneshot_request(path).with_post_hook(request_handler).respond_with_data(status=200) + routes[entry['requestsFromUrl']] = Mock() await RequestList.open(request_list_sources_input=request_list_sources_input) - for route in routes: - assert route.called + assert len(routes) == len(request_list_sources_input) + for entity in request_list_sources_input: + entity_url = entity['requestsFromUrl'] + assert entity_url in routes + assert routes[entity_url].called -@respx.mock -async def test_request_list_open_from_url() -> None: + +async def test_request_list_open_from_url(httpserver: HTTPServer) -> None: """Test that create_request_list is correctly reading urls from remote url sources and also from simple input.""" expected_simple_url = 'https://www.someurl.com' expected_remote_urls_1 = {'http://www.something.com', 'https://www.somethingelse.com', 'http://www.bla.net'} @@ -111,11 +126,11 @@ class MockedUrlInfo: mocked_urls = ( MockedUrlInfo( - 'https://abc.dev/file.txt', + httpserver.url_for('/file.txt'), 'blablabla{} more blablabla{} , even more blablabla. {} '.format(*expected_remote_urls_1), ), MockedUrlInfo( - 'https://www.abc.dev/file2', + httpserver.url_for('/file2'), 'some stuff{} more stuff{} www.false_positive.com'.format(*expected_remote_urls_2), ), ) @@ -132,7 +147,8 @@ class MockedUrlInfo: }, ] for mocked_url in mocked_urls: - respx.get(mocked_url.url).mock(return_value=Response(200, text=mocked_url.response_text)) + path = str(URL(mocked_url.url).path) + httpserver.expect_oneshot_request(path).respond_with_data(status=200, response_data=mocked_url.response_text) request_list = await RequestList.open(request_list_sources_input=request_list_sources_input) generated_requests = [] @@ -143,23 +159,20 @@ class MockedUrlInfo: assert {generated_request.url for generated_request in generated_requests} == expected_urls -@respx.mock -async def test_request_list_open_from_url_additional_inputs() -> None: +async def test_request_list_open_from_url_additional_inputs(httpserver: HTTPServer) -> None: """Test that all generated request properties are correctly populated from input values.""" expected_url = 'https://www.someurl.com' example_start_url_input: dict[str, Any] = { - 'requestsFromUrl': 'https://crawlee.dev/file.txt', + 'requestsFromUrl': httpserver.url_for('/file.txt'), 'method': 'POST', 'headers': {'key': 'value'}, 'payload': 'some_payload', 'userData': {'another_key': 'another_value'}, } - - respx.get(example_start_url_input['requestsFromUrl']).mock(return_value=Response(200, text=expected_url)) + httpserver.expect_oneshot_request('/file.txt').respond_with_data(status=200, response_data=expected_url) request_list = await RequestList.open(request_list_sources_input=[example_start_url_input]) request = await request_list.fetch_next_request() - # Check all properties correctly created for request assert request assert request.url == expected_url diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 929173ea..28a1e460 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -4,9 +4,12 @@ import inspect import os from collections import defaultdict +from logging import getLogger from typing import TYPE_CHECKING, Any, get_type_hints +import httpx import pytest +from pytest_httpserver import HTTPServer from apify_client import ApifyClientAsync from apify_shared.consts import ApifyEnvVars @@ -18,7 +21,7 @@ import apify._actor if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Iterator from pathlib import Path @@ -187,3 +190,37 @@ def memory_storage_client() -> MemoryStorageClient: configuration.write_metadata = True return MemoryStorageClient.from_config(configuration) + + +@pytest.fixture(scope='session') +def make_httpserver() -> Iterator[HTTPServer]: + werkzeug_logger = getLogger('werkzeug') + werkzeug_logger.disabled = True + + server = HTTPServer(threaded=True, host='127.0.0.1') + server.start() + yield server + server.clear() # type: ignore[no-untyped-call] + if server.is_running(): + server.stop() # type: ignore[no-untyped-call] + + +@pytest.fixture +def httpserver(make_httpserver: HTTPServer) -> Iterator[HTTPServer]: + server = make_httpserver + yield server + server.clear() # type: ignore[no-untyped-call] + + +@pytest.fixture +def patched_httpx_client(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: + """Patch httpx client to drop proxy settings.""" + + class ProxylessAsyncClient(httpx.AsyncClient): + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs.pop('proxy', None) + super().__init__(*args, **kwargs) + + monkeypatch.setattr(httpx, 'AsyncClient', ProxylessAsyncClient) + yield + monkeypatch.undo() diff --git a/tests/unit/test_proxy_configuration.py b/tests/unit/test_proxy_configuration.py index 96eb0544..c785a832 100644 --- a/tests/unit/test_proxy_configuration.py +++ b/tests/unit/test_proxy_configuration.py @@ -6,8 +6,8 @@ import re from dataclasses import asdict from typing import TYPE_CHECKING, Any +from unittest.mock import Mock -import httpx import pytest from apify_client import ApifyClientAsync @@ -16,7 +16,8 @@ from apify._proxy_configuration import ProxyConfiguration, is_url if TYPE_CHECKING: - from respx import MockRouter + from pytest_httpserver import HTTPServer + from werkzeug import Request, Response from .conftest import ApifyClientAsyncPatcher @@ -370,25 +371,29 @@ async def test_new_proxy_info_rotating_urls_with_sessions() -> None: assert proxy_info.url == proxy_urls[0] +@pytest.mark.usefixtures('patched_httpx_client') async def test_initialize_with_valid_configuration( monkeypatch: pytest.MonkeyPatch, - respx_mock: MockRouter, + httpserver: HTTPServer, patched_apify_client: ApifyClientAsync, ) -> None: - dummy_proxy_status_url = 'http://dummy-proxy-status-url.com' + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') monkeypatch.setenv(ApifyEnvVars.TOKEN.value, 'DUMMY_TOKEN') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) - route = respx_mock.get(dummy_proxy_status_url) - route.mock( - httpx.Response( - 200, - json={ - 'connected': True, - 'connectionError': None, - 'isManInTheMiddle': True, - }, - ) + call_mock = Mock() + + def request_handler(request: Request, response: Response) -> Response: + call_mock(request.url) + return response + + httpserver.expect_oneshot_request('/').with_post_hook(request_handler).respond_with_json( + { + 'connected': True, + 'connectionError': None, + 'isManInTheMiddle': True, + }, + status=200, ) proxy_configuration = ProxyConfiguration(_apify_client=patched_apify_client) @@ -399,7 +404,7 @@ async def test_initialize_with_valid_configuration( assert proxy_configuration.is_man_in_the_middle is True assert len(patched_apify_client.calls['user']['get']) == 1 # type: ignore[attr-defined] - assert len(route.calls) == 1 + assert call_mock.call_count == 1 async def test_initialize_without_password_or_token() -> None: @@ -409,19 +414,18 @@ async def test_initialize_without_password_or_token() -> None: await proxy_configuration.initialize() -async def test_initialize_with_manual_password(monkeypatch: pytest.MonkeyPatch, respx_mock: MockRouter) -> None: - dummy_proxy_status_url = 'http://dummy-proxy-status-url.com' +@pytest.mark.usefixtures('patched_httpx_client') +async def test_initialize_with_manual_password(monkeypatch: pytest.MonkeyPatch, httpserver: HTTPServer) -> None: + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) - respx_mock.get(dummy_proxy_status_url).mock( - httpx.Response( - 200, - json={ - 'connected': True, - 'connectionError': None, - 'isManInTheMiddle': False, - }, - ) + httpserver.expect_oneshot_request('/').respond_with_json( + { + 'connected': True, + 'connectionError': None, + 'isManInTheMiddle': False, + }, + status=200, ) proxy_configuration = ProxyConfiguration(password=DUMMY_PASSWORD) @@ -432,24 +436,23 @@ async def test_initialize_with_manual_password(monkeypatch: pytest.MonkeyPatch, assert proxy_configuration.is_man_in_the_middle is False +@pytest.mark.usefixtures('patched_httpx_client') async def test_initialize_prefering_password_from_env_over_calling_api( monkeypatch: pytest.MonkeyPatch, - respx_mock: MockRouter, + httpserver: HTTPServer, patched_apify_client: ApifyClientAsync, ) -> None: - dummy_proxy_status_url = 'http://dummy-proxy-status-url.com' + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) monkeypatch.setenv(ApifyEnvVars.PROXY_PASSWORD.value, DUMMY_PASSWORD) - respx_mock.get(dummy_proxy_status_url).mock( - httpx.Response( - 200, - json={ - 'connected': True, - 'connectionError': None, - 'isManInTheMiddle': False, - }, - ) + httpserver.expect_oneshot_request('/').respond_with_json( + { + 'connected': True, + 'connectionError': None, + 'isManInTheMiddle': False, + }, + status=200, ) proxy_configuration = ProxyConfiguration() @@ -462,28 +465,27 @@ async def test_initialize_prefering_password_from_env_over_calling_api( assert len(patched_apify_client.calls['user']['get']) == 0 # type: ignore[attr-defined] +@pytest.mark.usefixtures('patched_httpx_client') @pytest.mark.skip(reason='There are issues with log propagation to caplog, see issue #462.') async def test_initialize_with_manual_password_different_than_user_one( monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, - respx_mock: MockRouter, + httpserver: HTTPServer, patched_apify_client: ApifyClientAsync, ) -> None: - dummy_proxy_status_url = 'http://dummy-proxy-status-url.com' + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') different_dummy_password = 'DIFFERENT_DUMMY_PASSWORD' monkeypatch.setenv(ApifyEnvVars.TOKEN.value, 'DUMMY_TOKEN') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) monkeypatch.setenv(ApifyEnvVars.PROXY_PASSWORD.value, different_dummy_password) - respx_mock.get(dummy_proxy_status_url).mock( - httpx.Response( - 200, - json={ - 'connected': True, - 'connectionError': None, - 'isManInTheMiddle': True, - }, - ) + httpserver.expect_oneshot_request('/').respond_with_json( + { + 'connected': True, + 'connectionError': None, + 'isManInTheMiddle': True, + }, + status=200, ) proxy_configuration = ProxyConfiguration(_apify_client=patched_apify_client) @@ -498,19 +500,18 @@ async def test_initialize_with_manual_password_different_than_user_one( assert 'The Apify Proxy password you provided belongs to a different user' in caplog.records[0].message -async def test_initialize_when_not_connected(monkeypatch: pytest.MonkeyPatch, respx_mock: MockRouter) -> None: +@pytest.mark.usefixtures('patched_httpx_client') +async def test_initialize_when_not_connected(monkeypatch: pytest.MonkeyPatch, httpserver: HTTPServer) -> None: dummy_connection_error = 'DUMMY_CONNECTION_ERROR' - dummy_proxy_status_url = 'http://dummy-proxy-status-url.com' + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) - respx_mock.get(dummy_proxy_status_url).mock( - httpx.Response( - 200, - json={ - 'connected': False, - 'connectionError': dummy_connection_error, - }, - ) + httpserver.expect_oneshot_request('/').respond_with_json( + { + 'connected': False, + 'connectionError': dummy_connection_error, + }, + status=200, ) proxy_configuration = ProxyConfiguration(password=DUMMY_PASSWORD) @@ -521,14 +522,12 @@ async def test_initialize_when_not_connected(monkeypatch: pytest.MonkeyPatch, re @pytest.mark.skip(reason='There are issues with log propagation to caplog, see issue #462.') async def test_initialize_when_status_page_unavailable( - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, - respx_mock: MockRouter, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, httpserver: HTTPServer ) -> None: - dummy_proxy_status_url = 'http://dummy-proxy-status-url.com' + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) - respx_mock.get(dummy_proxy_status_url).mock(httpx.Response(500)) + httpserver.expect_oneshot_request('/').respond_with_data(status=500) proxy_configuration = ProxyConfiguration(password=DUMMY_PASSWORD) @@ -541,21 +540,26 @@ async def test_initialize_when_status_page_unavailable( async def test_initialize_with_non_apify_proxy( monkeypatch: pytest.MonkeyPatch, - respx_mock: MockRouter, + httpserver: HTTPServer, patched_apify_client: ApifyClientAsync, ) -> None: - dummy_proxy_status_url = 'http://dummy-proxy-status-url.com' + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) - route = respx_mock.get(dummy_proxy_status_url) - route.mock(httpx.Response(200)) + call_mock = Mock() + + def request_handler(request: Request, response: Response) -> Response: + call_mock(request.url) + return response + + httpserver.expect_oneshot_request('/').with_post_hook(request_handler).respond_with_data(status=200) proxy_configuration = ProxyConfiguration(proxy_urls=['http://dummy-proxy.com:8000']) await proxy_configuration.initialize() assert len(patched_apify_client.calls['user']['get']) == 0 # type: ignore[attr-defined] - assert len(route.calls) == 0 + assert call_mock.call_count == 0 def test_is_url_validation() -> None: diff --git a/uv.lock b/uv.lock index 9436c16e..073930e4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" [[package]] @@ -28,7 +28,7 @@ wheels = [ [[package]] name = "apify" -version = "2.7.3" +version = "2.7.1" source = { editable = "." } dependencies = [ { name = "apify-client" }, @@ -59,12 +59,14 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-httpserver" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, - { name = "respx" }, { name = "ruff" }, { name = "setuptools" }, { name = "uvicorn", extra = ["standard"] }, + { name = "werkzeug" }, + { name = "yarl" }, ] [package.metadata] @@ -94,12 +96,14 @@ dev = [ { name = "pytest", specifier = "~=8.4.0" }, { name = "pytest-asyncio", specifier = "~=1.1.0" }, { name = "pytest-cov", specifier = "~=6.2.0" }, + { name = "pytest-httpserver", specifier = ">=1.1.3" }, { name = "pytest-timeout", specifier = ">=2.4.0" }, { name = "pytest-xdist", specifier = "~=3.8.0" }, - { name = "respx", specifier = "~=0.22.0" }, { name = "ruff", specifier = "~=0.12.0" }, { name = "setuptools" }, { name = "uvicorn", extras = ["standard"] }, + { name = "werkzeug", specifier = "~=3.0.0" }, + { name = "yarl", specifier = "~=1.20.0" }, ] [[package]] @@ -1872,6 +1876,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] +[[package]] +name = "pytest-httpserver" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/d8/def15ba33bd696dd72dd4562a5287c0cba4d18a591eeb82e0b08ab385afc/pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec", size = 68870, upload-time = "2025-04-10T08:17:15.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/d2/dfc2f25f3905921c2743c300a48d9494d29032f1389fc142e718d6978fb2/pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9", size = 21000, upload-time = "2025-04-10T08:17:13.906Z" }, +] + [[package]] name = "pytest-timeout" version = "2.4.0" @@ -1986,18 +2002,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/25/dd878a121fcfdf38f52850f11c512e13ec87c2ea72385933818e5b6c15ce/requests_file-2.1.0-py2.py3-none-any.whl", hash = "sha256:cf270de5a4c5874e84599fc5778303d496c10ae5e870bfa378818f35d21bda5c", size = 4244, upload-time = "2024-05-21T16:27:57.733Z" }, ] -[[package]] -name = "respx" -version = "0.22.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, -] - [[package]] name = "ruff" version = "0.12.7" @@ -2500,6 +2504,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] +[[package]] +name = "werkzeug" +version = "3.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/f9/0ba83eaa0df9b9e9d1efeb2ea351d0677c37d41ee5d0f91e98423c7281c9/werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d", size = 805170, upload-time = "2024-10-25T18:52:31.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/69/05837f91dfe42109203ffa3e488214ff86a6d68b2ed6c167da6cdc42349b/werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17", size = 227979, upload-time = "2024-10-25T18:52:30.129Z" }, +] + [[package]] name = "wrapt" version = "1.17.2"