Skip to content

Commit 340c61b

Browse files
authored
Add HttpxOCIAuth Integration Documentation for HTTPX Clients (#1109)
2 parents e7e3f19 + c423d00 commit 340c61b

File tree

6 files changed

+122
-29
lines changed

6 files changed

+122
-29
lines changed

ads/aqua/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
from logging import getLogger
88

99
from ads import logger, set_auth
10-
from ads.aqua.client.client import AsyncClient, Client
10+
from ads.aqua.client.client import (
11+
AsyncClient,
12+
Client,
13+
HttpxOCIAuth,
14+
get_async_httpx_client,
15+
get_httpx_client,
16+
)
1117
from ads.aqua.common.utils import fetch_service_compartment
1218
from ads.config import OCI_RESOURCE_PRINCIPAL_VERSION
1319

ads/aqua/client/client.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,23 @@
5151
logger = logging.getLogger(__name__)
5252

5353

54-
class OCIAuth(httpx.Auth):
54+
class HttpxOCIAuth(httpx.Auth):
5555
"""
5656
Custom HTTPX authentication class that uses the OCI Signer for request signing.
5757
5858
Attributes:
5959
signer (oci.signer.Signer): The OCI signer used to sign requests.
6060
"""
6161

62-
def __init__(self, signer: oci.signer.Signer):
62+
def __init__(self, signer: Optional[oci.signer.Signer] = None):
6363
"""
64-
Initialize the OCIAuth instance.
64+
Initialize the HttpxOCIAuth instance.
6565
6666
Args:
6767
signer (oci.signer.Signer): The OCI signer to use for signing requests.
6868
"""
69-
self.signer = signer
69+
70+
self.signer = signer or authutil.default_signer().get("signer")
7071

7172
def auth_flow(self, request: httpx.Request) -> Iterator[httpx.Request]:
7273
"""
@@ -256,7 +257,7 @@ def __init__(
256257
auth = auth or authutil.default_signer()
257258
if not callable(auth.get("signer")):
258259
raise ValueError("Auth object must have a 'signer' callable attribute.")
259-
self.auth = OCIAuth(auth["signer"])
260+
self.auth = HttpxOCIAuth(auth["signer"])
260261

261262
logger.debug(
262263
f"Initialized {self.__class__.__name__} with endpoint={self.endpoint}, "
@@ -352,7 +353,7 @@ def __init__(self, *args, **kwargs) -> None:
352353
**kwargs: Keyword arguments forwarded to BaseClient.
353354
"""
354355
super().__init__(*args, **kwargs)
355-
self._client = httpx.Client(timeout=self.timeout)
356+
self._client = httpx.Client(timeout=self.timeout, auth=self.auth)
356357

357358
def is_closed(self) -> bool:
358359
return self._client.is_closed
@@ -400,7 +401,6 @@ def _request(
400401
response = self._client.post(
401402
self.endpoint,
402403
headers=self._prepare_headers(stream=False, headers=headers),
403-
auth=self.auth,
404404
json=payload,
405405
)
406406
logger.debug(f"Received response with status code: {response.status_code}")
@@ -447,7 +447,6 @@ def _stream(
447447
"POST",
448448
self.endpoint,
449449
headers=self._prepare_headers(stream=True, headers=headers),
450-
auth=self.auth,
451450
json={**payload, "stream": True},
452451
) as response:
453452
try:
@@ -581,7 +580,7 @@ def __init__(self, *args, **kwargs) -> None:
581580
**kwargs: Keyword arguments forwarded to BaseClient.
582581
"""
583582
super().__init__(*args, **kwargs)
584-
self._client = httpx.AsyncClient(timeout=self.timeout)
583+
self._client = httpx.AsyncClient(timeout=self.timeout, auth=self.auth)
585584

586585
def is_closed(self) -> bool:
587586
return self._client.is_closed
@@ -637,7 +636,6 @@ async def _request(
637636
response = await self._client.post(
638637
self.endpoint,
639638
headers=self._prepare_headers(stream=False, headers=headers),
640-
auth=self.auth,
641639
json=payload,
642640
)
643641
logger.debug(f"Received response with status code: {response.status_code}")
@@ -683,7 +681,6 @@ async def _stream(
683681
"POST",
684682
self.endpoint,
685683
headers=self._prepare_headers(stream=True, headers=headers),
686-
auth=self.auth,
687684
json={**payload, "stream": True},
688685
) as response:
689686
try:
@@ -797,3 +794,43 @@ async def embeddings(
797794
logger.debug(f"Generating embeddings with input: {input}, payload: {payload}")
798795
payload = {**(payload or {}), "input": input}
799796
return await self._request(payload=payload, headers=headers)
797+
798+
799+
def get_httpx_client(**kwargs: Any) -> httpx.Client:
800+
"""
801+
Creates and returns a synchronous httpx Client configured with OCI authentication signer based
802+
the authentication type setup using ads.set_auth method or env variable OCI_IAM_TYPE.
803+
More information - https://accelerated-data-science.readthedocs.io/en/stable/user_guide/cli/authentication.html
804+
805+
Parameters
806+
----------
807+
**kwargs : Any
808+
Keyword arguments supported by httpx.Client
809+
810+
Returns
811+
-------
812+
Client
813+
A configured synchronous httpx Client instance.
814+
"""
815+
kwargs["auth"] = kwargs.get("auth") or HttpxOCIAuth()
816+
return httpx.Client(**kwargs)
817+
818+
819+
def get_async_httpx_client(**kwargs: Any) -> httpx.AsyncClient:
820+
"""
821+
Creates and returns a synchronous httpx Client configured with OCI authentication signer based
822+
the authentication type setup using ads.set_auth method or env variable OCI_IAM_TYPE.
823+
More information - https://accelerated-data-science.readthedocs.io/en/stable/user_guide/cli/authentication.html
824+
825+
Parameters
826+
----------
827+
**kwargs : Any
828+
Keyword arguments supported by httpx.Client
829+
830+
Returns
831+
-------
832+
AsyncClient
833+
A configured asynchronous httpx AsyncClient instance.
834+
"""
835+
kwargs["auth"] = kwargs.get("auth") or HttpxOCIAuth()
836+
return httpx.AsyncClient(**kwargs)

ads/common/auth.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,15 @@ def set_auth(
8888
auth: Optional[str] = AuthType.API_KEY,
8989
oci_config_location: Optional[str] = DEFAULT_LOCATION,
9090
profile: Optional[str] = DEFAULT_PROFILE,
91-
config: Optional[Dict] = {"region": os.environ["OCI_RESOURCE_REGION"]}
92-
if os.environ.get("OCI_RESOURCE_REGION")
93-
else {},
91+
config: Optional[Dict] = (
92+
{"region": os.environ["OCI_RESOURCE_REGION"]}
93+
if os.environ.get("OCI_RESOURCE_REGION")
94+
else {}
95+
),
9496
signer: Optional[Any] = None,
9597
signer_callable: Optional[Callable] = None,
96-
signer_kwargs: Optional[Dict] = {},
97-
client_kwargs: Optional[Dict] = {},
98+
signer_kwargs: Optional[Dict] = None,
99+
client_kwargs: Optional[Dict] = None,
98100
) -> None:
99101
"""
100102
Sets the default authentication type.
@@ -195,6 +197,9 @@ def set_auth(
195197
>>> # instance principals authentication dictionary created based on callable with kwargs parameters:
196198
>>> ads.set_auth(signer_callable=signer_callable, signer_kwargs=signer_kwargs)
197199
"""
200+
signer_kwargs = signer_kwargs or {}
201+
client_kwargs = client_kwargs or {}
202+
198203
auth_state = AuthState()
199204

200205
valid_auth_keys = AuthFactory.classes.keys()
@@ -258,9 +263,11 @@ def api_keys(
258263
"""
259264
signer_args = dict(
260265
oci_config=oci_config if isinstance(oci_config, Dict) else {},
261-
oci_config_location=oci_config
262-
if isinstance(oci_config, str)
263-
else os.path.expanduser(DEFAULT_LOCATION),
266+
oci_config_location=(
267+
oci_config
268+
if isinstance(oci_config, str)
269+
else os.path.expanduser(DEFAULT_LOCATION)
270+
),
264271
oci_key_profile=profile,
265272
client_kwargs=client_kwargs,
266273
)
@@ -334,9 +341,11 @@ def security_token(
334341
"""
335342
signer_args = dict(
336343
oci_config=oci_config if isinstance(oci_config, Dict) else {},
337-
oci_config_location=oci_config
338-
if isinstance(oci_config, str)
339-
else os.path.expanduser(DEFAULT_LOCATION),
344+
oci_config_location=(
345+
oci_config
346+
if isinstance(oci_config, str)
347+
else os.path.expanduser(DEFAULT_LOCATION)
348+
),
340349
oci_key_profile=profile,
341350
client_kwargs=client_kwargs,
342351
)

docs/source/user_guide/large_language_model/aqua_client.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,45 @@ The following examples demonstrate how to perform the same operations using the
129129
input=["one", "two"]
130130
)
131131
print(response)
132+
133+
134+
HTTPX Client Integration with OCI Authentication
135+
================================================
136+
137+
.. versionadded:: 2.13.1
138+
139+
The latest client release now includes streamlined support for OCI authentication with HTTPX. Our helper functions for creating synchronous and asynchronous HTTPX clients automatically configure authentication based on your default settings. Additionally, you can pass extra keyword arguments to further customize the HTTPX client (e.g., timeouts, proxies, etc.), making it fully compatible with OCI Model Deployment service and third-party libraries (e.g., the OpenAI client).
140+
141+
Usage
142+
-----
143+
144+
**Synchronous HTTPX Client**
145+
146+
.. code-block:: python3
147+
148+
import ads
149+
150+
ads.set_auth(auth="security_token", profile="<replace-with-your-profile>")
151+
152+
client = ads.aqua.get_httpx_client(timeout=10.0)
153+
154+
response = client.post(
155+
url="https://<MD_OCID>/predict",
156+
json={
157+
"model": "odsc-llm",
158+
"prompt": "Tell me a joke."
159+
},
160+
)
161+
162+
response.raise_for_status()
163+
json_response = response.json()
164+
165+
**Asynchronous HTTPX Client**
166+
167+
.. code-block:: python3
168+
169+
import ads
170+
171+
ads.set_auth(auth="security_token", profile="<replace-with-your-profile>")
172+
173+
async_client = client = ads.aqua.get_async_httpx_client(timeout=10.0)

tests/unitary/default_setup/auth/test_auth.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
55

66
import os
7-
from mock import MagicMock
87
import pytest
98
from unittest import TestCase, mock
109

tests/unitary/with_extras/aqua/test_client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@
1515
BaseClient,
1616
Client,
1717
ExtendedRequestError,
18-
OCIAuth,
18+
HttpxOCIAuth,
1919
_create_retry_decorator,
2020
_retry_decorator,
2121
_should_retry_exception,
2222
)
2323
from ads.common import auth as authutil
2424

2525

26-
class TestOCIAuth:
27-
"""Unit tests for OCIAuth class."""
26+
class TestHttpxOCIAuth:
27+
"""Unit tests for HttpxOCIAuth class."""
2828

2929
def setup_method(self):
3030
self.signer_mock = Mock()
31-
self.oci_auth = OCIAuth(self.signer_mock)
31+
self.oci_auth = HttpxOCIAuth(self.signer_mock)
3232

3333
def test_auth_flow(self):
3434
"""Ensures that the auth_flow signs the request correctly."""
@@ -226,7 +226,7 @@ def test_init(self):
226226
assert self.base_client.retries == self.retries
227227
assert self.base_client.backoff_factor == self.backoff_factor
228228
assert self.base_client.timeout == self.timeout
229-
assert isinstance(self.base_client.auth, OCIAuth)
229+
assert isinstance(self.base_client.auth, HttpxOCIAuth)
230230

231231
def test_init_default_auth(self):
232232
"""Ensures that default auth is used when auth is None."""

0 commit comments

Comments
 (0)