diff --git a/pyproject.toml b/pyproject.toml index 5d83674..69bb913 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: GIS", ] dependencies = [ - "httpx>=0.24.0", + "httpx2>=2.0.0", ] [project.optional-dependencies] @@ -31,7 +31,6 @@ dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", "pytest-mock>=3.10.0", - "pytest-httpx>=0.27.0", "black>=23.0.0", "isort>=5.12.0", "flake8>=6.0.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index 50ab9bd..e61553d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,6 @@ pytest>=7.0.0 pytest-cov>=4.0.0 pytest-mock>=3.10.0 -pytest-httpx>=0.27.0 black>=23.0.0 isort>=5.12.0 flake8>=6.0.0 diff --git a/src/geocodio/client.py b/src/geocodio/client.py index 592e6b8..a000716 100644 --- a/src/geocodio/client.py +++ b/src/geocodio/client.py @@ -9,7 +9,7 @@ import os from typing import Dict, List, Optional, Tuple, Union -import httpx +import httpx2 as httpx from geocodio._version import __version__ diff --git a/src/geocodio/models.py b/src/geocodio/models.py index e2bc483..f8a2c7d 100644 --- a/src/geocodio/models.py +++ b/src/geocodio/models.py @@ -8,7 +8,7 @@ from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar -import httpx +import httpx2 as httpx T = TypeVar("T", bound="ExtrasMixin") diff --git a/tests/conftest.py b/tests/conftest.py index ec8ee82..2ea9272 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,10 +6,82 @@ import os import pytest +import httpx2 as httpx from dotenv import load_dotenv from geocodio import Geocodio + +class _HttpxMock: + """Simple compatibility replacement for pytest-httpx's httpx_mock fixture.""" + + def __init__(self): + self._callbacks = [] + + def add_callback(self, callback=None, **kwargs): + if callback is None: + raise ValueError("callback is required") + self._callbacks.append(callback) + + def add_response( + self, + *, + url=None, + match_headers=None, + status_code=200, + headers=None, + content=None, + json=None, + text=None, + ): + self._callbacks.append( + lambda request: httpx.Response( + status_code, + headers=headers, + content=content, + json=json, + text=text, + ) + ) + + def _assert_options(self): + assert not self._callbacks, ( + "The following responses are mocked but not requested: " + f"{self._callbacks}" + ) + + +@pytest.fixture +def httpx_mock(monkeypatch): + """Patch httpx2 transport methods to satisfy the tests' httpx_mock API.""" + mock = _HttpxMock() + real_handle_request = httpx.HTTPTransport.handle_request + + def mocked_handle_request(transport, request): + if not mock._callbacks: + return real_handle_request(transport, request) + callback = mock._callbacks.pop(0) + return callback(request) + + monkeypatch.setattr(httpx.HTTPTransport, "handle_request", mocked_handle_request) + + real_handle_async_request = httpx.AsyncHTTPTransport.handle_async_request + + async def mocked_handle_async_request(transport, request): + if not mock._callbacks: + return await real_handle_async_request(transport, request) + callback = mock._callbacks.pop(0) + return callback(request) + + monkeypatch.setattr( + httpx.AsyncHTTPTransport, + "handle_async_request", + mocked_handle_async_request, + ) + + yield mock + mock._assert_options() + # Load environment variables from .env file load_dotenv() diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index c4dbe67..a811f47 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -2,7 +2,7 @@ Tests for the Geocodio class """ -import httpx +import httpx2 as httpx import pytest from geocodio import Geocodio @@ -231,8 +231,8 @@ def test_user_agent_header_in_request(mocker): """Test that the User-Agent header is included in all requests.""" from geocodio import __version__ - # Mock the httpx.Client.request method to capture headers - mock_httpx_request = mocker.patch("httpx.Client.request") + # Mock the httpx2.Client.request method to capture headers + mock_httpx_request = mocker.patch("httpx2.Client.request") mock_httpx_request.return_value = httpx.Response( 200, json={ @@ -281,8 +281,8 @@ def test_user_agent_header_in_batch_request(mocker): """Test that the User-Agent header is included in batch requests.""" from geocodio import __version__ - # Mock the httpx.Client.request method - mock_httpx_request = mocker.patch("httpx.Client.request") + # Mock the httpx2.Client.request method + mock_httpx_request = mocker.patch("httpx2.Client.request") mock_httpx_request.return_value = httpx.Response(200, json={"results": []}) client = Geocodio("test-api-key") @@ -300,8 +300,8 @@ def test_user_agent_header_in_reverse_geocode(mocker): """Test that the User-Agent header is included in reverse geocoding requests.""" from geocodio import __version__ - # Mock the httpx.Client.request method - mock_httpx_request = mocker.patch("httpx.Client.request") + # Mock the httpx2.Client.request method + mock_httpx_request = mocker.patch("httpx2.Client.request") mock_httpx_request.return_value = httpx.Response( 200, json={ @@ -338,8 +338,8 @@ def test_user_agent_header_in_list_api(mocker): """Test that the User-Agent header is included in List API requests.""" from geocodio import __version__ - # Mock the httpx.Client.request method - mock_httpx_request = mocker.patch("httpx.Client.request") + # Mock the httpx2.Client.request method + mock_httpx_request = mocker.patch("httpx2.Client.request") mock_httpx_request.return_value = httpx.Response( 200, json={ diff --git a/tests/unit/test_distance.py b/tests/unit/test_distance.py index fdde559..4ee6962 100644 --- a/tests/unit/test_distance.py +++ b/tests/unit/test_distance.py @@ -4,7 +4,7 @@ import json -import httpx +import httpx2 as httpx import pytest from geocodio import ( diff --git a/tests/unit/test_errors.py b/tests/unit/test_errors.py index 28a8d12..2773e97 100644 --- a/tests/unit/test_errors.py +++ b/tests/unit/test_errors.py @@ -1,4 +1,4 @@ -import httpx +import httpx2 as httpx import pytest from geocodio.exceptions import ( diff --git a/tests/unit/test_geocode.py b/tests/unit/test_geocode.py index abf51cc..80581d6 100644 --- a/tests/unit/test_geocode.py +++ b/tests/unit/test_geocode.py @@ -1,7 +1,7 @@ import json from pathlib import Path -import httpx +import httpx2 as httpx from geocodio.models import AddressComponents, GeocodingResponse diff --git a/tests/unit/test_reverse.py b/tests/unit/test_reverse.py index d61ae52..cb0c701 100644 --- a/tests/unit/test_reverse.py +++ b/tests/unit/test_reverse.py @@ -1,4 +1,4 @@ -import httpx +import httpx2 as httpx import pytest from geocodio.models import GeocodingResponse, Location diff --git a/uv.lock b/uv.lock index ea63301..d7a2795 100644 --- a/uv.lock +++ b/uv.lock @@ -1,19 +1,22 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' or sys_platform != 'emscripten'", +] [[package]] name = "anyio" -version = "4.9.0" +version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] @@ -44,15 +47,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, ] -[[package]] -name = "certifi" -version = "2025.4.26" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, -] - [[package]] name = "click" version = "8.2.1" @@ -149,10 +143,10 @@ wheels = [ [[package]] name = "geocodio-library-python" -version = "1.0.0" +version = "1.3.0" source = { editable = "." } dependencies = [ - { name = "httpx" }, + { name = "httpx2" }, ] [package.optional-dependencies] @@ -163,7 +157,6 @@ dev = [ { name = "mypy" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "pytest-httpx" }, { name = "pytest-mock" }, { name = "python-dotenv" }, ] @@ -172,12 +165,11 @@ dev = [ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, { name = "flake8", marker = "extra == 'dev'", specifier = ">=6.0.0" }, - { name = "httpx", specifier = ">=0.24.0" }, + { name = "httpx2", specifier = ">=2.0.0" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=5.12.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, - { name = "pytest-httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.10.0" }, { name = "python-dotenv", marker = "extra == 'dev'", specifier = ">=1.0.0" }, ] @@ -193,40 +185,51 @@ wheels = [ ] [[package]] -name = "httpcore" -version = "1.0.9" +name = "httpcore2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, { name = "h11" }, + { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, ] [[package]] -name = "httpx" -version = "0.28.1" +name = "httpx2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -389,19 +392,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, ] -[[package]] -name = "pytest-httpx" -version = "0.35.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/89/5b12b7b29e3d0af3a4b9c071ee92fa25a9017453731a38f08ba01c280f4c/pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f", size = 54146, upload-time = "2024-11-28T19:16:54.237Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/ed/026d467c1853dd83102411a78126b4842618e86c895f93528b0528c7a620/pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744", size = 19442, upload-time = "2024-11-28T19:16:52.787Z" }, -] - [[package]] name = "pytest-mock" version = "3.14.1" @@ -423,15 +413,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, ] -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - [[package]] name = "tomli" version = "2.2.1" @@ -471,6 +452,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.0"