Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions newsfragments/152.changed.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Replace `HTTPX <https://www.python-httpx.org/>`_ with the maintained `HTTPX2 <https://httpx2.pydantic.dev/>`_ fork.
Users that provide a custom ``httpx_client`` must now pass an ``httpx2.Client`` or ``httpx2.AsyncClient``, and exception handling should catch exceptions from ``httpx2`` instead of ``httpx``.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ classifiers = [
]
dependencies = [
"filelock>=3.17.0",
"httpx>=0.28.1",
"httpx2>=2.3.0",
"logbook>=1.8.0",
"outcome>=1.3.0.post0",
"platformdirs>=4.3.6",
Expand Down Expand Up @@ -71,8 +71,8 @@ nox = [
test = [
"pytest>=9.0.3",
"pytest-asyncio>=1.4.0",
"httpx2-pytest>=1.0.1",
"pytest-trio>=0.8.0",
"respx>=0.23.1",
]
towncrier = [
"towncrier>=25.8.0",
Expand Down
11 changes: 4 additions & 7 deletions src/spacetrack/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import time
import weakref

import httpx
import httpx2
import outcome
import sniffio
from filelock import AsyncFileLock
from httpx import USE_CLIENT_DEFAULT
from httpx2 import USE_CLIENT_DEFAULT

from .base import (
BASE_URL,
Expand Down Expand Up @@ -35,10 +35,11 @@ class AsyncSpaceTrackClient(SpaceTrackClient):

Refer to the :class:`~spacetrack.base.SpaceTrackClient` documentation for
more information. Note that if passed, the ``httpx_client`` parameter must
be an ``httpx.AsyncClient``.
be an ``httpx2.AsyncClient``.
"""

_file_lock_cls = AsyncFileLock
_httpx_client_cls = httpx2.AsyncClient

def __init__(
self,
Expand All @@ -51,10 +52,6 @@ def __init__(
additional_rate_limit=None,
cache_path=None,
):
if httpx_client is None:
httpx_client = httpx.AsyncClient(timeout=30)
elif not isinstance(httpx_client, httpx.AsyncClient):
raise TypeError("httpx_client must be an httpx.AsyncClient instance")
super().__init__(
identity=identity,
password=password,
Expand Down
30 changes: 18 additions & 12 deletions src/spacetrack/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
from urllib.parse import quote

import attr
import httpx
import httpx2
import outcome
from filelock import FileLock
from httpx import USE_CLIENT_DEFAULT
from httpx2 import USE_CLIENT_DEFAULT
from logbook import Logger
from platformdirs import user_cache_path
from represent import ReprHelper, ReprHelperMixin
Expand Down Expand Up @@ -175,8 +175,8 @@ class SpaceTrackClient:
follow rate limits from multiple instances.
rush_key_prefix: You may choose a prefix for the keys that will be
stored in `rush_store`, e.g. to avoid conflicts in a redis db.
httpx_client: Provide a custom ``httpx.Client` instance.
``SpaceTrackClient`` takes ownership of the httpx client. You should
httpx_client: Provide a custom ``httpx2.Client`` instance.
``SpaceTrackClient`` takes ownership of the HTTPX2 client. You should
only provide your own client if you need to configure it first (e.g.
for a proxy).
additional_rate_limit: Optionally, a :class:`rush.quota.Quota` if you
Expand Down Expand Up @@ -300,6 +300,7 @@ class SpaceTrackClient:
}

_file_lock_cls = FileLock
_httpx_client_cls = httpx2.Client

def __init__(
self,
Expand All @@ -313,7 +314,12 @@ def __init__(
cache_path=None,
):
if httpx_client is None:
httpx_client = httpx.Client(timeout=30)
httpx_client = self._httpx_client_cls(timeout=30)
elif not isinstance(httpx_client, self._httpx_client_cls):
raise TypeError(
"httpx_client must be an "
f"httpx2.{self._httpx_client_cls.__name__} instance"
)
self.client = httpx_client
self.identity = identity
self.password = password
Expand Down Expand Up @@ -590,7 +596,7 @@ def _generic_request_generator(
request, stream=iter_lines or iter_content
)

if httpx.codes.is_error(resp.status_code):
if httpx2.codes.is_error(resp.status_code):
# If we're about to raise an error, fetch the full response in case
# we're streaming
yield ReadResponse(resp)
Expand Down Expand Up @@ -695,7 +701,7 @@ def generic_request(
Passing ``format='json'`` will return the JSON **unparsed**. Do
not set ``format`` if you want the parsed JSON object returned!

.. _`HTTPX Timeouts`: https://www.python-httpx.org/advanced/timeouts/
.. _`HTTPX Timeouts`: https://httpx2.pydantic.dev/advanced/timeouts/
"""
return self._run_event_generator(
self._generic_request_generator(
Expand Down Expand Up @@ -1109,32 +1115,32 @@ def _iter_content_generator(response, decode_unicode):
def _raise_for_status(response):
"""Raise the `HTTPStatusError` if one occurred.

This is the :meth:`httpx.Response.raise_for_status` method, modified to add
This is the :meth:`httpx2.Response.raise_for_status` method, modified to add
the response from Space-Track, if given.
"""

try:
response.raise_for_status()
except httpx.HTTPStatusError as exc:
except httpx2.HTTPStatusError as exc:
message = exc.args[0]
spacetrack_error_msg = None

try:
json = response.json()
if isinstance(json, Mapping):
spacetrack_error_msg = json["error"]
except (ValueError, KeyError, httpx.ResponseNotRead):
except (ValueError, KeyError, httpx2.ResponseNotRead):
pass

if not spacetrack_error_msg:
try:
spacetrack_error_msg = response.text
except httpx.ResponseNotRead:
except httpx2.ResponseNotRead:
pass

if spacetrack_error_msg:
message += "\nSpace-Track response:\n" + spacetrack_error_msg

raise httpx.HTTPStatusError(
raise httpx2.HTTPStatusError(
message, request=exc.request, response=exc.response
) from exc
54 changes: 33 additions & 21 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,13 @@
from unittest.mock import patch

import pytest
import respx

from spacetrack import SpaceTrackClient
from spacetrack.base import BASE_URL, CACHE_VERSION, PREDICATE_CACHE_EXPIRY_TIME


@pytest.fixture(scope="session")
def respx_router():
# Create an instance of MockRouter with our settings.
return respx.mock(assert_all_called=False, base_url=BASE_URL)


@pytest.fixture
def respx_mock(respx_router):
with respx_router:
yield respx_router
def api_url(path):
return f"{BASE_URL}{path}"


@pytest.fixture(autouse=True)
Expand All @@ -32,23 +23,41 @@ def user_cache_path(appname):


@pytest.fixture
def mock_auth(respx_mock):
respx_mock.post("ajaxauth/login").respond(json="")
respx_mock.get("ajaxauth/logout").respond(json="Successfully logged out")
def mock_auth(httpx2_mock):
def add_auth_responses():
httpx2_mock.add_response(
method="POST",
url=api_url("ajaxauth/login"),
json="",
)
httpx2_mock.add_response(
method="GET",
url=api_url("ajaxauth/logout"),
json="Successfully logged out",
)

add_auth_responses()
return add_auth_responses


@pytest.fixture
def mock_predicates_empty(respx_mock):
def mock_predicates_empty(httpx2_mock):
for controller, classes in SpaceTrackClient.request_controllers.items():
for class_ in classes:
respx_mock.get(f"{controller}/modeldef/class/{class_}").respond(
json={"data": []}
httpx2_mock.add_response(
method="GET",
url=api_url(f"{controller}/modeldef/class/{class_}"),
json={"data": []},
is_optional=True,
)


@pytest.fixture
def mock_gp_predicates(respx_mock):
respx_mock.get("basicspacedata/modeldef/class/gp").respond(
def mock_gp_predicates(httpx2_mock):
httpx2_mock.add_response(
method="GET",
url=api_url("basicspacedata/modeldef/class/gp"),
is_optional=True,
json={
"controller": "basicspacedata",
"data": [
Expand Down Expand Up @@ -378,8 +387,11 @@ def mock_gp_predicates(respx_mock):


@pytest.fixture
def mock_download_predicates(respx_mock):
respx_mock.get("fileshare/modeldef/class/download").respond(
def mock_download_predicates(httpx2_mock):
httpx2_mock.add_response(
method="GET",
url=api_url("fileshare/modeldef/class/download"),
is_optional=True,
json={
"controller": "fileshare",
"data": [
Expand Down
Loading
Loading