From 6730a63de171ccd052c99a221c51cd6446b7a86d Mon Sep 17 00:00:00 2001 From: lofingv <97442878+lofingv@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:15:47 +0700 Subject: [PATCH 1/5] refactor: update X402Auth with better stream handling and schema filtering --- src/opengradient/x402_auth.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/opengradient/x402_auth.py b/src/opengradient/x402_auth.py index ac80fa0..e066847 100644 --- a/src/opengradient/x402_auth.py +++ b/src/opengradient/x402_auth.py @@ -31,18 +31,9 @@ def __init__( self, account: typing.Any, max_value: typing.Optional[int] = None, - payment_requirements_selector: typing.Optional[ - typing.Callable[ - [ - list[PaymentRequirements], - typing.Optional[str], - typing.Optional[str], - typing.Optional[int], - ], - PaymentRequirements, - ] - ] = None, + payment_requirements_selector: typing.Optional[typing.Callable] = None, network_filter: typing.Optional[str] = None, + scheme_filter: typing.Optional[str] = None, ): """ Initialize X402Auth with an Ethereum account for signing payments. @@ -56,22 +47,18 @@ def __init__( self.x402_client = x402Client( account, max_value=max_value, - payment_requirements_selector=payment_requirements_selector, # type: ignore + payment_requirements_selector=payment_requirements_selector, ) self.network_filter = network_filter + self.scheme_filter = scheme_filter async def async_auth_flow( self, request: httpx.Request ) -> typing.AsyncGenerator[httpx.Request, httpx.Response]: """ Handle authentication flow for x402 payment protocol. - - Args: - request: httpx Request object to be authenticated - - Yields: - httpx Request object with authentication headers attached """ + request.read() response = yield request if response.status_code == 402: @@ -85,6 +72,10 @@ async def async_auth_flow( payment_response.accepts, self.network_filter, ) + + if not selected_requirements: + logging.error("X402Auth: No compatible payment requirements found") + return payment_header = self.x402_client.create_payment_header( selected_requirements, payment_response.x402_version @@ -95,5 +86,5 @@ async def async_auth_flow( yield request except Exception as e: - logging.error(f"X402Auth: Error handling payment: {e}") + logging.error(f"X402Auth: Error handling payment: {str(e)}") return From 0a5ad6ae63f5b9355eafb2c77c6723154bb71cb3 Mon Sep 17 00:00:00 2001 From: lofingv <97442878+lofingv@users.noreply.github.com> Date: Fri, 6 Feb 2026 07:54:59 +0700 Subject: [PATCH 2/5] refactor: improve type safety and use scheme_filter in auth flow Add PaymentSelector type alias and pass scheme_filter to requirements selector as suggested by Copilot --- src/opengradient/x402_auth.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/opengradient/x402_auth.py b/src/opengradient/x402_auth.py index e066847..7b0b0b5 100644 --- a/src/opengradient/x402_auth.py +++ b/src/opengradient/x402_auth.py @@ -12,6 +12,16 @@ from x402.clients.base import x402Client from x402.types import x402PaymentRequiredResponse, PaymentRequirements +# Define a type alias for better readability and type safety as suggested by Copilot +PaymentSelector = typing.Callable[ + [ + list[PaymentRequirements], + typing.Optional[str], + typing.Optional[str], + typing.Optional[int], + ], + PaymentRequirements, +] class X402Auth(httpx.Auth): """ @@ -31,7 +41,7 @@ def __init__( self, account: typing.Any, max_value: typing.Optional[int] = None, - payment_requirements_selector: typing.Optional[typing.Callable] = None, + payment_requirements_selector: typing.Optional[PaymentSelector] = None, network_filter: typing.Optional[str] = None, scheme_filter: typing.Optional[str] = None, ): @@ -57,8 +67,16 @@ async def async_auth_flow( ) -> typing.AsyncGenerator[httpx.Request, httpx.Response]: """ Handle authentication flow for x402 payment protocol. + + Args: + request: httpx Request object to be authenticated + + Yields: + httpx Request object with authentication headers attached """ + # Buffer request body to allow re-reading after 402 challenge request.read() + response = yield request if response.status_code == 402: @@ -71,6 +89,7 @@ async def async_auth_flow( selected_requirements = self.x402_client.select_payment_requirements( payment_response.accepts, self.network_filter, + self.scheme_filter, ) if not selected_requirements: From 1394d1468e3f85f0871b61cb4d06b6246e063345 Mon Sep 17 00:00:00 2001 From: lofingv <97442878+lofingv@users.noreply.github.com> Date: Sat, 7 Feb 2026 02:57:07 +0000 Subject: [PATCH 3/5] chore: remove deprecated x402_auth.py after upstream refactor --- src/opengradient/x402_auth.py | 109 ---------------------------------- 1 file changed, 109 deletions(-) delete mode 100644 src/opengradient/x402_auth.py diff --git a/src/opengradient/x402_auth.py b/src/opengradient/x402_auth.py deleted file mode 100644 index 7b0b0b5..0000000 --- a/src/opengradient/x402_auth.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -X402 Authentication handler for httpx streaming requests. - -This module provides an httpx Auth class that handles x402 payment protocol -authentication for streaming responses. -""" - -import httpx -import typing -import logging - -from x402.clients.base import x402Client -from x402.types import x402PaymentRequiredResponse, PaymentRequirements - -# Define a type alias for better readability and type safety as suggested by Copilot -PaymentSelector = typing.Callable[ - [ - list[PaymentRequirements], - typing.Optional[str], - typing.Optional[str], - typing.Optional[int], - ], - PaymentRequirements, -] - -class X402Auth(httpx.Auth): - """ - httpx Auth handler for x402 payment protocol. - - This class implements the httpx Auth interface to handle 402 Payment Required - responses by automatically creating and attaching payment headers. - - Example: - async with httpx.AsyncClient(auth=X402Auth(account=wallet_account)) as client: - response = await client.get("https://api.example.com/paid-resource") - """ - - requires_response_body = True - - def __init__( - self, - account: typing.Any, - max_value: typing.Optional[int] = None, - payment_requirements_selector: typing.Optional[PaymentSelector] = None, - network_filter: typing.Optional[str] = None, - scheme_filter: typing.Optional[str] = None, - ): - """ - Initialize X402Auth with an Ethereum account for signing payments. - - Args: - account: eth_account LocalAccount instance for signing payments - max_value: Optional maximum allowed payment amount in base units - network_filter: Optional network filter for selecting payment requirements - scheme_filter: Optional scheme filter for selecting payment requirements - """ - self.x402_client = x402Client( - account, - max_value=max_value, - payment_requirements_selector=payment_requirements_selector, - ) - self.network_filter = network_filter - self.scheme_filter = scheme_filter - - async def async_auth_flow( - self, request: httpx.Request - ) -> typing.AsyncGenerator[httpx.Request, httpx.Response]: - """ - Handle authentication flow for x402 payment protocol. - - Args: - request: httpx Request object to be authenticated - - Yields: - httpx Request object with authentication headers attached - """ - # Buffer request body to allow re-reading after 402 challenge - request.read() - - response = yield request - - if response.status_code == 402: - try: - await response.aread() - data = response.json() - - payment_response = x402PaymentRequiredResponse(**data) - - selected_requirements = self.x402_client.select_payment_requirements( - payment_response.accepts, - self.network_filter, - self.scheme_filter, - ) - - if not selected_requirements: - logging.error("X402Auth: No compatible payment requirements found") - return - - payment_header = self.x402_client.create_payment_header( - selected_requirements, payment_response.x402_version - ) - - request.headers["X-Payment"] = payment_header - request.headers["Access-Control-Expose-Headers"] = "X-Payment-Response" - yield request - - except Exception as e: - logging.error(f"X402Auth: Error handling payment: {str(e)}") - return From 08746a0b7be6741c3ec6d4d4244e5af8731c8266 Mon Sep 17 00:00:00 2001 From: lofingv <97442878+lofingv@users.noreply.github.com> Date: Sat, 7 Feb 2026 03:17:28 +0000 Subject: [PATCH 4/5] refactor: move x402_auth to client folder and add filters --- src/opengradient/client/x402_auth.py | 95 ++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/opengradient/client/x402_auth.py diff --git a/src/opengradient/client/x402_auth.py b/src/opengradient/client/x402_auth.py new file mode 100644 index 0000000..9f54c14 --- /dev/null +++ b/src/opengradient/client/x402_auth.py @@ -0,0 +1,95 @@ +""" +X402 Authentication handler for httpx streaming requests. + +This module provides an httpx Auth class that handles x402 payment protocol +authentication for streaming responses. +""" + +import logging +import typing + +import httpx +from x402.clients.base import x402Client +from x402.types import PaymentRequirements, x402PaymentRequiredResponse + + +class X402Auth(httpx.Auth): + """ + httpx Auth handler for x402 payment protocol. + + This class implements the httpx Auth interface to handle 402 Payment Required + responses by automatically creating and attaching payment headers. + + Example: + async with httpx.AsyncClient(auth=X402Auth(account=wallet_account)) as client: + response = await client.get("https://api.example.com/paid-resource") + """ + + requires_response_body = True + + def __init__( + self, + account: typing.Any, + max_value: typing.Optional[int] = None, + payment_requirements_selector: typing.Optional[ + typing.Callable[ + [ + list[PaymentRequirements], + typing.Optional[str], + typing.Optional[str], + typing.Optional[int], + ], + PaymentRequirements, + ] + ] = None, + network_filter: typing.Optional[str] = None, + ): + """ + Initialize X402Auth with an Ethereum account for signing payments. + + Args: + account: eth_account LocalAccount instance for signing payments + max_value: Optional maximum allowed payment amount in base units + network_filter: Optional network filter for selecting payment requirements + scheme_filter: Optional scheme filter for selecting payment requirements + """ + self.x402_client = x402Client( + account, + max_value=max_value, + payment_requirements_selector=payment_requirements_selector, # type: ignore + ) + self.network_filter = network_filter + + async def async_auth_flow(self, request: httpx.Request) -> typing.AsyncGenerator[httpx.Request, httpx.Response]: + """ + Handle authentication flow for x402 payment protocol. + + Args: + request: httpx Request object to be authenticated + + Yields: + httpx Request object with authentication headers attached + """ + response = yield request + + if response.status_code == 402: + try: + await response.aread() + data = response.json() + + payment_response = x402PaymentRequiredResponse(**data) + + selected_requirements = self.x402_client.select_payment_requirements( + payment_response.accepts, + self.network_filter, + ) + + payment_header = self.x402_client.create_payment_header(selected_requirements, payment_response.x402_version) + + request.headers["X-Payment"] = payment_header + request.headers["Access-Control-Expose-Headers"] = "X-Payment-Response" + yield request + + except Exception as e: + logging.error(f"X402Auth: Error handling payment: {e}") + return From bbf5b7d6e929ea45444272689753f319d879ae2b Mon Sep 17 00:00:00 2001 From: lofingv <97442878+lofingv@users.noreply.github.com> Date: Sat, 7 Feb 2026 03:22:31 +0000 Subject: [PATCH 5/5] refactor: move x402_auth to client folder and add filters --- src/opengradient/client/x402_auth.py | 75 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/src/opengradient/client/x402_auth.py b/src/opengradient/client/x402_auth.py index 9f54c14..b601865 100644 --- a/src/opengradient/client/x402_auth.py +++ b/src/opengradient/client/x402_auth.py @@ -12,17 +12,20 @@ from x402.clients.base import x402Client from x402.types import PaymentRequirements, x402PaymentRequiredResponse +# Псевдоним типов для чистоты и прохождения проверок +PaymentSelector = typing.Callable[ + [ + list[PaymentRequirements], + typing.Optional[str], + typing.Optional[str], + typing.Optional[int], + ], + PaymentRequirements, +] class X402Auth(httpx.Auth): """ httpx Auth handler for x402 payment protocol. - - This class implements the httpx Auth interface to handle 402 Payment Required - responses by automatically creating and attaching payment headers. - - Example: - async with httpx.AsyncClient(auth=X402Auth(account=wallet_account)) as client: - response = await client.get("https://api.example.com/paid-resource") """ requires_response_body = True @@ -31,45 +34,31 @@ def __init__( self, account: typing.Any, max_value: typing.Optional[int] = None, - payment_requirements_selector: typing.Optional[ - typing.Callable[ - [ - list[PaymentRequirements], - typing.Optional[str], - typing.Optional[str], - typing.Optional[int], - ], - PaymentRequirements, - ] - ] = None, + payment_requirements_selector: typing.Optional[PaymentSelector] = None, network_filter: typing.Optional[str] = None, + scheme_filter: typing.Optional[str] = None, ): """ - Initialize X402Auth with an Ethereum account for signing payments. - Args: - account: eth_account LocalAccount instance for signing payments - max_value: Optional maximum allowed payment amount in base units - network_filter: Optional network filter for selecting payment requirements - scheme_filter: Optional scheme filter for selecting payment requirements + account: eth_account LocalAccount instance + max_value: Optional maximum allowed payment + network_filter: Optional network filter + scheme_filter: Optional scheme filter """ self.x402_client = x402Client( account, max_value=max_value, - payment_requirements_selector=payment_requirements_selector, # type: ignore + payment_requirements_selector=payment_requirements_selector, ) self.network_filter = network_filter - - async def async_auth_flow(self, request: httpx.Request) -> typing.AsyncGenerator[httpx.Request, httpx.Response]: - """ - Handle authentication flow for x402 payment protocol. - - Args: - request: httpx Request object to be authenticated - - Yields: - httpx Request object with authentication headers attached - """ + self.scheme_filter = scheme_filter + + async def async_auth_flow( + self, request: httpx.Request + ) -> typing.AsyncGenerator[httpx.Request, httpx.Response]: + # Подготавливаем тело запроса для возможности повторной отправки + request.read() + response = yield request if response.status_code == 402: @@ -79,17 +68,27 @@ async def async_auth_flow(self, request: httpx.Request) -> typing.AsyncGenerator payment_response = x402PaymentRequiredResponse(**data) + # Используем оба фильтра selected_requirements = self.x402_client.select_payment_requirements( payment_response.accepts, self.network_filter, + self.scheme_filter, ) - payment_header = self.x402_client.create_payment_header(selected_requirements, payment_response.x402_version) + if not selected_requirements: + logging.error("X402Auth: No compatible payment requirements found") + return + + payment_header = self.x402_client.create_payment_header( + selected_requirements, + payment_response.x402_version + ) request.headers["X-Payment"] = payment_header request.headers["Access-Control-Expose-Headers"] = "X-Payment-Response" + yield request except Exception as e: logging.error(f"X402Auth: Error handling payment: {e}") - return + return \ No newline at end of file