diff --git a/packages/gen/gen_ai_hub/_ssl.py b/packages/gen/gen_ai_hub/_ssl.py new file mode 100644 index 00000000..f161d6bc --- /dev/null +++ b/packages/gen/gen_ai_hub/_ssl.py @@ -0,0 +1,15 @@ +""" +Shared SSL context factory for httpx2 clients. + +httpx2 defaults to the OS trust store (truststore). This module preserves +the pre-migration behaviour of using certifi's CA bundle so existing +deployments are not affected. Switch callers to verify=True to adopt the +httpx2 default when ready. +""" +import ssl +import certifi + + +def default_ssl_context() -> ssl.SSLContext: + """Return an SSL context backed by certifi's CA bundle.""" + return ssl.create_default_context(cafile=certifi.where()) diff --git a/packages/gen/gen_ai_hub/batch_service/exceptions.py b/packages/gen/gen_ai_hub/batch_service/exceptions.py index 91797239..cc8b6474 100644 --- a/packages/gen/gen_ai_hub/batch_service/exceptions.py +++ b/packages/gen/gen_ai_hub/batch_service/exceptions.py @@ -2,7 +2,7 @@ Exceptions for the batch service module. """ -import httpx +import httpx2 class BatchServiceError(Exception): @@ -17,7 +17,7 @@ def __init__( request_id: str, message: str, status_code: int, - headers: httpx.Headers, + headers: httpx2.Headers, ): self.request_id = request_id self.message = message diff --git a/packages/gen/gen_ai_hub/batch_service/service.py b/packages/gen/gen_ai_hub/batch_service/service.py index cf378207..dc21fe6c 100644 --- a/packages/gen/gen_ai_hub/batch_service/service.py +++ b/packages/gen/gen_ai_hub/batch_service/service.py @@ -7,7 +7,8 @@ from typing import Optional, Union -import httpx +import httpx2 +from gen_ai_hub._ssl import default_ssl_context from gen_ai_hub import GenAIHubProxyClient from gen_ai_hub.proxy import get_proxy_client @@ -25,11 +26,11 @@ _BASE_PATH = "/llm-batch-service/v1/batches" -def _handle_http_error(response: httpx.Response) -> None: +def _handle_http_error(response: httpx2.Response) -> None: """Raises BatchServiceError from a non-2xx httpx response.""" try: response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: try: payload = response.json() request_id = payload.get('request_id', '') @@ -65,8 +66,8 @@ class BatchService: :param resource_group: Value for the ``AI-Resource-Group`` header. Falls back to the resource group on ``proxy_client`` when omitted. :type resource_group: str, Optional - :param timeout: Default HTTP request timeout passed to httpx. - :type timeout: Union[int, float, httpx.Timeout], Optional + :param timeout: Default HTTP request timeout passed to httpx2. + :type timeout: Union[int, float, httpx2.Timeout], Optional """ def __init__( @@ -74,7 +75,7 @@ def __init__( api_url: Optional[str] = None, proxy_client: Optional[GenAIHubProxyClient] = None, resource_group: Optional[str] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ): self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") if api_url: @@ -84,8 +85,8 @@ def __init__( self.api_url = base self.resource_group = resource_group self.timeout = timeout - self.client = httpx.Client(timeout=self.timeout) - self.async_client = httpx.AsyncClient(timeout=self.timeout) + self.client = httpx2.Client(timeout=self.timeout, verify=default_ssl_context()) + self.async_client = httpx2.AsyncClient(timeout=self.timeout, verify=default_ssl_context()) # ------------------------------------------------------------------ # Internal helpers @@ -98,13 +99,13 @@ def _headers(self) -> dict: return headers def _determine_timeout( - self, timeout: Union[int, float, httpx.Timeout, None] - ) -> Union[int, float, httpx.Timeout]: + self, timeout: Union[int, float, httpx2.Timeout, None] + ) -> Union[int, float, httpx2.Timeout]: if timeout is not None: return timeout if self.timeout is not None: return self.timeout - return httpx.USE_CLIENT_DEFAULT + return httpx2.USE_CLIENT_DEFAULT def _batches_url(self, *segments: str) -> str: parts = [self.api_url + _BASE_PATH] + list(segments) @@ -122,7 +123,7 @@ def create( output_uri: str, provider: str, model: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchCreateResponse: """Create a new batch processing job. @@ -137,7 +138,7 @@ def create( :param model: Model name (e.g. ``"gpt-4.1-mini"``). :type model: str :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchCreateResponse` with the job ID and initial status. """ body = BatchCreateRequest( @@ -158,12 +159,12 @@ def create( def list( self, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchListResponse: """List all batch jobs for the current resource group. :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchListResponse` containing the batch summaries. """ response = self.client.get( @@ -178,14 +179,14 @@ def list( def get( self, batch_id: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchDetailResponse: """Retrieve details of a specific batch job. :param batch_id: UUID of the batch job. :type batch_id: str :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchDetailResponse` with full job details. """ response = self.client.get( @@ -200,14 +201,14 @@ def get( def get_status( self, batch_id: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchStatusResponse: """Retrieve the current status of a batch job. :param batch_id: UUID of the batch job. :type batch_id: str :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchStatusResponse` with current and target status. """ response = self.client.get( @@ -222,14 +223,14 @@ def get_status( def cancel( self, batch_id: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchCancelResponse: """Schedule a batch job for cancellation. :param batch_id: UUID of the batch job. :type batch_id: str :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchCancelResponse` confirming the cancellation request. """ response = self.client.patch( @@ -244,14 +245,14 @@ def cancel( def delete( self, batch_id: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchDeleteResponse: """Delete a batch job (only allowed for terminal states: COMPLETED, FAILED, CANCELLED). :param batch_id: UUID of the batch job. :type batch_id: str :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchDeleteResponse` confirming the deletion. """ response = self.client.delete( @@ -275,7 +276,7 @@ async def acreate( output_uri: str, provider: str, model: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchCreateResponse: """Async variant of :meth:`create`. @@ -290,7 +291,7 @@ async def acreate( :param model: Model name (e.g. ``"gpt-4.1-mini"``). :type model: str :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchCreateResponse` with the job ID and initial status. """ body = BatchCreateRequest( @@ -311,12 +312,12 @@ async def acreate( async def alist( self, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchListResponse: """Async variant of :meth:`list`. :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchListResponse` containing the batch summaries. """ response = await self.async_client.get( @@ -331,14 +332,14 @@ async def alist( async def aget( self, batch_id: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchDetailResponse: """Async variant of :meth:`get`. :param batch_id: UUID of the batch job. :type batch_id: str :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchDetailResponse` with full job details. """ response = await self.async_client.get( @@ -353,14 +354,14 @@ async def aget( async def aget_status( self, batch_id: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchStatusResponse: """Async variant of :meth:`get_status`. :param batch_id: UUID of the batch job. :type batch_id: str :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchStatusResponse` with current and target status. """ response = await self.async_client.get( @@ -375,14 +376,14 @@ async def aget_status( async def acancel( self, batch_id: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchCancelResponse: """Async variant of :meth:`cancel`. :param batch_id: UUID of the batch job. :type batch_id: str :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchCancelResponse` confirming the cancellation request. """ response = await self.async_client.patch( @@ -397,14 +398,14 @@ async def acancel( async def adelete( self, batch_id: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> BatchDeleteResponse: """Async variant of :meth:`delete`. :param batch_id: UUID of the batch job. :type batch_id: str :param timeout: Per-request timeout override. - :type timeout: Union[int, float, httpx.Timeout], Optional + :type timeout: Union[int, float, httpx2.Timeout], Optional :returns: :class:`BatchDeleteResponse` confirming the deletion. """ response = await self.async_client.delete( diff --git a/packages/gen/gen_ai_hub/orchestration/exceptions.py b/packages/gen/gen_ai_hub/orchestration/exceptions.py index 822b0f21..843f8b79 100644 --- a/packages/gen/gen_ai_hub/orchestration/exceptions.py +++ b/packages/gen/gen_ai_hub/orchestration/exceptions.py @@ -1,4 +1,4 @@ -import httpx +import httpx2 from typing import Dict, Any @@ -12,7 +12,7 @@ class OrchestrationError(Exception): def __init__( self, request_id: str, - http_headers: httpx.Headers, + http_headers: httpx2.Headers, message: str, code: int, location: str, @@ -24,7 +24,7 @@ def __init__( :param request_id: unique identifier for the request :type request_id: str :param http_headers: the HTTP headers associated with the error, useful in case of e.g. rate limiting. - :type http_headers: httpx.Headers + :type http_headers: httpx2.Headers :param message: Detailed error message describing the issue. :type message: str :param code: Error code associated with the specific type of failure diff --git a/packages/gen/gen_ai_hub/orchestration/service.py b/packages/gen/gen_ai_hub/orchestration/service.py index 349abd23..a839ca1e 100644 --- a/packages/gen/gen_ai_hub/orchestration/service.py +++ b/packages/gen/gen_ai_hub/orchestration/service.py @@ -16,7 +16,8 @@ import dacite from gen_ai_hub.orchestration.exceptions import OrchestrationError -import httpx +import httpx2 +from gen_ai_hub._ssl import default_ssl_context from ai_api_client_sdk.models.status import Status from gen_ai_hub import GenAIHubProxyClient @@ -202,7 +203,7 @@ def __init__(self, deployment_id: Optional[str] = None, config_name: Optional[str] = None, config_id: Optional[str] = None, - timeout: Union[int, float, httpx.Timeout, None] = None): + timeout: Union[int, float, httpx2.Timeout, None] = None): """Initializes the OrchestrationService with the provided parameters. :param api_url: The base URL for the orchestration API, defaults to None @@ -218,7 +219,7 @@ def __init__(self, :param config_id: the configuration ID, defaults to None :type config_id: Optional[str], optional :param timeout: the timeout for HTTP requests, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional """ self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") if api_url: @@ -228,10 +229,10 @@ def __init__(self, self.config = config self.timeout = timeout # create reusable httpx client to improve performance - self.client = httpx.Client(timeout=self.timeout) - self.async_client = httpx.AsyncClient(timeout=self.timeout) + self.client = httpx2.Client(timeout=self.timeout, verify=default_ssl_context()) + self.async_client = httpx2.AsyncClient(timeout=self.timeout, verify=default_ssl_context()) - def _determine_timeout(self, timeout: httpx.Timeout) -> httpx.Timeout: + def _determine_timeout(self, timeout: httpx2.Timeout) -> httpx2.Timeout: # Determine the timeout to use for this request if timeout is not None: # Overwrite default timeout for this request @@ -241,7 +242,7 @@ def _determine_timeout(self, timeout: httpx.Timeout) -> httpx.Timeout: request_timeout = self.timeout else: # If timeout is not set, use httpx client's default behavior, rather than "None" (disables timeout) - request_timeout = httpx.USE_CLIENT_DEFAULT + request_timeout = httpx2.USE_CLIENT_DEFAULT return request_timeout def _should_retry(self, error: Exception) -> bool: @@ -252,7 +253,7 @@ def _should_retry(self, error: Exception) -> bool: :return: True if the error is retryable (only 429 rate limit errors), False otherwise. :rtype: bool """ - if isinstance(error, httpx.HTTPStatusError): + if isinstance(error, httpx2.HTTPStatusError): return error.response.status_code == 429 return False @@ -264,7 +265,7 @@ def _get_retry_after(self, error: Exception) -> Optional[float]: :return: Number of seconds to wait before retrying, or None if not specified. :rtype: Optional[float] """ - if isinstance(error, httpx.HTTPStatusError) and error.response.status_code == 429: + if isinstance(error, httpx2.HTTPStatusError) and error.response.status_code == 429: retry_after = error.response.headers.get('Retry-After') if retry_after: try: @@ -311,7 +312,7 @@ def _execute_request( history: List[Message], stream: bool, stream_options: Optional[dict] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> Union[OrchestrationResponse, Iterable[OrchestrationResponseStreaming]]: """Executes an orchestration request synchronously. For streaming requests, this method creates a single HTTP stream. It manually enters the stream's @@ -330,7 +331,7 @@ def _execute_request( :param stream_options: additional streaming options, defaults to None :type stream_options: Optional[dict], optional :param timeout: the timeout for the request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :raises ValueError: If no configuration is provided. :raises OrchestrationError: If the HTTP request fails. :return: An OrchestrationResponse if not streaming, or an iterable of OrchestrationResponseStreaming @@ -369,7 +370,7 @@ def _execute_request( ) try: response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: _handle_http_error(error, response) data = response.json() @@ -386,7 +387,7 @@ async def _a_execute_request( history: List[Message], stream: bool, stream_options: Optional[dict] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> Union[OrchestrationResponse, AsyncSSEClient]: """Executes an orchestration request asynchronously. @@ -401,7 +402,7 @@ async def _a_execute_request( :param stream_options: additional streaming options, defaults to None :type stream_options: Optional[dict], optional :param timeout: the timeout for the request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :raises ValueError: If no configuration is provided. :raises OrchestrationError: If the HTTP request fails. :return: An OrchestrationResponse if not streaming, or an AsyncSSEClient for iterating over @@ -438,7 +439,7 @@ async def _a_execute_request( ) try: response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: _handle_http_error(error, response) data = response.json() @@ -453,7 +454,7 @@ def run( config: Optional[OrchestrationConfig] = None, template_values: Optional[List[TemplateValue]] = None, history: Optional[List[Message]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> OrchestrationResponse: """Executes an orchestration request synchronously (non-streaming). @@ -464,7 +465,7 @@ def run( :param history: the message history, defaults to None :type history: Optional[List[Message]], optional :param timeout: the timeout for the request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :return: An OrchestrationResponse object. :rtype: OrchestrationResponse """ @@ -482,7 +483,7 @@ def stream( template_values: Optional[List[TemplateValue]] = None, history: Optional[List[Message]] = None, stream_options: Optional[dict] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> SSEClient: """Executes an orchestration request in streaming mode (synchronously). @@ -495,7 +496,7 @@ def stream( :param stream_options: the additional streaming options, defaults to None :type stream_options: Optional[dict], optional :param timeout: the timeout for the request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :return: An SSEClient instance for iterating over the streaming response. :rtype: SSEClient """ @@ -513,7 +514,7 @@ async def arun( config: Optional[OrchestrationConfig] = None, template_values: Optional[List[TemplateValue]] = None, history: Optional[List[Message]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> OrchestrationResponse: """Executes an orchestration request asynchronously (non-streaming). @@ -524,7 +525,7 @@ async def arun( :param history: the message history, defaults to None :type history: Optional[List[Message]], optional :param timeout: the timeout for the request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :return: An OrchestrationResponse object. :rtype: OrchestrationResponse """ @@ -542,7 +543,7 @@ async def astream( template_values: Optional[List[TemplateValue]] = None, history: Optional[List[Message]] = None, stream_options: Optional[dict] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> AsyncSSEClient: """Executes an orchestration request asynchronously in streaming mode. @@ -555,7 +556,7 @@ async def astream( :param stream_options: the additional streaming options, defaults to None :type stream_options: Optional[dict], optional :param timeout: the timeout for the request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :return: An AsyncSSEClient instance for iterating over the streaming response. :rtype: AsyncSSEClient """ @@ -585,7 +586,7 @@ def run_with_retries( config: Optional[OrchestrationConfig] = None, template_values: Optional[List[TemplateValue]] = None, history: Optional[List[Message]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, max_retries: int = 10, base_delay: float = 1.0, ) -> OrchestrationResponseWithRetries | None: @@ -598,7 +599,7 @@ def run_with_retries( :param history: the message history, defaults to None :type history: Optional[List[Message]], optional :param timeout: the timeout for the request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :param max_retries: the maximum number of retry attempts, defaults to 10 :type max_retries: int, optional :param base_delay: the initial delay between retries in seconds, defaults to 1.0 @@ -626,7 +627,7 @@ def run_with_retries( retries=retry_count, ) - except (OrchestrationError, httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException) as error: + except (OrchestrationError, httpx2.HTTPStatusError, httpx2.ConnectError, httpx2.TimeoutException) as error: time.sleep(self.handle_retry(retry_count, base_delay, error, max_retries)) return None @@ -661,7 +662,7 @@ async def arun_with_retries( config: Optional[OrchestrationConfig] = None, template_values: Optional[List[TemplateValue]] = None, history: Optional[List[Message]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, max_retries: int = 10, base_delay: float = 1.0, ) -> OrchestrationResponseWithRetries | None: @@ -675,7 +676,7 @@ async def arun_with_retries( :param history: the message history, defaults to None :type history: Optional[List[Message]], optional :param timeout: the timeout for the request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :param max_retries: the maximum number of retry attempts, defaults to 10 :type max_retries: int, optional :param base_delay: the initial delay between retries in seconds, defaults to 1.0 @@ -703,6 +704,6 @@ async def arun_with_retries( retries=retry_count, ) - except (OrchestrationError, httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException) as error: + except (OrchestrationError, httpx2.HTTPStatusError, httpx2.ConnectError, httpx2.TimeoutException) as error: await asyncio.sleep(self.handle_retry(retry_count, base_delay, error, max_retries)) return None diff --git a/packages/gen/gen_ai_hub/orchestration/sse_client.py b/packages/gen/gen_ai_hub/orchestration/sse_client.py index 26cb9a81..f90e38a0 100644 --- a/packages/gen/gen_ai_hub/orchestration/sse_client.py +++ b/packages/gen/gen_ai_hub/orchestration/sse_client.py @@ -11,7 +11,7 @@ from typing import Iterable, Iterator, AsyncIterator import dacite -import httpx +import httpx2 from gen_ai_hub.orchestration.exceptions import OrchestrationError from gen_ai_hub.orchestration.models.response import OrchestrationResponseStreaming @@ -34,7 +34,7 @@ def _parse_event_data(event_data: str, final_message: str) -> "OrchestrationResp if "code" in event: raise OrchestrationError( request_id=event.get("request_id"), - http_headers=httpx.Headers({}), + http_headers=httpx2.Headers({}), message=event.get("message"), code=event.get("code"), location=event.get("location"), @@ -49,7 +49,7 @@ def _parse_event_data(event_data: str, final_message: str) -> "OrchestrationResp class SSEClient: """ - A synchronous Server-Sent Events (SSE) client that wraps an httpx.Response for iterating + A synchronous Server-Sent Events (SSE) client that wraps an httpx2.Response for iterating over streaming responses. This client reads data chunks from the HTTP stream and parses each SSE event. @@ -59,8 +59,8 @@ class SSEClient: def __init__(self, response_cm, prefix: str = "data: ", final_message: str = "[DONE]"): """Initializes the SSEClient. - :param response_cm: An httpx.Response context manager for the streaming response. - :type response_cm: httpx.Response + :param response_cm: An httpx2.Response context manager for the streaming response. + :type response_cm: httpx2.Response :param prefix: The prefix string that identifies SSE event data, defaults to "data: " :type prefix: str, optional :param final_message: The message that indicates the end of the stream, defaults to "[DONE]" @@ -86,9 +86,9 @@ def __enter__(self): self._response = self.response_cm.__enter__() try: self._response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: content = self._response.read() - error_response = httpx.Response( + error_response = httpx2.Response( status_code=self._response.status_code, headers=self._response.headers, content=content, @@ -169,7 +169,7 @@ def __init__(self, response_cm, prefix: str = "data: ", final_message: str = "[D """Initializes the AsyncSSEClient. :param response_cm: An asynchronous context manager for the HTTP streaming response. - :type response_cm: the type of an async context manager returning httpx.Response + :type response_cm: the type of an async context manager returning httpx2.Response :param prefix: The SSE data prefix, defaults to "data: " :type prefix: str, optional :param final_message: The message indicating the end of the stream, defaults to "[DONE]" @@ -194,9 +194,9 @@ async def __aenter__(self): self._response = await self.response_cm.__aenter__() try: self._response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: content = await self._response.aread() - error_response = httpx.Response( + error_response = httpx2.Response( status_code=self._response.status_code, headers=self._response.headers, content=content, @@ -276,13 +276,13 @@ async def __anext__(self): raise StopAsyncIteration -def _handle_http_error(error, response: httpx.Response): +def _handle_http_error(error, response: httpx2.Response): """Handles HTTP errors by raising an OrchestrationError with details from the response. :param error: the original HTTP error. - :type error: httpx.HTTPStatusError - :param response: the httpx.Response object containing error details incl. headers. - :type response: httpx.Response + :type error: httpx2.HTTPStatusError + :param response: the httpx2.Response object containing error details incl. headers. + :type response: httpx2.Response :raises OrchestrationError: with information extracted from the response. """ if not response.content: diff --git a/packages/gen/gen_ai_hub/orchestration_v2/exceptions.py b/packages/gen/gen_ai_hub/orchestration_v2/exceptions.py index a905b4d4..a0a5415f 100644 --- a/packages/gen/gen_ai_hub/orchestration_v2/exceptions.py +++ b/packages/gen/gen_ai_hub/orchestration_v2/exceptions.py @@ -4,7 +4,7 @@ from gen_ai_hub.orchestration_v2.models.response import ModuleResults -import httpx +import httpx2 from typing import Optional @@ -18,7 +18,7 @@ class OrchestrationError(Exception): def __init__( self, request_id: str, - headers: httpx.Headers, + headers: httpx2.Headers, message: str, code: int, location: str, @@ -30,7 +30,7 @@ def __init__( :param request_id: unique identifier for the request that encountered the error. :type request_id: str :param headers: HTTP headers associated with the request, useful in case of e.g. rate limiting.. - :type headers: httpx.Headers + :type headers: httpx2.Headers :param message: Detailed error message describing the issue. :type message: str :param code: Error code associated with the specific type of failure. diff --git a/packages/gen/gen_ai_hub/orchestration_v2/service.py b/packages/gen/gen_ai_hub/orchestration_v2/service.py index e9732ade..48066f9d 100644 --- a/packages/gen/gen_ai_hub/orchestration_v2/service.py +++ b/packages/gen/gen_ai_hub/orchestration_v2/service.py @@ -13,7 +13,8 @@ from functools import wraps from typing import List, Optional, Iterable, Union -import httpx +import httpx2 +from gen_ai_hub._ssl import default_ssl_context from ai_api_client_sdk.models.status import Status from gen_ai_hub import GenAIHubProxyClient @@ -189,7 +190,7 @@ def __init__(self, deployment_id: Optional[str] = None, config_name: Optional[str] = None, config_id: Optional[str] = None, - timeout: Union[int, float, httpx.Timeout, None] = None): + timeout: Union[int, float, httpx2.Timeout, None] = None): """Initializes the OrchestrationService. :param api_url: the base URL for the orchestration API, defaults to None @@ -207,7 +208,7 @@ def __init__(self, :param config_id: the configuration ID, defaults to None :type config_id: Optional[str], optional :param timeout: the timeout for HTTP requests, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :raises ValueError: if both config and config_ref are provided. """ self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") @@ -221,10 +222,10 @@ def __init__(self, raise ValueError(CONFIG_AND_CONFIG_REF_ERROR_TEXT) self.timeout = timeout # create reusable httpx client to improve performance - self.client = httpx.Client(timeout=self.timeout) - self.async_client = httpx.AsyncClient(timeout=self.timeout) + self.client = httpx2.Client(timeout=self.timeout, verify=default_ssl_context()) + self.async_client = httpx2.AsyncClient(timeout=self.timeout, verify=default_ssl_context()) - def _determine_timeout(self, timeout: httpx.Timeout) -> httpx.Timeout: + def _determine_timeout(self, timeout: httpx2.Timeout) -> httpx2.Timeout: # Determine the timeout to use for this request if timeout is not None: # Overwrite default timeout for this request @@ -234,7 +235,7 @@ def _determine_timeout(self, timeout: httpx.Timeout) -> httpx.Timeout: request_timeout = self.timeout else: # If timeout is not set, use httpx client's default behavior, rather than "None" (disables timeout) - request_timeout = httpx.USE_CLIENT_DEFAULT + request_timeout = httpx2.USE_CLIENT_DEFAULT return request_timeout def _should_retry(self, error: Exception) -> bool: @@ -247,7 +248,7 @@ def _should_retry(self, error: Exception) -> bool: Returns: True if the error is retryable (only 429 rate limit errors), False otherwise. """ - if isinstance(error, httpx.HTTPStatusError): + if isinstance(error, httpx2.HTTPStatusError): return error.response.status_code == 429 return False @@ -261,7 +262,7 @@ def _get_retry_after(self, error: Exception) -> Optional[float]: Returns: Number of seconds to wait before retrying, or None if not specified. """ - if isinstance(error, httpx.HTTPStatusError) and error.response.status_code == 429: + if isinstance(error, httpx2.HTTPStatusError) and error.response.status_code == 429: retry_after = error.response.headers.get('Retry-After') if retry_after: try: @@ -309,7 +310,7 @@ def _execute_request( config_ref: Optional[OrchestrationConfigReference] = None, placeholder_values: Optional[dict] = None, history: Optional[List[ChatMessage]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, stream: bool = False, ) -> Union[CompletionPostResponse | Iterable[StreamCompletionPostResponse]]: """ @@ -376,7 +377,7 @@ def _execute_request( ) try: response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: _handle_http_error(error, response) data = response.json() @@ -388,7 +389,7 @@ async def _a_execute_request( config_ref: Optional[OrchestrationConfigReference] = None, placeholder_values: Optional[dict] = None, history: Optional[List[ChatMessage]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, stream: bool = False, ) -> Union[CompletionPostResponse | AsyncSSEClient]: """ @@ -451,7 +452,7 @@ async def _a_execute_request( ) try: response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: _handle_http_error(error, response) data = response.json() @@ -463,7 +464,7 @@ def run( config_ref: Optional[OrchestrationConfigReference] = None, placeholder_values: Optional[dict] = None, history: Optional[List[ChatMessage]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> CompletionPostResponse: """Executes an orchestration request synchronously (non-streaming). @@ -477,7 +478,7 @@ def run( :param history: the message history, defaults to None :type history: Optional[List[ChatMessage]], optional :param timeout: the timeout overwrite per request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :return: the CompletionPostResponse object :rtype: CompletionPostResponse """ @@ -496,7 +497,7 @@ def stream( config_ref: Optional[OrchestrationConfigReference] = None, placeholder_values: Optional[dict] = None, history: Optional[List[ChatMessage]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> Iterable[StreamCompletionPostResponse]: """Executes an orchestration streaming request synchronously. @@ -510,7 +511,7 @@ def stream( :param history: the message history, defaults to None :type history: Optional[List[ChatMessage]], optional :param timeout: the timeout overwrite per request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :return: An Iterable[StreamCompletionPostResponse] object :rtype: Iterable[StreamCompletionPostResponse] """ @@ -530,7 +531,7 @@ async def arun( config_ref: Optional[OrchestrationConfigReference] = None, placeholder_values: Optional[dict] = None, history: Optional[List[ChatMessage]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> CompletionPostResponse: """Executes an orchestration request asynchronously (non-streaming). @@ -543,7 +544,7 @@ async def arun( :param history: the message history, defaults to None :type history: Optional[List[ChatMessage]], optional :param timeout: the timeout overwrite per request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :return: the CompletionPostResponse object :rtype: CompletionPostResponse """ @@ -562,7 +563,7 @@ async def astream( config_ref: Optional[OrchestrationConfigReference] = None, placeholder_values: Optional[dict] = None, history: Optional[List[ChatMessage]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> AsyncSSEClient: """Executes an orchestration streaming request asynchronously. @@ -575,7 +576,7 @@ async def astream( :param history: the message history, defaults to None :type history: Optional[List[ChatMessage]], optional :param timeout: the timeout overwrite per request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :return: the AsyncSSEClient object :rtype: AsyncSSEClient """ @@ -595,7 +596,7 @@ def run_with_retries( config_ref: Optional[OrchestrationConfigReference] = None, placeholder_values: Optional[dict] = None, history: Optional[List[ChatMessage]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, max_retries: int = 10, base_delay: float = 1.0, ) -> OrchestrationResponseWithRetries | None: @@ -610,7 +611,7 @@ def run_with_retries( :param history: the message history, defaults to None :type history: Optional[List[ChatMessage]], optional :param timeout: the timeout overwrite per request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :param max_retries: the maximum number of retry attempts, defaults to 10 :type max_retries: int, optional :param base_delay: the initial delay between retries in seconds, defaults to 1.0 @@ -641,7 +642,7 @@ def run_with_retries( retries=retry_count, ) - except (OrchestrationError, httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException) as error: + except (OrchestrationError, httpx2.HTTPStatusError, httpx2.ConnectError, httpx2.TimeoutException) as error: time.sleep(self.handle_retry(retry_count, base_delay, error, max_retries)) return None @@ -678,7 +679,7 @@ async def arun_with_retries( config_ref: Optional[OrchestrationConfigReference] = None, placeholder_values: Optional[dict] = None, history: Optional[List[ChatMessage]] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, max_retries: int = 10, base_delay: float = 1.0, ) -> OrchestrationResponseWithRetries | None: @@ -694,7 +695,7 @@ async def arun_with_retries( :param history: the message history, defaults to None :type history: Optional[List[ChatMessage]], optional :param timeout: the timeout overwrite per request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :param max_retries: the maximum number of retry attempts, defaults to 10 :type max_retries: int, optional :param base_delay: the initial delay between retries in seconds, defaults to 1.0 @@ -724,7 +725,7 @@ async def arun_with_retries( retries=retry_count, ) - except (OrchestrationError, httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException) as error: + except (OrchestrationError, httpx2.HTTPStatusError, httpx2.ConnectError, httpx2.TimeoutException) as error: await asyncio.sleep(self.handle_retry(retry_count, base_delay, error, max_retries)) return None @@ -732,7 +733,7 @@ def embed( self, config: EmbeddingsOrchestrationConfig, input: EmbeddingsInput, # pylint: disable=redefined-builtin - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> EmbeddingsPostResponse: """Executes an embeddings request synchronously. @@ -741,7 +742,7 @@ def embed( :param input: the input text to embed :type input: EmbeddingsInput :param timeout: the timeout overwrite per request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :return: the EmbeddingsPostResponse object :rtype: EmbeddingsPostResponse """ @@ -758,7 +759,7 @@ def embed( ) try: response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: _handle_http_error(error, response) data = response.json() @@ -768,7 +769,7 @@ async def aembed( self, config: EmbeddingsOrchestrationConfig, input: EmbeddingsInput, # pylint: disable=redefined-builtin - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> EmbeddingsPostResponse: """Executes an embeddings request asynchronously. @@ -777,7 +778,7 @@ async def aembed( :param input: the input text to embed :type input: EmbeddingsInput :param timeout: the timeout overwrite per request, defaults to None - :type timeout: Union[int, float, httpx.Timeout, None], optional + :type timeout: Union[int, float, httpx2.Timeout, None], optional :return: the EmbeddingsPostResponse object :rtype: EmbeddingsPostResponse """ @@ -794,7 +795,7 @@ async def aembed( ) try: response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: _handle_http_error(error, response) data = response.json() diff --git a/packages/gen/gen_ai_hub/orchestration_v2/sse_client.py b/packages/gen/gen_ai_hub/orchestration_v2/sse_client.py index 5bf18ab9..a1b27f8d 100644 --- a/packages/gen/gen_ai_hub/orchestration_v2/sse_client.py +++ b/packages/gen/gen_ai_hub/orchestration_v2/sse_client.py @@ -10,7 +10,7 @@ import json from typing import Iterable, Iterator, AsyncIterator -import httpx +import httpx2 from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError, OrchestrationErrorList from gen_ai_hub.orchestration_v2.models.response import StreamCompletionPostResponse @@ -39,7 +39,7 @@ def _parse_event_data(event_data: str, final_message: str) -> "StreamCompletionP if isinstance(error_event, dict): raise OrchestrationError( request_id=error_event.get("request_id"), - headers=httpx.Headers({}), + headers=httpx2.Headers({}), message=error_event.get("message"), code=error_event.get("code"), location=error_event.get("location"), @@ -49,7 +49,7 @@ def _parse_event_data(event_data: str, final_message: str) -> "StreamCompletionP errors = [ OrchestrationError( request_id=e.get("request_id"), - headers=httpx.Headers({}), + headers=httpx2.Headers({}), message=e.get("message"), code=e.get("code"), location=e.get("location"), @@ -64,7 +64,7 @@ def _parse_event_data(event_data: str, final_message: str) -> "StreamCompletionP class SSEClient: """ - A synchronous Server-Sent Events (SSE) client that wraps an httpx.Response for iterating + A synchronous Server-Sent Events (SSE) client that wraps an httpx2.Response for iterating over streaming responses. This client reads data chunks from the HTTP stream and parses each SSE event. @@ -74,8 +74,8 @@ class SSEClient: def __init__(self, response_cm, prefix: str = "data: ", final_message: str = "[DONE]"): """Initializes the SSEClient. - :param response_cm: An httpx.Response context manager for the streaming response. - :type response_cm: httpx.Response + :param response_cm: An httpx2.Response context manager for the streaming response. + :type response_cm: httpx2.Response :param prefix: The prefix string that identifies SSE event data, defaults to data: :type prefix: str, optional :param final_message: The message that indicates the end of the stream, defaults to [DONE] @@ -101,9 +101,9 @@ def __enter__(self): self._response = self.response_cm.__enter__() try: self._response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: content = self._response.read() - error_response = httpx.Response( + error_response = httpx2.Response( status_code=self._response.status_code, headers=self._response.headers, content=content, @@ -187,7 +187,7 @@ def __init__(self, response_cm, prefix: str = "data: ", final_message: str = "[D """Initializes the AsyncSSEClient. :param response_cm: An asynchronous context manager for the HTTP streaming response. - :type response_cm: typing.AsyncContextManager[httpx.Response] + :type response_cm: typing.AsyncContextManager[httpx2.Response] :param prefix: the SSE data prefix, defaults to "data: " :type prefix: str, optional :param final_message: the message indicating the end of the stream, defaults to "[DONE]" @@ -213,9 +213,9 @@ async def __aenter__(self): self._response = await self.response_cm.__aenter__() try: self._response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: content = await self._response.aread() - error_response = httpx.Response( + error_response = httpx2.Response( status_code=self._response.status_code, headers=self._response.headers, content=content, @@ -297,13 +297,13 @@ async def __anext__(self): raise StopAsyncIteration -def _handle_http_error(error, response: httpx.Response): +def _handle_http_error(error, response: httpx2.Response): """ Handles HTTP errors by raising an OrchestrationError with details from the response. Args: error: The original HTTP error. - response: The httpx.Response object containing error details incl. headers. + response: The httpx2.Response object containing error details incl. headers. Raises: OrchestrationError with information extracted from the response. diff --git a/packages/gen/gen_ai_hub/proxy/native/google_genai/clients.py b/packages/gen/gen_ai_hub/proxy/native/google_genai/clients.py index 4dd6f635..a109d54e 100644 --- a/packages/gen/gen_ai_hub/proxy/native/google_genai/clients.py +++ b/packages/gen/gen_ai_hub/proxy/native/google_genai/clients.py @@ -1,5 +1,5 @@ from typing import Optional, Union -import httpx +import httpx2 from google.genai import Client as GoogleClient from google.genai import types from google.genai.models import Models as GoogleModels @@ -32,7 +32,7 @@ def _resolve_deployment(transport_instance, requested_model_name: str): return deployment -def _rewrite_request(transport_instance, request: httpx.Request): +def _rewrite_request(transport_instance, request: httpx2.Request): """Interception and rewriting of the request. Handles dynamic routing, header injection, etc.""" path = request.url.path @@ -55,7 +55,7 @@ def _rewrite_request(transport_instance, request: httpx.Request): deployment = _resolve_deployment(transport_instance, model_name) - deployment_url = httpx.URL(deployment.url) + deployment_url = httpx2.URL(deployment.url) # Path construction: deployment_url + /models/ + {model_name}:generateContent new_path = f"{deployment_url.path.rstrip('/')}/models/{suffix}" @@ -85,7 +85,7 @@ def _rewrite_request(transport_instance, request: httpx.Request): return request -class AICoreDynamicTransport(httpx.BaseTransport): +class AICoreDynamicTransport(httpx2.BaseTransport): """Synchronous transport that dynamically resolves deployment URLs per request.""" def __init__(self, proxy_client: BaseProxyClient, **deployment_selector_kwargs): @@ -95,7 +95,7 @@ def __init__(self, proxy_client: BaseProxyClient, **deployment_selector_kwargs): :type proxy_client: BaseProxyClient """ self.proxy_client = proxy_client - self._inner_transport = httpx.HTTPTransport() + self._inner_transport = httpx2.HTTPTransport() self._selector_kwargs = kwargs_if_set(**deployment_selector_kwargs) def get_selector_kwargs(self): @@ -106,13 +106,13 @@ def get_selector_kwargs(self): """ return self._selector_kwargs - def handle_request(self, request: httpx.Request) -> httpx.Response: + def handle_request(self, request: httpx2.Request) -> httpx2.Response: """Handles the request by rewriting it to route through the appropriate AI Core deployment. :param request: The original HTTPX request. - :type request: httpx.Request + :type request: httpx2.Request :return: The HTTPX response from the AI Core deployment. - :rtype: httpx.Response + :rtype: httpx2.Response """ modified_request = _rewrite_request(self, request) return self._inner_transport.handle_request(modified_request) @@ -122,7 +122,7 @@ def close(self): self._inner_transport.close() -class AsyncAICoreDynamicTransport(httpx.AsyncBaseTransport): +class AsyncAICoreDynamicTransport(httpx2.AsyncBaseTransport): """Asynchronous transport that dynamically resolves deployment URLs per request.""" def __init__(self, proxy_client: BaseProxyClient, **deployment_selector_kwargs): @@ -132,7 +132,7 @@ def __init__(self, proxy_client: BaseProxyClient, **deployment_selector_kwargs): :type proxy_client: BaseProxyClient """ self.proxy_client = proxy_client - self._inner_transport = httpx.AsyncHTTPTransport() + self._inner_transport = httpx2.AsyncHTTPTransport() self._selector_kwargs = kwargs_if_set(**deployment_selector_kwargs) def get_selector_kwargs(self): @@ -143,13 +143,13 @@ def get_selector_kwargs(self): """ return self._selector_kwargs - async def handle_async_request(self, request: httpx.Request) -> httpx.Response: + async def handle_async_request(self, request: httpx2.Request) -> httpx2.Response: """Handles the request by rewriting it to route through the appropriate AI Core deployment. :param request: The original HTTPX request. - :type request: httpx.Request + :type request: httpx2.Request :return: The HTTPX response from the AI Core deployment. - :rtype: httpx.Response + :rtype: httpx2.Response """ modified_request = _rewrite_request(self, request) return await self._inner_transport.handle_async_request(modified_request) @@ -230,18 +230,17 @@ def __init__( **deployment_selector_kwargs ) + sync_http_client = httpx2.Client(transport=sync_transport) + async_http_client = httpx2.AsyncClient(transport=async_transport) + super().__init__( vertexai=True, project=project, location=location, credentials=Credentials(token="dummy-token-placeholder"), http_options=types.HttpOptions( - client_args={ - "transport": sync_transport - }, - async_client_args={ - "transport": async_transport - }, + httpx_client=sync_http_client, + httpx_async_client=async_http_client, timeout=timeout, ), **kwargs diff --git a/packages/gen/gen_ai_hub/proxy/native/openai/clients.py b/packages/gen/gen_ai_hub/proxy/native/openai/clients.py index 0e0d58c6..fb919557 100644 --- a/packages/gen/gen_ai_hub/proxy/native/openai/clients.py +++ b/packages/gen/gen_ai_hub/proxy/native/openai/clients.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from typing import Optional, Union, List, TypeVar, Iterable -import httpx +import httpx2 from openai import AsyncOpenAI as AsyncOpenAI_ from openai import OpenAI as OpenAI_ from openai import resources @@ -965,15 +965,15 @@ def __init__(self, client: AsyncOpenAI) -> None: self.beta = None -def _prepare_url(url: str) -> httpx.URL: +def _prepare_url(url: str) -> httpx2.URL: deployment = get_current_deployment() prediction_url = deployment.prediction_url if prediction_url: - return httpx.URL(prediction_url) + return httpx2.URL(prediction_url) - url = httpx.URL(url) + url = httpx2.URL(url) if url.is_relative_url: - deployment_url = httpx.URL(get_current_deployment().url.rstrip('/') + '/') + deployment_url = httpx2.URL(get_current_deployment().url.rstrip('/') + '/') url = deployment_url.raw_path + url.raw_path.lstrip(b"/") return deployment_url.copy_with(raw_path=url) return url @@ -1056,7 +1056,7 @@ def default_headers(self) -> dict[str, str | Omit]: headers.update(self.proxy_client.request_header) return headers - def _prepare_url(self, url: str) -> httpx.URL: + def _prepare_url(self, url: str) -> httpx2.URL: return _prepare_url(url) def request(self, cast_to, options, *args, **kwargs): @@ -1141,7 +1141,7 @@ def default_headers(self) -> dict[str, str | Omit]: headers.update(self.proxy_client.request_header) return headers - def _prepare_url(self, url: str) -> httpx.URL: + def _prepare_url(self, url: str) -> httpx2.URL: return _prepare_url(url) def request(self, cast_to, options, *args, **kwargs): diff --git a/packages/gen/gen_ai_hub/proxy/native/sap/client.py b/packages/gen/gen_ai_hub/proxy/native/sap/client.py index 45345cf0..004ef39f 100644 --- a/packages/gen/gen_ai_hub/proxy/native/sap/client.py +++ b/packages/gen/gen_ai_hub/proxy/native/sap/client.py @@ -1,5 +1,6 @@ from typing import Optional, Union -import httpx +import httpx2 +from gen_ai_hub._ssl import default_ssl_context from gen_ai_hub import GenAIHubProxyClient from gen_ai_hub.proxy import get_proxy_client @@ -7,7 +8,7 @@ PREDICTION_SUFFIX = "/predict" -def _handle_http_error(error, response: httpx.Response): +def _handle_http_error(error, response: httpx2.Response): if not response.content: raise error @@ -35,20 +36,20 @@ class RPTClient: :type proxy_client: Optional[GenAIHubProxyClient] :param timeout: Default timeout value for the HTTP client used for requests. - :type timeout: Union[int, float, httpx.Timeout, None] + :type timeout: Union[int, float, httpx2.Timeout, None] """ def __init__( self, proxy_client: Optional[GenAIHubProxyClient] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ): self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") self.timeout = timeout - self.client = httpx.Client(timeout=self.timeout) - self.async_client = httpx.AsyncClient(timeout=self.timeout) + self.client = httpx2.Client(timeout=self.timeout, verify=default_ssl_context()) + self.async_client = httpx2.AsyncClient(timeout=self.timeout, verify=default_ssl_context()) - def _determine_timeout(self, timeout: httpx.Timeout) -> httpx.Timeout: + def _determine_timeout(self, timeout: httpx2.Timeout) -> httpx2.Timeout: # Determine the timeout to use for this request if timeout is not None: # Overwrite default timeout for this request @@ -58,14 +59,14 @@ def _determine_timeout(self, timeout: httpx.Timeout) -> httpx.Timeout: request_timeout = self.timeout else: # If timeout is not set, use httpx client's default behavior, rather than "None" (disables timeout) - request_timeout = httpx.USE_CLIENT_DEFAULT + request_timeout = httpx2.USE_CLIENT_DEFAULT return request_timeout def _execute_request( self, body: RPTRequest, api_url: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> RPTResponse: """ Executes an HTTP POST request to a prediction API endpoint with the given @@ -82,8 +83,8 @@ def _execute_request( :type api_url: str :param timeout: The timeout configuration for the HTTP request. Can be an - integer, float, httpx.Timeout object, or None. - :type timeout: Union[int, float, httpx.Timeout, None] + integer, float, httpx2.Timeout object, or None. + :type timeout: Union[int, float, httpx2.Timeout, None] :returns: A response object representing the data returned from the API after successful execution. @@ -100,7 +101,7 @@ def _execute_request( ) try: response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: _handle_http_error(error, response) data = response.json() @@ -110,7 +111,7 @@ async def _a_execute_request( self, body: RPTRequest, api_url: str, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, ) -> RPTResponse: """ Asynchronously executes an HTTP POST request to a prediction API endpoint @@ -127,8 +128,8 @@ async def _a_execute_request( :type api_url: str :param timeout: The timeout configuration for the HTTP request. Can be an - integer, float, httpx.Timeout object, or None. - :type timeout: Union[int, float, httpx.Timeout, None] + integer, float, httpx2.Timeout object, or None. + :type timeout: Union[int, float, httpx2.Timeout, None] :returns: A response object representing the data returned from the API after successful execution. @@ -145,7 +146,7 @@ async def _a_execute_request( ) try: response.raise_for_status() - except httpx.HTTPStatusError as error: + except httpx2.HTTPStatusError as error: _handle_http_error(error, response) data = response.json() @@ -202,7 +203,7 @@ def predict(self, deployment_url: Optional[str] = None, model_name: Optional[str] = None, model_version: Optional[str] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, **kwargs) -> RPTResponse: """ Executes a prediction request by sending the provided data and deployment parameters. @@ -224,8 +225,8 @@ def predict(self, Could be provided only if `model_name` is provided. :type model_version: Optional[str] :param timeout: The time duration to wait for the prediction request to complete. - Can be an integer, float, or an instance of `httpx.Timeout`. - :type timeout: Union[int, float, httpx.Timeout, None] + Can be an integer, float, or an instance of `httpx2.Timeout`. + :type timeout: Union[int, float, httpx2.Timeout, None] :returns: The response received from the prediction endpoint, represented as an `RPTResponse` object. @@ -245,7 +246,7 @@ async def apredict(self, deployment_url: Optional[str] = None, model_name: Optional[str] = None, model_version: Optional[str] = None, - timeout: Union[int, float, httpx.Timeout, None] = None, + timeout: Union[int, float, httpx2.Timeout, None] = None, **kwargs) -> RPTResponse: """ Asynchronously executes a prediction request by sending the provided data and deployment parameters. @@ -269,8 +270,8 @@ async def apredict(self, :type model_version: Optional[str] :param timeout: The time duration to wait for the prediction request to complete. - Can be an integer, float, or an instance of `httpx.Timeout`. - :type timeout: Union[int, float, httpx.Timeout, None] + Can be an integer, float, or an instance of `httpx2.Timeout`. + :type timeout: Union[int, float, httpx2.Timeout, None] :returns: The response received from the prediction endpoint, represented as an `RPTResponse` object. diff --git a/packages/gen/integration_tests/orchestration/test_async.py b/packages/gen/integration_tests/orchestration/test_async.py index 9f75a149..6e785061 100644 --- a/packages/gen/integration_tests/orchestration/test_async.py +++ b/packages/gen/integration_tests/orchestration/test_async.py @@ -1,6 +1,6 @@ import unittest -from httpx import TimeoutException +from httpx2 import TimeoutException from gen_ai_hub.orchestration.exceptions import OrchestrationError from gen_ai_hub.orchestration.models.config import OrchestrationConfig diff --git a/packages/gen/integration_tests/orchestration/test_service.py b/packages/gen/integration_tests/orchestration/test_service.py index 3414491f..df64fb4d 100644 --- a/packages/gen/integration_tests/orchestration/test_service.py +++ b/packages/gen/integration_tests/orchestration/test_service.py @@ -1,4 +1,4 @@ -from httpx import TimeoutException +from httpx2 import TimeoutException from gen_ai_hub.orchestration.models.config import OrchestrationConfig from gen_ai_hub.orchestration.models.llm import LLM from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage diff --git a/packages/gen/integration_tests/orchestration_v2/test_async.py b/packages/gen/integration_tests/orchestration_v2/test_async.py index 7ca790d4..218154a2 100644 --- a/packages/gen/integration_tests/orchestration_v2/test_async.py +++ b/packages/gen/integration_tests/orchestration_v2/test_async.py @@ -1,6 +1,6 @@ import unittest -from httpx import TimeoutException +from httpx2 import TimeoutException from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError from gen_ai_hub.orchestration_v2.models.config import (OrchestrationConfig, ModuleConfig, diff --git a/packages/gen/integration_tests/orchestration_v2/test_service.py b/packages/gen/integration_tests/orchestration_v2/test_service.py index f91456f4..6597e578 100644 --- a/packages/gen/integration_tests/orchestration_v2/test_service.py +++ b/packages/gen/integration_tests/orchestration_v2/test_service.py @@ -1,6 +1,6 @@ import requests import time -from httpx import TimeoutException +from httpx2 import TimeoutException from gen_ai_hub.orchestration_v2.models.config import (OrchestrationConfig, ModuleConfig, CompletionRequestConfigurationReferenceByIdConfigRef, CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef) diff --git a/packages/gen/pyproject.toml b/packages/gen/pyproject.toml index b030f71b..fd3ebee6 100644 --- a/packages/gen/pyproject.toml +++ b/packages/gen/pyproject.toml @@ -32,7 +32,8 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "httpx>=0.27.0", + "httpx2>=2.0.0", + "certifi", "h11>=0.16.0", "dacite>=1.8.1", "click>=8.1.7", @@ -40,7 +41,7 @@ dependencies = [ "packaging>=23.2", "sap-ai-sdk-core>=3.4.0", "pydantic~=2.12", - "openai>=1.66.0", + "openai>=3.0.0", "langcodes~=3.5.1", "pandas>=2.2.0", "langchain~=1.4.0", @@ -60,7 +61,7 @@ dev = [ "pytest-asyncio==1.4.0", "pylint==4.0.8", "requests-mock==1.12.1", - "respx==0.23.1", + "httpx2-pytest", "parameterized==0.9.0", "pillow==12.3.0", "sphinx<9.0.0", diff --git a/packages/gen/tests/batch_service/test_batch_service.py b/packages/gen/tests/batch_service/test_batch_service.py index 520531fe..1ef570cb 100644 --- a/packages/gen/tests/batch_service/test_batch_service.py +++ b/packages/gen/tests/batch_service/test_batch_service.py @@ -5,8 +5,8 @@ import unittest from unittest.mock import patch -import httpx -from httpx import Response +import httpx2 +from httpx2 import Response from gen_ai_hub.batch_service.exceptions import BatchServiceError from gen_ai_hub.batch_service.models.response import ( @@ -171,7 +171,7 @@ def capture_post(url, **kwargs): with patch.object(self.client.client, "post", side_effect=capture_post): self.client.create(type="llm-native", input_uri="ai://x", output_uri="ai://y", provider="p", model="m") - self.assertEqual(captured["timeout"], httpx.USE_CLIENT_DEFAULT) + self.assertEqual(captured["timeout"], httpx2.USE_CLIENT_DEFAULT) def test_timeout_priority_service_default(self): client = BatchService(proxy_client=self.proxy_client, timeout=99.0) diff --git a/packages/gen/tests/batch_service/test_batch_service_async.py b/packages/gen/tests/batch_service/test_batch_service_async.py index e1862c7c..daa7f998 100644 --- a/packages/gen/tests/batch_service/test_batch_service_async.py +++ b/packages/gen/tests/batch_service/test_batch_service_async.py @@ -5,7 +5,7 @@ import unittest from unittest.mock import AsyncMock, patch -import httpx +import httpx2 from gen_ai_hub.batch_service.models.response import ( BatchCreateResponse, @@ -85,12 +85,12 @@ async def test_async_timeout_no_timeout_set(self): async def capture_post(url, **kwargs): captured["timeout"] = kwargs.get("timeout") - return httpx.Response(202, json=CREATE_RESPONSE) + return httpx2.Response(202, json=CREATE_RESPONSE) with patch.object(self.client.async_client, "post", new=AsyncMock(side_effect=capture_post)): await self.client.acreate(input_uri="ai://x", output_uri="ai://y", provider="p", model="m") - self.assertEqual(captured["timeout"], httpx.USE_CLIENT_DEFAULT) + self.assertEqual(captured["timeout"], httpx2.USE_CLIENT_DEFAULT) async def test_async_timeout_service_default(self): client = BatchService(proxy_client=self.proxy_client, timeout=55.0) @@ -98,7 +98,7 @@ async def test_async_timeout_service_default(self): async def capture_post(url, **kwargs): captured["timeout"] = kwargs.get("timeout") - return httpx.Response(202, json=CREATE_RESPONSE) + return httpx2.Response(202, json=CREATE_RESPONSE) with patch.object(client.async_client, "post", new=AsyncMock(side_effect=capture_post)): await client.acreate(input_uri="ai://x", output_uri="ai://y", provider="p", model="m") @@ -111,7 +111,7 @@ async def test_async_timeout_per_request_overrides_default(self): async def capture_post(url, **kwargs): captured["timeout"] = kwargs.get("timeout") - return httpx.Response(202, json=CREATE_RESPONSE) + return httpx2.Response(202, json=CREATE_RESPONSE) with patch.object(client.async_client, "post", new=AsyncMock(side_effect=capture_post)): await client.acreate(input_uri="ai://x", output_uri="ai://y", provider="p", model="m", timeout=33.0) diff --git a/packages/gen/tests/mock.py b/packages/gen/tests/mock.py index 5633d326..97b66b13 100644 --- a/packages/gen/tests/mock.py +++ b/packages/gen/tests/mock.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re import asyncio import json import os @@ -9,8 +10,79 @@ import numpy as np import requests_mock -import respx -from httpx import Response, AsyncByteStream +import httpx2 as _httpx2_mod +from pytest_httpx2 import HTTPXMock as _HTTPXMockBase +from pytest_httpx2._options import _HTTPXMockOptions +from pytest_httpx2._httpx_internals import IteratorStream + + +class _HTTPXMock: + """ + Standalone context manager wrapping HTTPXMock for use inside + unittest.TestCase and @contextmanager helpers that cannot receive + pytest fixtures. + + Patches httpx2.HTTPTransport / httpx2.AsyncHTTPTransport for the + duration of the ``with`` block, then restores the originals and + asserts all registered responses were consumed (matching the + default fixture behaviour). + """ + + def __init__( + self, + *, + assert_all_responses_were_requested: bool = True, + assert_all_requests_were_expected: bool = True, + can_send_already_matched_responses: bool = True, + ) -> None: + options = _HTTPXMockOptions( + assert_all_responses_were_requested=assert_all_responses_were_requested, + assert_all_requests_were_expected=assert_all_requests_were_expected, + can_send_already_matched_responses=can_send_already_matched_responses, + ) + self._mock = _HTTPXMockBase(options) + self._real_handle_request = None + self._real_handle_async_request = None + + # --- public API (delegated to _HTTPXMockBase) --- + + def add_response(self, **kwargs): + self._mock.add_response(**kwargs) + + def add_callback(self, callback, **kwargs): + self._mock.add_callback(callback, **kwargs) + + # --- context manager --- + + def __enter__(self): + mock = self._mock + options = mock._options + self._real_handle_request = _httpx2_mod.HTTPTransport.handle_request + self._real_handle_async_request = _httpx2_mod.AsyncHTTPTransport.handle_async_request + _real_sync = self._real_handle_request + _real_async = self._real_handle_async_request + + def _mocked_sync(transport, request): + if options.should_mock(request): + return mock._handle_request(transport, request) + return _real_sync(transport, request) + + async def _mocked_async(transport, request): + if options.should_mock(request): + return await mock._handle_async_request(transport, request) + return await _real_async(transport, request) + + _httpx2_mod.HTTPTransport.handle_request = _mocked_sync + _httpx2_mod.AsyncHTTPTransport.handle_async_request = _mocked_async + return self + + def __exit__(self, *exc_info): + _httpx2_mod.HTTPTransport.handle_request = self._real_handle_request + _httpx2_mod.AsyncHTTPTransport.handle_async_request = self._real_handle_async_request + if exc_info[0] is None: + self._mock._assert_options() + self._mock.reset() +from httpx2 import Response, AsyncByteStream from gen_ai_hub.prompt_registry.models.prompt_template import (PromptTemplateSpec, PromptTemplateListResponse, PromptTemplateGetResponse, PromptTemplatePostResponse, @@ -843,14 +915,14 @@ def get_mocked_ai_core_client(client_id='XXX'): @contextmanager def orchestration_completion_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=GET_ORCHESTRATION_COMPLETION_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=200, json=GET_ORCHESTRATION_COMPLETION_RESPONSE) yield @contextmanager def orchestration_completion_v2_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=GET_ORCHESTRATION_V2_COMPLETION_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=200, json=GET_ORCHESTRATION_V2_COMPLETION_RESPONSE) yield GET_ORCHESTRATION_V2_EMBEDDINGS_RESPONSE = { @@ -933,36 +1005,35 @@ def orchestration_completion_v2_mocker(deployment_url): @contextmanager def orchestration_embeddings_v2_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=GET_ORCHESTRATION_V2_EMBEDDINGS_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=200, json=GET_ORCHESTRATION_V2_EMBEDDINGS_RESPONSE) yield @contextmanager def orchestration_embeddings_v2_batch_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=GET_ORCHESTRATION_V2_EMBEDDINGS_BATCH_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=200, json=GET_ORCHESTRATION_V2_EMBEDDINGS_BATCH_RESPONSE) yield @contextmanager def orchestration_embeddings_v2_with_masking_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=GET_ORCHESTRATION_V2_EMBEDDINGS_WITH_MASKING_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=200, json=GET_ORCHESTRATION_V2_EMBEDDINGS_WITH_MASKING_RESPONSE) yield @contextmanager def orchestration_deployment_not_found_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(404, content=b'deployment not found')) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=404, content=b'deployment not found') yield @contextmanager def orchestration_too_many_requests_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(429, headers={"X-Custom-Header": "value"}, - json={"error": {"message": "too many requests"}})) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=429, headers={"X-Custom-Header": "value"}, json={"error": {"message": "too many requests"}}) yield @@ -1124,18 +1195,14 @@ def generate_v2_events(): @contextmanager def orchestration_stream_completion_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock( - return_value=Response(200, stream=generate_events()) - ) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=200, stream=IteratorStream(generate_events())) yield @contextmanager def orchestration_stream_v2_completion_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock( - return_value=Response(200, stream=generate_v2_events()) - ) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=200, stream=IteratorStream(generate_v2_events())) yield # Wrap the synchronous generator in an async generator. @@ -1162,18 +1229,14 @@ async def __aiter__(self): @asynccontextmanager async def orchestration_stream_completion_mocker_async(deployment_url): - with respx.mock: - respx.post(deployment_url).mock( - return_value=Response(200, stream=AsyncIteratorStream(async_generate_events())) - ) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=200, stream=AsyncIteratorStream(async_generate_events())) yield @asynccontextmanager async def orchestration_v2_stream_completion_mocker_async(deployment_url): - with respx.mock: - respx.post(deployment_url).mock( - return_value=Response(200, stream=AsyncIteratorStream(async_generate_v2_events())) - ) + with _HTTPXMock() as _mock: + _mock.add_response(url=deployment_url, method="POST", status_code=200, stream=AsyncIteratorStream(async_generate_v2_events())) yield OPENAI_CHAT_COMPLETION_RESPONSE = { @@ -1222,15 +1285,15 @@ async def orchestration_v2_stream_completion_mocker_async(deployment_url): @contextmanager def openai_chat_completion_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_CHAT_COMPLETION_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=(re.compile(re.escape(deployment_url) + r'(\?.*)?$') if deployment_url else None), method="POST", status_code=200, json=OPENAI_CHAT_COMPLETION_RESPONSE) yield @contextmanager def cohere_chat_completion_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=COHERE_CHAT_COMPLETION_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=(re.compile(re.escape(deployment_url) + r'(\?.*)?$') if deployment_url else None), method="POST", status_code=200, json=COHERE_CHAT_COMPLETION_RESPONSE) yield @@ -1256,8 +1319,8 @@ def cohere_chat_completion_mocker(deployment_url): @contextmanager def openai_structured_outputs_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_STRUCTRED_OUTPUTS_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=(re.compile(re.escape(deployment_url) + r'(\?.*)?$') if deployment_url else None), method="POST", status_code=200, json=OPENAI_STRUCTRED_OUTPUTS_RESPONSE) yield @@ -1281,8 +1344,8 @@ def openai_structured_outputs_mocker(deployment_url): @contextmanager def openai_embeddings_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_EMBEDDINGS_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=(re.compile(re.escape(deployment_url) + r'(\?.*)?$') if deployment_url else None), method="POST", status_code=200, json=OPENAI_EMBEDDINGS_RESPONSE) yield @@ -1315,8 +1378,8 @@ def openai_embeddings_mocker(deployment_url): @contextmanager def openai_completion_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_GPT35_INSTRUCT_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=(re.compile(re.escape(deployment_url) + r'(\?.*)?$') if deployment_url else None), method="POST", status_code=200, json=OPENAI_GPT35_INSTRUCT_RESPONSE) yield RPT_RESPONSE_CODE_0 = { @@ -1485,8 +1548,8 @@ def openai_completion_mocker(deployment_url): @contextmanager def openai_responses_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_RESPONSES_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=(re.compile(re.escape(deployment_url) + r'(\?.*)?$') if deployment_url else None), method="POST", status_code=200, json=OPENAI_RESPONSES_RESPONSE) yield OPENAI_RESPONSES_RESPONSE_PARSE = { @@ -1656,14 +1719,14 @@ def openai_responses_mocker(deployment_url): @contextmanager def openai_responses_structured_outputs_mocker(deployment_url): - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_RESPONSES_RESPONSE_PARSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=(re.compile(re.escape(deployment_url) + r'(\?.*)?$') if deployment_url else None), method="POST", status_code=200, json=OPENAI_RESPONSES_RESPONSE_PARSE) yield @contextmanager def sap_rpt_moke_response_code_0(url: str): - with respx.mock: - respx.post(f"{url}/predict").mock(return_value=Response(200, json=RPT_RESPONSE_CODE_0)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{url}/predict", method="POST", status_code=200, json=RPT_RESPONSE_CODE_0) yield RPT_RESPONSE_CODE_2 = { @@ -1687,8 +1750,8 @@ def sap_rpt_moke_response_code_0(url: str): @contextmanager def sap_rpt_moke_response_code_2(url: str): - with respx.mock: - respx.post(f"{url}/predict").mock(return_value=Response(422, json=RPT_RESPONSE_CODE_2)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{url}/predict", method="POST", status_code=422, json=RPT_RESPONSE_CODE_2) yield @contextmanager @@ -1714,8 +1777,8 @@ def stream_events(*args): ) ) - with respx.mock: - respx.post(deployment_url).mock(return_value=Response(200, json=list(stream_events()))) + with _HTTPXMock() as _mock: + _mock.add_response(url=(re.compile(re.escape(deployment_url) + r'(\?.*)?$') if deployment_url else None), method="POST", status_code=200, json=list(stream_events())) yield @@ -2266,97 +2329,97 @@ def __aiter__(self): @contextmanager def batch_create_mocker(): - with respx.mock: - respx.post(BATCHES_URL).mock(return_value=Response(202, json=BATCH_CREATE_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=BATCHES_URL, method="POST", status_code=202, json=BATCH_CREATE_RESPONSE) yield @contextmanager def batch_list_mocker(): - with respx.mock: - respx.get(BATCHES_URL).mock(return_value=Response(200, json=BATCH_LIST_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=BATCHES_URL, method="GET", status_code=200, json=BATCH_LIST_RESPONSE) yield @contextmanager def batch_get_mocker(batch_id: str = BATCH_ID): - with respx.mock: - respx.get(f"{BATCHES_URL}/{batch_id}").mock(return_value=Response(200, json=BATCH_DETAIL_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{BATCHES_URL}/{batch_id}", method="GET", status_code=200, json=BATCH_DETAIL_RESPONSE) yield @contextmanager def batch_status_mocker(batch_id: str = BATCH_ID): - with respx.mock: - respx.get(f"{BATCHES_URL}/{batch_id}/status").mock(return_value=Response(200, json=BATCH_STATUS_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{BATCHES_URL}/{batch_id}/status", method="GET", status_code=200, json=BATCH_STATUS_RESPONSE) yield @contextmanager def batch_cancel_mocker(batch_id: str = BATCH_ID): - with respx.mock: - respx.patch(f"{BATCHES_URL}/{batch_id}/cancel").mock(return_value=Response(202, json=BATCH_CANCEL_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{BATCHES_URL}/{batch_id}/cancel", method="PATCH", status_code=202, json=BATCH_CANCEL_RESPONSE) yield @contextmanager def batch_delete_mocker(batch_id: str = BATCH_ID): - with respx.mock: - respx.delete(f"{BATCHES_URL}/{batch_id}").mock(return_value=Response(202, json=BATCH_DELETE_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{BATCHES_URL}/{batch_id}", method="DELETE", status_code=202, json=BATCH_DELETE_RESPONSE) yield @contextmanager def batch_not_found_mocker(batch_id: str = BATCH_ID): - with respx.mock: - respx.get(f"{BATCHES_URL}/{batch_id}").mock(return_value=Response(404, json=BATCH_ERROR_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{BATCHES_URL}/{batch_id}", method="GET", status_code=404, json=BATCH_ERROR_RESPONSE) yield @contextmanager def batch_create_error_mocker(): - with respx.mock: - respx.post(BATCHES_URL).mock(return_value=Response(400, json=BATCH_ERROR_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=BATCHES_URL, method="POST", status_code=400, json=BATCH_ERROR_RESPONSE) yield @asynccontextmanager async def batch_create_mocker_async(): - with respx.mock: - respx.post(BATCHES_URL).mock(return_value=Response(202, json=BATCH_CREATE_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=BATCHES_URL, method="POST", status_code=202, json=BATCH_CREATE_RESPONSE) yield @asynccontextmanager async def batch_list_mocker_async(): - with respx.mock: - respx.get(BATCHES_URL).mock(return_value=Response(200, json=BATCH_LIST_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=BATCHES_URL, method="GET", status_code=200, json=BATCH_LIST_RESPONSE) yield @asynccontextmanager async def batch_get_mocker_async(batch_id: str = BATCH_ID): - with respx.mock: - respx.get(f"{BATCHES_URL}/{batch_id}").mock(return_value=Response(200, json=BATCH_DETAIL_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{BATCHES_URL}/{batch_id}", method="GET", status_code=200, json=BATCH_DETAIL_RESPONSE) yield @asynccontextmanager async def batch_status_mocker_async(batch_id: str = BATCH_ID): - with respx.mock: - respx.get(f"{BATCHES_URL}/{batch_id}/status").mock(return_value=Response(200, json=BATCH_STATUS_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{BATCHES_URL}/{batch_id}/status", method="GET", status_code=200, json=BATCH_STATUS_RESPONSE) yield @asynccontextmanager async def batch_cancel_mocker_async(batch_id: str = BATCH_ID): - with respx.mock: - respx.patch(f"{BATCHES_URL}/{batch_id}/cancel").mock(return_value=Response(202, json=BATCH_CANCEL_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{BATCHES_URL}/{batch_id}/cancel", method="PATCH", status_code=202, json=BATCH_CANCEL_RESPONSE) yield @asynccontextmanager async def batch_delete_mocker_async(batch_id: str = BATCH_ID): - with respx.mock: - respx.delete(f"{BATCHES_URL}/{batch_id}").mock(return_value=Response(202, json=BATCH_DELETE_RESPONSE)) + with _HTTPXMock() as _mock: + _mock.add_response(url=f"{BATCHES_URL}/{batch_id}", method="DELETE", status_code=202, json=BATCH_DELETE_RESPONSE) yield diff --git a/packages/gen/tests/orchestration/test_service.py b/packages/gen/tests/orchestration/test_service.py index 77fbb00b..5b40a272 100644 --- a/packages/gen/tests/orchestration/test_service.py +++ b/packages/gen/tests/orchestration/test_service.py @@ -1,5 +1,5 @@ -import httpx +import httpx2 import unittest from unittest.mock import Mock, patch, AsyncMock from typing import cast @@ -90,7 +90,7 @@ def test_run_with_non_existing_deployment_id(self): with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): client = OrchestrationService(deployment_id=self.NOT_EXISTENT_DEPLOYMENT_ID, proxy_client=self.proxy_client) with orchestration_deployment_not_found_mocker(client.api_url + '/completion'): - with self.assertRaises(httpx.HTTPStatusError): + with self.assertRaises(httpx2.HTTPStatusError): client.run(config=self.config) def test_run_without_config(self): @@ -166,13 +166,13 @@ def test_handle_retry_with_retry_after(self): client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) # Create a mock error without Retry-After header - response = Mock(spec=httpx.Response) + response = Mock(spec=httpx2.Response) response.status_code = 429 - response.headers = httpx.Headers({"Retry-After": "3"}) + response.headers = httpx2.Headers({"Retry-After": "3"}) response.text = "Too Many Requests" response.request = Mock() - error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + error = httpx2.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) # Collect delays for multiple retries delays = [] @@ -191,13 +191,13 @@ def test_handle_retry_without_retry_after(self): client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) # Create a mock error without Retry-After header - response = Mock(spec=httpx.Response) + response = Mock(spec=httpx2.Response) response.status_code = 429 - response.headers = httpx.Headers({}) + response.headers = httpx2.Headers({}) response.text = "Too Many Requests" response.request = Mock() - error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + error = httpx2.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) # Collect delays for multiple retries delays = [] @@ -216,13 +216,13 @@ def test_handle_retry_max_retries_exceeded(self): client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) # Create a mock error - response = Mock(spec=httpx.Response) + response = Mock(spec=httpx2.Response) response.status_code = 429 - response.headers = httpx.Headers({}) + response.headers = httpx2.Headers({}) response.text = "Too Many Requests" response.request = Mock() - error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + error = httpx2.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) # Mock _should_retry to return True so we test the retry_count >= max_retries condition with patch.object(client, '_should_retry', return_value=True): @@ -230,8 +230,8 @@ def test_handle_retry_max_retries_exceeded(self): # Need to call handle_retry within an exception context since it uses bare 'raise' try: raise error - except httpx.HTTPStatusError as e: - with self.assertRaises(httpx.HTTPStatusError) as context: + except httpx2.HTTPStatusError as e: + with self.assertRaises(httpx2.HTTPStatusError) as context: client.handle_retry(retry_count=3, base_delay=1.0, error=e, max_retries=3) # Verify retries attribute was set @@ -356,7 +356,7 @@ def capture_request(*args, **kwargs): service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) with patch.object(service.client, "post", side_effect=capture_request): service.run(config=self.config) - self.assertEqual(timeout_captured.get("timeout"), httpx.USE_CLIENT_DEFAULT) + self.assertEqual(timeout_captured.get("timeout"), httpx2.USE_CLIENT_DEFAULT) # timeout set in httpx client, not overwritten in request service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) @@ -413,7 +413,7 @@ async def capture_request(*args, **kwargs): service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) with patch.object(service.async_client, "post", new=AsyncMock(side_effect=capture_request)): await service.arun(config=self.config) - self.assertEqual(timeout_captured.get("timeout"), httpx.USE_CLIENT_DEFAULT) + self.assertEqual(timeout_captured.get("timeout"), httpx2.USE_CLIENT_DEFAULT) # timeout set in httpx client, not overwritten in request service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) diff --git a/packages/gen/tests/orchestration/test_sse_client.py b/packages/gen/tests/orchestration/test_sse_client.py index 4bf8a51d..0b1492c4 100644 --- a/packages/gen/tests/orchestration/test_sse_client.py +++ b/packages/gen/tests/orchestration/test_sse_client.py @@ -2,7 +2,7 @@ import unittest from unittest.mock import Mock -from httpx import Response +from httpx2 import Response from gen_ai_hub.orchestration.sse_client import AsyncSSEClient diff --git a/packages/gen/tests/orchestration_v2/test_service_v2.py b/packages/gen/tests/orchestration_v2/test_service_v2.py index da14af72..b4079552 100644 --- a/packages/gen/tests/orchestration_v2/test_service_v2.py +++ b/packages/gen/tests/orchestration_v2/test_service_v2.py @@ -2,7 +2,7 @@ from typing import cast from unittest.mock import Mock, patch, AsyncMock -import httpx +import httpx2 from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig @@ -98,7 +98,7 @@ def test_run_with_non_existing_deployment_id(self): with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): client = OrchestrationService(deployment_id=self.NOT_EXISTENT_DEPLOYMENT_ID, proxy_client=self.proxy_client) with orchestration_deployment_not_found_mocker(client.api_url + '/v2/completion'): - with self.assertRaises(httpx.HTTPStatusError): + with self.assertRaises(httpx2.HTTPStatusError): client.run(config=self.config) def test_run_without_config(self): @@ -192,13 +192,13 @@ def test_handle_retry_with_retry_after(self): client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) # Create a mock error without Retry-After header - response = Mock(spec=httpx.Response) + response = Mock(spec=httpx2.Response) response.status_code = 429 - response.headers = httpx.Headers({"Retry-After": "3"}) + response.headers = httpx2.Headers({"Retry-After": "3"}) response.text = "Too Many Requests" response.request = Mock() - error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + error = httpx2.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) # Collect delays for multiple retries delays = [] @@ -217,13 +217,13 @@ def test_handle_retry_without_retry_after(self): client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) # Create a mock error without Retry-After header - response = Mock(spec=httpx.Response) + response = Mock(spec=httpx2.Response) response.status_code = 429 - response.headers = httpx.Headers({}) + response.headers = httpx2.Headers({}) response.text = "Too Many Requests" response.request = Mock() - error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + error = httpx2.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) # Collect delays for multiple retries delays = [] @@ -242,13 +242,13 @@ def test_handle_retry_max_retries_exceeded(self): client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) # Create a mock error - response = Mock(spec=httpx.Response) + response = Mock(spec=httpx2.Response) response.status_code = 429 - response.headers = httpx.Headers({}) + response.headers = httpx2.Headers({}) response.text = "Too Many Requests" response.request = Mock() - error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + error = httpx2.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) # Mock _should_retry to return True so we test the retry_count >= max_retries condition with patch.object(client, '_should_retry', return_value=True): @@ -256,8 +256,8 @@ def test_handle_retry_max_retries_exceeded(self): # Need to call handle_retry within an exception context since it uses bare 'raise' try: raise error - except httpx.HTTPStatusError as e: - with self.assertRaises(httpx.HTTPStatusError) as context: + except httpx2.HTTPStatusError as e: + with self.assertRaises(httpx2.HTTPStatusError) as context: client.handle_retry(retry_count=3, base_delay=1.0, error=e, max_retries=3) # Verify retries attribute was set @@ -382,7 +382,7 @@ def capture_request(*args, **kwargs): service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) with patch.object(service.client, "post", side_effect=capture_request): service.run(config=self.config) - self.assertEqual(timeout_captured.get("timeout"), httpx.USE_CLIENT_DEFAULT) + self.assertEqual(timeout_captured.get("timeout"), httpx2.USE_CLIENT_DEFAULT) # timeout set in httpx client, not overwritten in request service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) @@ -443,7 +443,7 @@ async def capture_request(*args, **kwargs): service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) with patch.object(service.async_client, "post", new=AsyncMock(side_effect=capture_request)): await service.arun(config=self.config) - self.assertEqual(timeout_captured.get("timeout"), httpx.USE_CLIENT_DEFAULT) + self.assertEqual(timeout_captured.get("timeout"), httpx2.USE_CLIENT_DEFAULT) # timeout set in httpx client, not overwritten in request service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) diff --git a/packages/gen/tests/orchestration_v2/test_sse_client_v2.py b/packages/gen/tests/orchestration_v2/test_sse_client_v2.py index 366ba9f9..fd70b1ea 100644 --- a/packages/gen/tests/orchestration_v2/test_sse_client_v2.py +++ b/packages/gen/tests/orchestration_v2/test_sse_client_v2.py @@ -2,7 +2,7 @@ import unittest from unittest.mock import Mock -from httpx import Response +from httpx2 import Response from gen_ai_hub.orchestration_v2.sse_client import AsyncSSEClient from gen_ai_hub.orchestration_v2.exceptions import OrchestrationErrorList diff --git a/packages/gen/tests/proxy/gen_ai_hub_proxy/test_additional_header_e2e.py b/packages/gen/tests/proxy/gen_ai_hub_proxy/test_additional_header_e2e.py index 69603c6f..6590de8d 100644 --- a/packages/gen/tests/proxy/gen_ai_hub_proxy/test_additional_header_e2e.py +++ b/packages/gen/tests/proxy/gen_ai_hub_proxy/test_additional_header_e2e.py @@ -5,8 +5,8 @@ from collections import Counter from contextlib import contextmanager -import respx -from httpx import Response +from tests.mock import _HTTPXMock as _HTTPXMock +from httpx2 import Response from gen_ai_hub.proxy.gen_ai_hub_proxy import temporary_headers_addition from gen_ai_hub.proxy.native.openai import AsyncOpenAI, OpenAI @@ -36,9 +36,9 @@ def mock_callback(request): @contextmanager def mocker(deployment_url): - with respx.mock: - route = respx.post(deployment_url).mock(side_effect=mock_callback) - yield route + with _HTTPXMock() as _mock: + _mock.add_callback(mock_callback, url=deployment_url, method="POST") + yield _mock n_requests = 10 @@ -74,9 +74,9 @@ def mock_callback(request): @contextmanager def mocker(deployment_url): - with respx.mock: - route = respx.post(deployment_url).mock(side_effect=mock_callback) - yield route + with _HTTPXMock() as _mock: + _mock.add_callback(mock_callback, url=deployment_url, method="POST") + yield _mock n_requests = 10 diff --git a/packages/gen/tests/proxy/native/test_google_genai_clients.py b/packages/gen/tests/proxy/native/test_google_genai_clients.py index fc9b84f3..5df9ff24 100644 --- a/packages/gen/tests/proxy/native/test_google_genai_clients.py +++ b/packages/gen/tests/proxy/native/test_google_genai_clients.py @@ -1,6 +1,6 @@ import unittest from unittest.mock import MagicMock, patch -import httpx +import httpx2 from gen_ai_hub.proxy.native.google_genai.clients import ( _rewrite_request, AICoreDynamicTransport, @@ -20,9 +20,9 @@ def setUp(self): self.transport_instance.get_selector_kwargs.return_value = {} def test_rewrite_request_with_valid_model(self): - request = httpx.Request( + request = httpx2.Request( method="POST", - url=httpx.URL("https://api.base_url.com/models/test-model:generateContent"), + url=httpx2.URL("https://api.base_url.com/models/test-model:generateContent"), ) modified_request = _rewrite_request(self.transport_instance, request) @@ -32,9 +32,9 @@ def test_rewrite_request_with_valid_model(self): self.assertEqual(modified_request.headers["Custom-Header"], "CustomValue") def test_rewrite_request_with_discovery_call(self): - request = httpx.Request( + request = httpx2.Request( method="GET", - url=httpx.URL("https://api.base_url.com/models/"), + url=httpx2.URL("https://api.base_url.com/models/"), ) modified_request = _rewrite_request(self.transport_instance, request) @@ -49,13 +49,13 @@ def setUp(self): url="https://base_url.com/deployment" ) - @patch("httpx.HTTPTransport.handle_request") + @patch("httpx2.HTTPTransport.handle_request") def test_handle_request(self, mock_handle_request): - request = httpx.Request( + request = httpx2.Request( method="POST", - url=httpx.URL("https://api.base_url.com/models/test-model:generateContent"), + url=httpx2.URL("https://api.base_url.com/models/test-model:generateContent"), ) - mock_response = httpx.Response(200, text="Success") + mock_response = httpx2.Response(200, text="Success") mock_handle_request.return_value = mock_response response = self.transport.handle_request(request) @@ -77,13 +77,13 @@ def setUp(self): url="https://base_url.com/deployment" ) - @patch("httpx.AsyncHTTPTransport.handle_async_request") + @patch("httpx2.AsyncHTTPTransport.handle_async_request") async def test_handle_async_request(self, mock_handle_async_request): - request = httpx.Request( + request = httpx2.Request( method="POST", - url=httpx.URL("https://api.base_url.com/models/test-model:generateContent"), + url=httpx2.URL("https://api.base_url.com/models/test-model:generateContent"), ) - mock_response = httpx.Response(200, text="Success") + mock_response = httpx2.Response(200, text="Success") mock_handle_async_request.return_value = mock_response response = await self.transport.handle_async_request(request) diff --git a/packages/gen/tests/test_additional_headers.py b/packages/gen/tests/test_additional_headers.py index f9306a5c..45e77ac0 100644 --- a/packages/gen/tests/test_additional_headers.py +++ b/packages/gen/tests/test_additional_headers.py @@ -170,7 +170,7 @@ def test_vector_api_client_injects_headers(self, mock_get): self.assertEqual(call_kwargs['headers']['X-Instance'], 'value1') self.assertEqual(call_kwargs['headers']['X-Temp'], 'value2') - @patch('httpx.Client.post') + @patch('httpx2.Client.post') def test_orchestration_service_injects_headers(self, mock_post): """Test OrchestrationService passes headers via request_header.""" mock_response = MagicMock() @@ -208,7 +208,7 @@ def test_orchestration_service_injects_headers(self, mock_post): self.assertEqual(call_kwargs['headers']['X-Instance'], 'value1') self.assertEqual(call_kwargs['headers']['X-Temp'], 'value2') - @patch('httpx.Client.post') + @patch('httpx2.Client.post') def test_orchestration_service_v2_injects_headers(self, mock_post): """Test OrchestrationService V2 passes headers via request_header.""" mock_response = MagicMock() diff --git a/uv.lock b/uv.lock index 86122e41..f2098d4d 100644 --- a/uv.lock +++ b/uv.lock @@ -1254,6 +1254,19 @@ 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" }, ] +[[package]] +name = "httpcore2" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/8c/e925b1c92018abb3a1863ce1549d76d2381e334d21d65d4ac8f65dabd78a/httpcore2-2.13.0.tar.gz", hash = "sha256:2adc8be4fb285fbcd6d894298db3b52c177e74b6674eda3a76bd36be3292a3db", size = 67740, upload-time = "2026-09-14T14:18:04.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/0d/117a771a2bb91df334b66bf4da14cd02f21aefbcfe53180f336ce55e8f90/httpcore2-2.13.0-py3-none-any.whl", hash = "sha256:35ae5be347aa40467b4a5dc032ac67ebb6d27189fc97e8cebcf99616f6a1bb9e", size = 83162, upload-time = "2026-09-14T14:18:02.529Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -1269,6 +1282,45 @@ 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" }, ] +[[package]] +name = "httpx2" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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/b9/a0/e9deef4654132857b5a5dbe4eddd0ac59c2814500e11f2f5044cd81103ee/httpx2-2.13.0.tar.gz", hash = "sha256:81bd07dc67a3701729ef1f777a3c00c915d4539604fdb5afd327f8682f6b7b44", size = 100290, upload-time = "2026-09-14T14:18:05.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/d1/a0c72b0e006df654709fbc366cc5bcb53e5aee13e1e3395152c6dd293376/httpx2-2.13.0-py3-none-any.whl", hash = "sha256:fc12720cedf72faa26cca6b4ca394e05c894e7d7933fc45cafe767960804e49a", size = 95565, upload-time = "2026-09-14T14:18:03.553Z" }, +] + +[[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 = "httpx2-pytest" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx2" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/2e/66762cb820f4857a8038181ee4deb679905dbd5a1447a7e82993e1c5ddc0/httpx2_pytest-2.0.0.tar.gz", hash = "sha256:0f7e3777be96016bb070c12a64f99e0f1d9437b5b374c7ca331bc95a398cd946", size = 60492, upload-time = "2026-09-04T09:09:57.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/f100314adb8bc833bd224616e269c8a5feb196b0baa9f7d634d2f36c4e1e/httpx2_pytest-2.0.0-py3-none-any.whl", hash = "sha256:b8a63711bc40edc163c22268800d50e21c4266bb9571240bcd0cbfaf69c33cf6", size = 21304, upload-time = "2026-09-04T09:09:56.392Z" }, +] + [[package]] name = "idna" version = "3.18" @@ -2498,21 +2550,19 @@ wheels = [ [[package]] name = "openai" -version = "2.49.0" +version = "3.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, - { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/9a/275bee8349b766906741223b98c668fcd4e17e1982a87597778743b4156f/openai-2.49.0.tar.gz", hash = "sha256:80f934333b5b83cef2fde9af7151dacaa72e150f43f92b7675f7647ca6157f48", size = 1080973, upload-time = "2026-07-27T22:51:40.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/9d/a00f8bca3df57716771cca5b4c0ddb5c2e03e1dd2ddfbb1607371ffb8372/openai-3.17.0.tar.gz", hash = "sha256:28910914e6ffaf622f1bbf5dfe02e221cc2b929e492dd30d06b6826939dedf92", size = 1710984, upload-time = "2026-09-22T02:37:17.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/ca/53357e460a1172e831ecbe43dd0c37342b7211a1eb09f4cf21a412adbbdf/openai-2.49.0-py3-none-any.whl", hash = "sha256:b694201eaa42a1ccf2aa125fe29458150108fb22df1abfb55d7188599da81d8c", size = 1648589, upload-time = "2026-07-27T22:51:38Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/5b80a0ca4069a6b5ba6c7d48ae7a4f8176b2c0bde0bebccdb867472ec6f6/openai-3.17.0-py3-none-any.whl", hash = "sha256:712bc2982a6da1af10f6948a1d2d9ff5ce3ee3604ded88002a53765a77cf5ae5", size = 2068574, upload-time = "2026-09-22T02:37:15.373Z" }, ] [[package]] @@ -3776,18 +3826,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] -[[package]] -name = "respx" -version = "0.23.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/98/4e55c9c486404ec12373708d015ebce157966965a5ebe7f28ff2c784d41b/respx-0.23.1.tar.gz", hash = "sha256:242dcc6ce6b5b9bf621f5870c82a63997e8e82bc7c947f9ffe272b8f3dd5a780", size = 29243, upload-time = "2026-04-08T14:37:16.008Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/4a/221da6ca167db45693d8d26c7dc79ccfc978a440251bf6721c9aaf251ac0/respx-0.23.1-py2.py3-none-any.whl", hash = "sha256:b18004b029935384bccfa6d7d9d74b4ec9af73a081cc28600fffc0447f4b8c1a", size = 25557, upload-time = "2026-04-08T14:37:14.613Z" }, -] - [[package]] name = "roman-numerals" version = "4.1.0" @@ -4171,10 +4209,11 @@ name = "sap-ai-sdk-gen" version = "7.4.0" source = { editable = "packages/gen" } dependencies = [ + { name = "certifi" }, { name = "click" }, { name = "dacite" }, { name = "h11" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "langchain" }, { name = "langchain-classic" }, { name = "langchain-openai" }, @@ -4208,6 +4247,7 @@ google = [ [package.dev-dependencies] dev = [ + { name = "httpx2-pytest" }, { name = "myst-nb" }, { name = "parameterized" }, { name = "pillow" }, @@ -4217,7 +4257,6 @@ dev = [ { name = "pytest-cov" }, { name = "pytest-dotenv" }, { name = "requests-mock" }, - { name = "respx" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinxawesome-theme" }, @@ -4227,18 +4266,19 @@ dev = [ requires-dist = [ { name = "aiobotocore", marker = "extra == 'amazon'", specifier = ">=3.2.0" }, { name = "boto3", marker = "extra == 'amazon'", specifier = ">=1.40.61" }, + { name = "certifi" }, { name = "click", specifier = ">=8.1.7" }, { name = "dacite", specifier = ">=1.8.1" }, { name = "google-genai", marker = "extra == 'google'", specifier = "~=2.23.0" }, { name = "h11", specifier = ">=0.16.0" }, - { name = "httpx", specifier = ">=0.27.0" }, + { name = "httpx2", specifier = ">=2.0.0" }, { name = "langchain", specifier = "~=1.4.0" }, { name = "langchain-aws", marker = "extra == 'amazon'", specifier = "~=1.7.0" }, { name = "langchain-classic", specifier = "~=1.0.0" }, { name = "langchain-google-genai", marker = "extra == 'google'", specifier = "~=4.3.6,<4.3.7" }, { name = "langchain-openai", specifier = "~=1.6.0" }, { name = "langcodes", specifier = "~=3.5.1" }, - { name = "openai", specifier = ">=1.66.0" }, + { name = "openai", specifier = ">=3.0.0" }, { name = "overloading", specifier = "==0.5.0" }, { name = "packaging", specifier = ">=23.2" }, { name = "pandas", specifier = ">=2.2.0" }, @@ -4250,6 +4290,7 @@ provides-extras = ["google", "amazon", "all"] [package.metadata.requires-dev] dev = [ + { name = "httpx2-pytest" }, { name = "myst-nb" }, { name = "parameterized", specifier = "==0.9.0" }, { name = "pillow", specifier = "==12.3.0" }, @@ -4259,7 +4300,6 @@ dev = [ { name = "pytest-cov", specifier = "==7.1.0" }, { name = "pytest-dotenv", specifier = ">=0.5.2" }, { name = "requests-mock", specifier = "==1.12.1" }, - { name = "respx", specifier = "==0.23.1" }, { name = "sphinx", specifier = "<9.0.0" }, { name = "sphinxawesome-theme" }, ] @@ -4705,24 +4745,21 @@ wheels = [ ] [[package]] -name = "tqdm" -version = "4.70.0" +name = "traitlets" +version = "5.15.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] [[package]] -name = "traitlets" -version = "5.15.1" +name = "truststore" +version = "0.10.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } +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/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, + { 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]]