Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions nemoguardrails/http/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Transport-neutral asynchronous HTTP clients for NeMo Guardrails integrations."""

from nemoguardrails.http.client import ClosableHTTPClient, HTTPClient
from nemoguardrails.http.errors import (
HTTPClientError,
HTTPConnectionError,
HTTPResponseDecodeError,
HTTPStatusError,
HTTPTimeoutError,
)
from nemoguardrails.http.retry import RetryingHTTPClient, RetryPolicy
from nemoguardrails.http.transport import HttpxHTTPClient
from nemoguardrails.http.types import HTTPRequest, HTTPResponse

__all__ = [
"ClosableHTTPClient",
"HTTPClient",
"HTTPClientError",
"HTTPConnectionError",
"HTTPRequest",
"HTTPResponse",
"HTTPResponseDecodeError",
"HTTPStatusError",
"HTTPTimeoutError",
"HttpxHTTPClient",
"RetryPolicy",
"RetryingHTTPClient",
]
35 changes: 35 additions & 0 deletions nemoguardrails/http/_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from urllib.parse import SplitResult, urlsplit, urlunsplit


def split_url(url: str) -> SplitResult:
return urlsplit(url)


def sanitize_url(url: str) -> str:
parts = split_url(url)
hostname = parts.hostname
if hostname is None:
netloc = parts.netloc.rsplit("@", 1)[-1]
return urlunsplit((parts.scheme, netloc, parts.path, "", ""))
host = f"[{hostname}]" if ":" in hostname else hostname
try:
port = parts.port
except ValueError:
port = None
netloc = f"{host}:{port}" if port is not None else host
return urlunsplit((parts.scheme, netloc, parts.path, "", ""))
66 changes: 66 additions & 0 deletions nemoguardrails/http/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Protocols implemented by transport-neutral asynchronous HTTP clients."""

from typing import Any, Mapping, Protocol, runtime_checkable

from nemoguardrails.http.types import HTTPResponse


@runtime_checkable
class HTTPClient(Protocol):
"""Send asynchronous HTTP requests without exposing transport-specific types."""

async def request(
self,
method: str,
url: str,
*,
headers: Mapping[str, str] | None = None,
params: Mapping[str, Any] | None = None,
json: Any = None,
content: bytes | str | None = None,
timeout: float | None = None,
) -> HTTPResponse:
"""Send one request and return a response whose content is fully owned.

Args:
method: HTTP method, case-insensitive.
url: Absolute request URL.
headers: Optional request headers.
params: Optional query parameters.
json: Optional JSON-serializable request body.
content: Optional raw request body.
timeout: Optional total request timeout in seconds.

Returns:
A transport-neutral response containing materialized body bytes.
"""

...


@runtime_checkable
class ClosableHTTPClient(HTTPClient, Protocol):
"""An HTTP client that exposes asynchronous resource cleanup."""

async def close(self) -> None:
"""Release resources owned by the client.

Implementations must make repeated calls safe.
"""

...
68 changes: 68 additions & 0 deletions nemoguardrails/http/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Transport-neutral exceptions raised by the outbound HTTP subsystem."""

from typing import TYPE_CHECKING

from nemoguardrails.http._url import sanitize_url

if TYPE_CHECKING:
from nemoguardrails.http.types import HTTPRequest, HTTPResponse


class HTTPClientError(Exception):
"""Base class for outbound HTTP failures.

``retry_count`` records how many retries completed before the error was
returned to the caller.
"""

def __init__(self, *args: object):
super().__init__(*args)
self.retry_count = 0


class HTTPConnectionError(HTTPClientError):
"""Raised when the transport cannot establish or maintain a connection."""


class HTTPTimeoutError(HTTPClientError):
"""Raised when an outbound request exceeds its total timeout."""


class HTTPStatusError(HTTPClientError):
"""Raised for an unsuccessful HTTP status.

Attributes:
response: The materialized response that produced the error.
request: Request metadata when supplied by the caller.
"""

def __init__(self, response: "HTTPResponse", request: "HTTPRequest | None" = None):
message = f"HTTP request failed with status {response.status_code}"
if request is not None:
message = f"{message}: {request.method.upper()} {sanitize_url(request.url)}"
super().__init__(message)
self.response = response
self.request = request


class HTTPResponseDecodeError(HTTPClientError):
"""Raised when a response body cannot be decoded as JSON."""

def __init__(self, response: "HTTPResponse"):
super().__init__(f"HTTP response body is not valid JSON (status {response.status_code})")
self.response = response
Loading