Skip to content
Merged
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
4 changes: 2 additions & 2 deletions auth/management.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Annotated

import httpx
import httpx2
from cachetools import TTLCache
from fastapi import Depends

Expand All @@ -24,7 +24,7 @@ def get_management_token(settings: Annotated[Settings, Depends(get_settings)]):
"client_secret": settings.auth0_management_secret,
"audience": f"https://{settings.auth0_domain}/api/v2/",
}
response = httpx.post(url, json=payload)
response = httpx2.post(url, json=payload)
response.raise_for_status()
data = response.json()
token = data["access_token"]
Expand Down
6 changes: 3 additions & 3 deletions auth/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import weakref
from datetime import UTC, datetime, timedelta

import httpx
import httpx2
import jwt
from cachetools import TTLCache
from fastapi import HTTPException
Expand Down Expand Up @@ -106,7 +106,7 @@ async def _fetch_rsa_keys(auth0_domain: str) -> dict:

try:
metadata_url = f"https://{auth0_domain}/.well-known/openid-configuration"
async with httpx.AsyncClient() as client:
async with httpx2.AsyncClient() as client:
metadata_response = await client.get(metadata_url)
metadata_response.raise_for_status()
metadata = metadata_response.json()
Expand All @@ -118,7 +118,7 @@ async def _fetch_rsa_keys(auth0_domain: str) -> dict:
except KeyError as exc:
logger.error(f"OIDC metadata from {metadata_url} did not include jwks_uri")
raise InvalidTokenError("Failed to fetch JWKS") from exc
except (httpx.HTTPError, ValueError) as exc:
except (httpx2.HTTPError, ValueError) as exc:
logger.error(
f"Failed to fetch OIDC metadata or JWKS for domain {auth0_domain}: {exc}"
)
Expand Down
20 changes: 10 additions & 10 deletions auth0/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import time
from typing import Iterator, Optional, Type, TypeVar

import httpx
import httpx2
from fastapi import Depends
from httpx import HTTPStatusError
from httpx2 import HTTPStatusError
from pydantic import BaseModel, EmailStr, Field, HttpUrl, model_validator

from auth.management import get_management_token
Expand Down Expand Up @@ -192,7 +192,7 @@ def __init__(self, domain: str, management_token: str):
self.domain = domain
self.api_base = f"https://{domain}/api/v2"
self.management_token = management_token
self._client = httpx.Client(headers={"Authorization": f"Bearer {management_token}"})
self._client = httpx2.Client(headers={"Authorization": f"Bearer {management_token}"})

def close(self) -> None:
self._client.close()
Expand All @@ -206,16 +206,16 @@ def __exit__(self, exc_type, exc, tb) -> None:
T = TypeVar('T', bound=BaseModel)

@staticmethod
def _convert_list(resp: httpx.Response, model: Type[T]) -> list[T]:
def _convert_list(resp: httpx2.Response, model: Type[T]) -> list[T]:
"""Convert a list of data to the given pydantic model."""
return [model(**item) for item in resp.json()]

@staticmethod
def _convert_users(resp: httpx.Response):
def _convert_users(resp: httpx2.Response):
return Auth0Client._convert_list(resp, Auth0UserData)

@staticmethod
def _convert_roles(resp: httpx.Response):
def _convert_roles(resp: httpx2.Response):
return Auth0Client._convert_list(resp, RoleData)

def get_users(self, page: Optional[int] = None, per_page: Optional[int] = None, include_totals: Optional[bool] = None, q: Optional[str] = None) -> list[Auth0UserData] | UsersWithTotals:
Expand Down Expand Up @@ -332,7 +332,7 @@ def export_and_download_users(

logger.info(f"User export job {job_id} completed successfully. Downloading from {location}...")
# Don't use client for this, we don't want the auth header here
download = httpx.get(location)
download = httpx2.get(location)
download.raise_for_status()

content = download.content
Expand Down Expand Up @@ -365,7 +365,7 @@ def check_user_password(self, email: str, password: str, settings: Settings) ->
"scope": "openid",
}
# We don't want the management token here so not using self._client
resp = httpx.post(url, data=data)
resp = httpx2.post(url, data=data)
if resp.status_code in {400, 403}:
error = resp.json().get("error")
if error == "invalid_grant":
Expand Down Expand Up @@ -408,7 +408,7 @@ def remove_roles_from_user(self, user_id: str, role_id: str | list[str]):
url = f"{self.api_base}/users/{user_id}/roles"
if isinstance(role_id, str):
role_id = [role_id]
# httpx.Client.delete() no longer accepts json payloads (0.28+), so use request()
# httpx2.Client.delete() no longer accepts json payloads (0.28+), so use request()
resp = self._client.request("DELETE", url, json={"roles": role_id})
resp.raise_for_status()
return True
Expand Down Expand Up @@ -611,7 +611,7 @@ def trigger_password_change(self, user_email: str, client_id: str, settings: Set
# NOTE: Authentication API, not management API
url = f"https://{self.domain}/dbconnections/change_password"
# Don't use _client here, since it's not a management API endpoint
resp = httpx.post(
resp = httpx2.post(
url,
json={"email": user_email,
"client_id": client_id,
Expand Down
4 changes: 2 additions & 2 deletions auth0/user_info.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Annotated

import httpx
import httpx2
from fastapi.params import Depends
from pydantic import BaseModel, ConfigDict, Field

Expand Down Expand Up @@ -47,7 +47,7 @@ async def get_auth0_user_info(
Doesn't require management API access so may be more efficient when
only the current user is required
"""
async with httpx.AsyncClient() as client:
async with httpx2.AsyncClient() as client:
resp = await client.get(
f"https://{settings.auth0_domain}/userinfo", headers={
"Authorization": f"Bearer {auth0_token}"
Expand Down
2 changes: 1 addition & 1 deletion db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from logging import getLogger
from typing import Optional, Self, Type

from httpx import HTTPStatusError
from httpx2 import HTTPStatusError
from pydantic import AwareDatetime
from sqlalchemy import Column, Index, String, Text, UniqueConstraint, delete, desc, func
from sqlmodel import DateTime, Field, Relationship, Session, select
Expand Down
2 changes: 1 addition & 1 deletion galaxy/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Annotated

from fastapi import Depends
from httpx import Client
from httpx2 import Client

from galaxy.config import GalaxySettings, get_galaxy_settings
from galaxy.schemas import GalaxyUserModel
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ dependencies = [
"fastapi[standard]>=0.139.0",
# Security floor for CVE-2026-26007 (subgroup validation for SECT curves)
"cryptography>=49.0",
"httpx>=0.28.1",
"httpx2>=2.9.1",
"psycopg[binary]>=3.3.4",
"email-validator~=2.3",
"pydantic-settings>=2.14.2",
Expand All @@ -31,7 +31,6 @@ dependencies = [
[project.optional-dependencies]
dev = [
"boto3-stubs>=1.43.45",
"httpx2>=2.9.1",
"pytest>=9.0.3",
"pytest-mock>=3.14.0",
"pytest-sugar>=1.1.1",
Expand All @@ -56,7 +55,7 @@ exclude-newer = "7 days"
[tool.pytest.ini_options]
pythonpath = ["."]
testpaths = ["tests"]
addopts = "--cov=auth --cov=routers --cov=schemas --cov=db --cov=biocommons --cov=scheduled_tasks --cov-report=term --cov-report=xml"
addopts = "-p no:respx --cov=auth --cov=routers --cov=schemas --cov=db --cov=biocommons --cov=scheduled_tasks --cov-report=term --cov-report=xml"

[build-system]
requires = ["setuptools>=61.0"]
Expand All @@ -71,4 +70,5 @@ line-length = 88
target-version = "py313"
lint.select = ["E", "F", "I"]
lint.ignore = ["E501"]
lint.per-file-ignores = { "tests/conftest.py" = ["E402"] }
exclude = ["tests/data", ".venv", "venv", "migrations"]
4 changes: 2 additions & 2 deletions register/tokens.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import UTC, datetime, timedelta

import httpx
import httpx2
import jwt
from fastapi import HTTPException
from jwt.exceptions import InvalidTokenError
Expand Down Expand Up @@ -35,7 +35,7 @@ def verify_registration_token(token: str, settings: Settings):


def validate_recaptcha(token: str, settings: Settings):
response = httpx.post(
response = httpx2.post(
url="https://www.google.com/recaptcha/api/siteverify",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"secret": settings.recaptcha_secret, "response": token},
Expand Down
2 changes: 1 addition & 1 deletion routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Response
from fastapi.params import Query
from httpx import HTTPStatusError
from httpx2 import HTTPStatusError
from pydantic import (
BaseModel,
ConfigDict,
Expand Down
2 changes: 1 addition & 1 deletion routers/biocommons_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Response
from httpx import HTTPStatusError
from httpx2 import HTTPStatusError
from sqlmodel import Session

from auth0.client import Auth0Client, get_auth0_client
Expand Down
2 changes: 1 addition & 1 deletion routers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from botocore.exceptions import ClientError
from fastapi import APIRouter, Body, Depends, HTTPException, Response, status
from httpx import AsyncClient, HTTPStatusError
from httpx2 import AsyncClient, HTTPStatusError
from loguru import logger
from pydantic import AliasPath, AwareDatetime, BaseModel, Field
from pydantic import BaseModel as PydanticBaseModel
Expand Down
2 changes: 1 addition & 1 deletion scheduled_tasks/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import Any
from uuid import UUID

from httpx import HTTPStatusError
from httpx2 import HTTPStatusError
from loguru import logger
from pydantic import BaseModel, field_validator
from sqlalchemy.exc import IntegrityError
Expand Down
4 changes: 2 additions & 2 deletions services/institutions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Optional

import httpx
import httpx2

GALAXY_AU_VALIDATE_URL = "https://site.usegalaxy.org.au/institution/validate"

Expand All @@ -13,7 +13,7 @@ async def check_australian_research_institution_email(email: str) -> Optional[bo
that must not act on an outage should treat None distinctly from False.
"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
async with httpx2.AsyncClient(timeout=5.0) as client:
response = await client.get(GALAXY_AU_VALIDATE_URL, params={"email": email})
response.raise_for_status()
return bool(response.json().get("valid", False))
Expand Down
6 changes: 3 additions & 3 deletions tests/admin_api/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2123,7 +2123,7 @@ def test_admin_update_user_username_duplicate_in_auth0(
persistent_factories,
):
"""Test username update fails when Auth0 returns 409 conflict."""
from httpx import HTTPStatusError, Request, Response
from httpx2 import HTTPStatusError, Request, Response

user = _create_user_with_platform_membership(
db_session=test_db_session,
Expand Down Expand Up @@ -2170,7 +2170,7 @@ def test_admin_update_user_username_auth0_400_error(
persistent_factories,
):
"""Test username update handles Auth0 400 error."""
from httpx import HTTPStatusError, Request, Response
from httpx2 import HTTPStatusError, Request, Response

user = _create_user_with_platform_membership(
db_session=test_db_session,
Expand Down Expand Up @@ -2244,7 +2244,7 @@ def test_get_unverified_users(test_client, test_db_session, as_admin_user, galax
def test_auth0client_get_users_forwards_filter_to_httpx(mocker):
client = Auth0Client(domain="tenant.example.auth0.com", management_token="tok")

# Mock the underlying httpx client and its response
# Mock the underlying httpx2 client and its response
fake_resp = mocker.Mock()
fake_resp.json.return_value = [] # get_users() reads .json() only
client._client = mocker.Mock()
Expand Down
2 changes: 1 addition & 1 deletion tests/auth/test_auth_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


def test_get_management_token_success(mock_settings):
with patch("auth.management.httpx.post") as mock_post:
with patch("auth.management.httpx2.post") as mock_post:
mock_post.return_value.json.return_value = {"access_token": "abc123"}
mock_post.return_value.raise_for_status = lambda: None

Expand Down
4 changes: 2 additions & 2 deletions tests/auth/test_auth_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from fastapi.security import HTTPAuthorizationCredentials
from fastapi.testclient import TestClient
from freezegun import freeze_time
from httpx import Request, Response
from httpx2 import Request, Response
from jwt import InvalidSignatureError
from jwt.algorithms import RSAAlgorithm

Expand Down Expand Up @@ -150,7 +150,7 @@ async def test_get_rsa_key_returns_key(mock_settings: Settings):
jwks_url = f"https://{mock_settings.auth0_domain}/.well-known/jwks.json"

with patch("auth.validator.jwt.get_unverified_header", return_value=unverified_header), \
patch("auth.validator.httpx.AsyncClient.get", new_callable=AsyncMock) as mock_get:
patch("auth.validator.httpx2.AsyncClient.get", new_callable=AsyncMock) as mock_get:
metadata_response = Response(
200,
json={"jwks_uri": jwks_url},
Expand Down
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import httpx2

# respx only patches the real `httpx`/`httpcore`; alias them to httpx2 so it
# intercepts requests made through our httpx2-based clients. Must run before
# anything else imports httpx or httpcore, so its pytest11 entry point plugin
# is disabled (see `-p no:respx` in pyproject.toml) and loaded manually here.
httpx2.alias_httpx()

pytest_plugins = ["respx.plugin"]

import os
import warnings
from datetime import datetime
Expand Down
2 changes: 1 addition & 1 deletion tests/db/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest
import respx
from freezegun import freeze_time
from httpx import Response
from httpx2 import Response
from mimesis import Person
from mimesis.locales import Locale
from sqlalchemy.exc import IntegrityError
Expand Down
4 changes: 2 additions & 2 deletions tests/galaxy/test_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import httpx
import httpx2
import pytest

from galaxy.client import GalaxyClient
Expand All @@ -15,7 +15,7 @@ def test_username_exists(galaxy_client, respx_mock):
user1 = GalaxyUserFactory.build(username="user1")
user2 = GalaxyUserFactory.build(username="user2")
respx_mock.get("https://galaxy.example.com/api/users").mock(
return_value=httpx.Response(
return_value=httpx2.Response(
200,
json=[user1.model_dump(mode="json"),
user2.model_dump(mode="json")]
Expand Down
2 changes: 1 addition & 1 deletion tests/scheduled_tasks/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import pytest
from botocore.exceptions import ClientError, EndpointConnectionError
from httpx import HTTPStatusError
from httpx2 import HTTPStatusError
from sqlmodel import Session, select

from db.models import (
Expand Down
2 changes: 1 addition & 1 deletion tests/test_auth0_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest
import respx
from httpx import Response
from httpx2 import Response
from pydantic import ValidationError

from auth0.client import (
Expand Down
2 changes: 1 addition & 1 deletion tests/test_biocommons_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest
import respx
from httpx import Response
from httpx2 import Response
from sqlmodel import select

from db.models import Auth0Role, BiocommonsGroup, Platform
Expand Down
Loading