Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5320.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-exporter-http-transport`: enable entry-point loading of transport implementations
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ classifiers = [
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = []
dependencies = [
"opentelemetry-api ~= 1.15",
]

[project.optional-dependencies]
urllib3 = [
Expand All @@ -39,6 +41,10 @@ requests = [
Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-http-transport"
Repository = "https://github.com/open-telemetry/opentelemetry-python"

[project.entry-points.opentelemetry_http_transport]
Comment thread
MikeGoldsmith marked this conversation as resolved.
urllib3 = "opentelemetry.exporter.http.transport._urllib3:Urllib3HTTPTransport"
requests = "opentelemetry.exporter.http.transport._requests:RequestsHTTPTransport"

[tool.hatch.version]
path = "src/opentelemetry/exporter/http/transport/version/__init__.py"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,76 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

from typing import TYPE_CHECKING, cast

# pylint: disable-next=import-error
from opentelemetry.exporter.http.transport._requests import (
RequestsHTTPTransport as _RequestsHTTPTransport,
)

# pylint: disable-next=import-error
from opentelemetry.exporter.http.transport._urllib3 import (
Urllib3HTTPTransport as _Urllib3HTTPTransport,
)

if TYPE_CHECKING:
from typing import Any, Protocol

from opentelemetry.exporter.http.transport._base import BaseHTTPTransport

class BaseHTTPTransportFactory(Protocol):
def __call__(
self,
*,
verify: bool | str,
cert: str | tuple[str, str] | None,
**kwargs: Any,
) -> BaseHTTPTransport: ...


_KNOWN_TRANSPORTS: dict[str, BaseHTTPTransportFactory] = {
"requests": _RequestsHTTPTransport,
"urllib3": _Urllib3HTTPTransport,
}


def _load_http_transport_factory(name: str) -> BaseHTTPTransportFactory:
"""Return the transport factory registered under *name*.

Checks the built-in transport registry first to avoid the overhead of an
entry-point scan for built-in transports. Falls back to standard entry-point
discovery for user supplied transports registered under the
``opentelemetry_http_transport`` group.

:param name: Entry point name, e.g. ``"requests"`` or ``"urllib3"``.
:returns: A callable with signature
``(*, verify, cert, **kwargs) -> BaseHTTPTransport``.
:raises ValueError: If no transport is registered under *name*.
:raises TypeError: If the loaded entry point is not callable.
"""
if name in _KNOWN_TRANSPORTS:
return _KNOWN_TRANSPORTS[name]
# pylint: disable-next=import-outside-toplevel,import-error
from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415
entry_points,
)

ep = next(
iter(entry_points(group="opentelemetry_http_transport", name=name)),
None,
)
if not ep:
raise ValueError(
f"No HTTP transport registered under name {name!r}. "
"Install the corresponding extra or register an entry point "
"under the 'opentelemetry_http_transport' group."
)
factory = ep.load()
if not callable(factory):
raise TypeError(
f"Transport {name!r} loaded from entry point is not callable "
f"(got {factory!r})."
)
return cast("BaseHTTPTransportFactory", factory)
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

import json
from abc import ABC, abstractmethod
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from collections.abc import Mapping
from typing import Any


@dataclass(frozen=True, slots=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
from __future__ import annotations

import functools
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING

# pylint: disable-next=import-error
from opentelemetry.exporter.http.transport._base import (
Expand All @@ -15,6 +14,9 @@
)

if TYPE_CHECKING:
from collections.abc import Mapping
from typing import Any

import requests
from requests import Response

Expand Down Expand Up @@ -66,6 +68,7 @@ def __init__(
verify: bool | str = True,
cert: str | tuple[str, str] | None = None,
session: requests.Session | None = None,
**kwargs: Any,
) -> None:
# pylint: disable-next=import-outside-toplevel
import requests # noqa: PLC0415
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

import functools
import json
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING

# pylint: disable-next=import-error
from opentelemetry.exporter.http.transport._base import (
Expand All @@ -16,6 +15,9 @@
)

if TYPE_CHECKING:
from collections.abc import Mapping
from typing import Any

from urllib3 import BaseHTTPResponse


Expand Down Expand Up @@ -70,6 +72,7 @@ def __init__(
*,
verify: bool | str = True,
cert: str | tuple[str, str] | None = None,
**kwargs: Any,
) -> None:
# pylint: disable-next=import-outside-toplevel
import urllib3 # noqa: PLC0415
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ mocket==3.14.1
packaging==26.2
pluggy==1.6.0
pytest==7.4.4
-e opentelemetry-api
-e exporter/opentelemetry-exporter-http-transport[urllib3,requests]
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# uv pip compile --python 3.10 --universal --resolution highest exporter/opentelemetry-exporter-http-transport/test-requirements.in -o exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt
-e exporter/opentelemetry-exporter-http-transport
# via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in
-e opentelemetry-api
# via
# -r exporter/opentelemetry-exporter-http-transport/test-requirements.in
# opentelemetry-exporter-http-transport
certifi==2026.4.22
# via requests
charset-normalizer==3.4.7
Expand Down Expand Up @@ -44,6 +48,7 @@ typing-extensions==4.15.0
# via
# exceptiongroup
# mocket
# opentelemetry-api
urllib3==2.7.0
# via
# mocket
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# uv pip compile --python 3.10 --universal --resolution lowest-direct exporter/opentelemetry-exporter-http-transport/test-requirements.in -o exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt
-e exporter/opentelemetry-exporter-http-transport
# via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in
-e opentelemetry-api
# via
# -r exporter/opentelemetry-exporter-http-transport/test-requirements.in
# opentelemetry-exporter-http-transport
certifi==2026.4.22
# via requests
chardet==3.0.4
Expand Down Expand Up @@ -44,6 +48,7 @@ typing-extensions==4.15.0
# via
# exceptiongroup
# mocket
# opentelemetry-api
urllib3==1.26.20
# via
# mocket
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
# pylint: disable=import-error

import unittest
from unittest.mock import MagicMock, patch

from opentelemetry.exporter.http.transport import _load_http_transport_factory
from opentelemetry.exporter.http.transport._requests import (
RequestsHTTPTransport,
)
from opentelemetry.exporter.http.transport._urllib3 import (
Urllib3HTTPTransport,
)

_ENTRY_POINTS_TARGET = "opentelemetry.util._importlib_metadata.entry_points"


# pylint: disable=no-self-use
class TestLoadHTTPTransportFactory(unittest.TestCase):
def test_returns_requests_transport(self):
self.assertIs(
_load_http_transport_factory("requests"), RequestsHTTPTransport
)

def test_returns_urllib3_transport(self):
self.assertIs(
_load_http_transport_factory("urllib3"), Urllib3HTTPTransport
)

def test_known_transport_does_not_call_entry_points(self):
with patch(_ENTRY_POINTS_TARGET) as mock_ep:
_load_http_transport_factory("requests")
_load_http_transport_factory("urllib3")
self.assertFalse(mock_ep.called)

def test_unknown_transport_calls_entry_points(self):
def _custom_factory(*, verify, cert, **kwargs):
pass

mock_ep = MagicMock()
mock_ep.load.return_value = _custom_factory
with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]) as mock_fn:
result = _load_http_transport_factory("custom")
self.assertEqual(
mock_fn.call_args.kwargs,
{"group": "opentelemetry_http_transport", "name": "custom"},
)
self.assertIs(result, _custom_factory)

def test_entry_point_non_callable_raises_type_error(self):
mock_ep = MagicMock()
mock_ep.load.return_value = "not_callable"
with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]):
self.assertRaises(TypeError, _load_http_transport_factory, "bad")

def test_unknown_transport_raises_value_error(self):
with patch(_ENTRY_POINTS_TARGET, return_value=[]):
self.assertRaises(
ValueError, _load_http_transport_factory, "nonexistent"
)
4 changes: 4 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading