Skip to content

Commit b40dcbc

Browse files
authored
opentelemetry-exporter-http-transport: enable entry-point loading of transport implementations (#5320)
* opentelemetry-exporter-http-transport: enable entry-point loading of transport implementations * add changelog fragment * fix Python 3.10 typing import error * fix typecheck errors * check that returned class is a subclass of BaseHTTPTransport * Change kwargs type hint to Any in _requests.py * Refactor kwargs type hint to Any for urllib3 * address comments
1 parent 10e8577 commit b40dcbc

11 files changed

Lines changed: 175 additions & 7 deletions

File tree

.changelog/5320.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`opentelemetry-exporter-http-transport`: enable entry-point loading of transport implementations

exporter/opentelemetry-exporter-http-transport/pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ classifiers = [
2525
"Programming Language :: Python :: 3.13",
2626
"Programming Language :: Python :: 3.14",
2727
]
28-
dependencies = []
28+
dependencies = [
29+
"opentelemetry-api ~= 1.15",
30+
]
2931

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

44+
[project.entry-points.opentelemetry_http_transport]
45+
urllib3 = "opentelemetry.exporter.http.transport._urllib3:Urllib3HTTPTransport"
46+
requests = "opentelemetry.exporter.http.transport._requests:RequestsHTTPTransport"
47+
4248
[tool.hatch.version]
4349
path = "src/opentelemetry/exporter/http/transport/version/__init__.py"
4450

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,76 @@
11
# Copyright The OpenTelemetry Authors
22
# SPDX-License-Identifier: Apache-2.0
3+
4+
from __future__ import annotations
5+
6+
from typing import TYPE_CHECKING, cast
7+
8+
# pylint: disable-next=import-error
9+
from opentelemetry.exporter.http.transport._requests import (
10+
RequestsHTTPTransport as _RequestsHTTPTransport,
11+
)
12+
13+
# pylint: disable-next=import-error
14+
from opentelemetry.exporter.http.transport._urllib3 import (
15+
Urllib3HTTPTransport as _Urllib3HTTPTransport,
16+
)
17+
18+
if TYPE_CHECKING:
19+
from typing import Any, Protocol
20+
21+
from opentelemetry.exporter.http.transport._base import BaseHTTPTransport
22+
23+
class BaseHTTPTransportFactory(Protocol):
24+
def __call__(
25+
self,
26+
*,
27+
verify: bool | str,
28+
cert: str | tuple[str, str] | None,
29+
**kwargs: Any,
30+
) -> BaseHTTPTransport: ...
31+
32+
33+
_KNOWN_TRANSPORTS: dict[str, BaseHTTPTransportFactory] = {
34+
"requests": _RequestsHTTPTransport,
35+
"urllib3": _Urllib3HTTPTransport,
36+
}
37+
38+
39+
def _load_http_transport_factory(name: str) -> BaseHTTPTransportFactory:
40+
"""Return the transport factory registered under *name*.
41+
42+
Checks the built-in transport registry first to avoid the overhead of an
43+
entry-point scan for built-in transports. Falls back to standard entry-point
44+
discovery for user supplied transports registered under the
45+
``opentelemetry_http_transport`` group.
46+
47+
:param name: Entry point name, e.g. ``"requests"`` or ``"urllib3"``.
48+
:returns: A callable with signature
49+
``(*, verify, cert, **kwargs) -> BaseHTTPTransport``.
50+
:raises ValueError: If no transport is registered under *name*.
51+
:raises TypeError: If the loaded entry point is not callable.
52+
"""
53+
if name in _KNOWN_TRANSPORTS:
54+
return _KNOWN_TRANSPORTS[name]
55+
# pylint: disable-next=import-outside-toplevel,import-error
56+
from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415
57+
entry_points,
58+
)
59+
60+
ep = next(
61+
iter(entry_points(group="opentelemetry_http_transport", name=name)),
62+
None,
63+
)
64+
if not ep:
65+
raise ValueError(
66+
f"No HTTP transport registered under name {name!r}. "
67+
"Install the corresponding extra or register an entry point "
68+
"under the 'opentelemetry_http_transport' group."
69+
)
70+
factory = ep.load()
71+
if not callable(factory):
72+
raise TypeError(
73+
f"Transport {name!r} loaded from entry point is not callable "
74+
f"(got {factory!r})."
75+
)
76+
return cast("BaseHTTPTransportFactory", factory)

exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
# Copyright The OpenTelemetry Authors
22
# SPDX-License-Identifier: Apache-2.0
33

4+
from __future__ import annotations
5+
46
import json
57
from abc import ABC, abstractmethod
6-
from collections.abc import Mapping
78
from dataclasses import dataclass
8-
from typing import Any
9+
from typing import TYPE_CHECKING
10+
11+
if TYPE_CHECKING:
12+
from collections.abc import Mapping
13+
from typing import Any
914

1015

1116
@dataclass(frozen=True, slots=True)

exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
from __future__ import annotations
55

66
import functools
7-
from collections.abc import Mapping
87
from dataclasses import dataclass, field
9-
from typing import TYPE_CHECKING, Any
8+
from typing import TYPE_CHECKING
109

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

1716
if TYPE_CHECKING:
17+
from collections.abc import Mapping
18+
from typing import Any
19+
1820
import requests
1921
from requests import Response
2022

@@ -66,6 +68,7 @@ def __init__(
6668
verify: bool | str = True,
6769
cert: str | tuple[str, str] | None = None,
6870
session: requests.Session | None = None,
71+
**kwargs: Any,
6972
) -> None:
7073
# pylint: disable-next=import-outside-toplevel
7174
import requests # noqa: PLC0415

exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55

66
import functools
77
import json
8-
from collections.abc import Mapping
98
from dataclasses import dataclass, field
10-
from typing import TYPE_CHECKING, Any
9+
from typing import TYPE_CHECKING
1110

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

1817
if TYPE_CHECKING:
18+
from collections.abc import Mapping
19+
from typing import Any
20+
1921
from urllib3 import BaseHTTPResponse
2022

2123

@@ -70,6 +72,7 @@ def __init__(
7072
*,
7173
verify: bool | str = True,
7274
cert: str | tuple[str, str] | None = None,
75+
**kwargs: Any,
7376
) -> None:
7477
# pylint: disable-next=import-outside-toplevel
7578
import urllib3 # noqa: PLC0415

exporter/opentelemetry-exporter-http-transport/test-requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ mocket==3.14.1
33
packaging==26.2
44
pluggy==1.6.0
55
pytest==7.4.4
6+
-e opentelemetry-api
67
-e exporter/opentelemetry-exporter-http-transport[urllib3,requests]

exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
# 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
33
-e exporter/opentelemetry-exporter-http-transport
44
# via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in
5+
-e opentelemetry-api
6+
# via
7+
# -r exporter/opentelemetry-exporter-http-transport/test-requirements.in
8+
# opentelemetry-exporter-http-transport
59
certifi==2026.4.22
610
# via requests
711
charset-normalizer==3.4.7
@@ -44,6 +48,7 @@ typing-extensions==4.15.0
4448
# via
4549
# exceptiongroup
4650
# mocket
51+
# opentelemetry-api
4752
urllib3==2.7.0
4853
# via
4954
# mocket

exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
# 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
33
-e exporter/opentelemetry-exporter-http-transport
44
# via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in
5+
-e opentelemetry-api
6+
# via
7+
# -r exporter/opentelemetry-exporter-http-transport/test-requirements.in
8+
# opentelemetry-exporter-http-transport
59
certifi==2026.4.22
610
# via requests
711
chardet==3.0.4
@@ -44,6 +48,7 @@ typing-extensions==4.15.0
4448
# via
4549
# exceptiongroup
4650
# mocket
51+
# opentelemetry-api
4752
urllib3==1.26.20
4853
# via
4954
# mocket
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright The OpenTelemetry Authors
2+
# SPDX-License-Identifier: Apache-2.0
3+
# pylint: disable=import-error
4+
5+
import unittest
6+
from unittest.mock import MagicMock, patch
7+
8+
from opentelemetry.exporter.http.transport import _load_http_transport_factory
9+
from opentelemetry.exporter.http.transport._requests import (
10+
RequestsHTTPTransport,
11+
)
12+
from opentelemetry.exporter.http.transport._urllib3 import (
13+
Urllib3HTTPTransport,
14+
)
15+
16+
_ENTRY_POINTS_TARGET = "opentelemetry.util._importlib_metadata.entry_points"
17+
18+
19+
# pylint: disable=no-self-use
20+
class TestLoadHTTPTransportFactory(unittest.TestCase):
21+
def test_returns_requests_transport(self):
22+
self.assertIs(
23+
_load_http_transport_factory("requests"), RequestsHTTPTransport
24+
)
25+
26+
def test_returns_urllib3_transport(self):
27+
self.assertIs(
28+
_load_http_transport_factory("urllib3"), Urllib3HTTPTransport
29+
)
30+
31+
def test_known_transport_does_not_call_entry_points(self):
32+
with patch(_ENTRY_POINTS_TARGET) as mock_ep:
33+
_load_http_transport_factory("requests")
34+
_load_http_transport_factory("urllib3")
35+
self.assertFalse(mock_ep.called)
36+
37+
def test_unknown_transport_calls_entry_points(self):
38+
def _custom_factory(*, verify, cert, **kwargs):
39+
pass
40+
41+
mock_ep = MagicMock()
42+
mock_ep.load.return_value = _custom_factory
43+
with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]) as mock_fn:
44+
result = _load_http_transport_factory("custom")
45+
self.assertEqual(
46+
mock_fn.call_args.kwargs,
47+
{"group": "opentelemetry_http_transport", "name": "custom"},
48+
)
49+
self.assertIs(result, _custom_factory)
50+
51+
def test_entry_point_non_callable_raises_type_error(self):
52+
mock_ep = MagicMock()
53+
mock_ep.load.return_value = "not_callable"
54+
with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]):
55+
self.assertRaises(TypeError, _load_http_transport_factory, "bad")
56+
57+
def test_unknown_transport_raises_value_error(self):
58+
with patch(_ENTRY_POINTS_TARGET, return_value=[]):
59+
self.assertRaises(
60+
ValueError, _load_http_transport_factory, "nonexistent"
61+
)

0 commit comments

Comments
 (0)