diff --git a/elastic_transport/connection/base.py b/elastic_transport/connection/base.py index 2a18830..7025ceb 100644 --- a/elastic_transport/connection/base.py +++ b/elastic_transport/connection/base.py @@ -51,6 +51,8 @@ class Connection(object): :arg user_agent: 'User-Agent' HTTP header for the given service. """ + HTTP_CLIENT_META = None + def __init__( self, host="localhost", diff --git a/elastic_transport/connection/http_requests.py b/elastic_transport/connection/http_requests.py index 108530b..206efb9 100644 --- a/elastic_transport/connection/http_requests.py +++ b/elastic_transport/connection/http_requests.py @@ -20,17 +20,19 @@ import urllib3 +from ..compat import urlencode +from ..exceptions import ConnectionError, ConnectionTimeout +from ..utils import DEFAULT, client_meta_version, normalize_headers +from .base import Connection + try: import requests - REQUESTS_AVAILABLE = True + _REQUESTS_AVAILABLE = True + _REQUESTS_META_VERSION = client_meta_version(requests.__version__) except ImportError: # pragma: nocover - REQUESTS_AVAILABLE = False - -from ..compat import urlencode -from ..exceptions import ConnectionError, ConnectionTimeout -from ..utils import DEFAULT, normalize_headers -from .base import Connection + _REQUESTS_AVAILABLE = False + _REQUESTS_META_VERSION = "" class RequestsHttpConnection(Connection): @@ -52,6 +54,8 @@ class RequestsHttpConnection(Connection): For tracing all requests made by this transport. """ + HTTP_CLIENT_META = ("rq", _REQUESTS_META_VERSION) + def __init__( self, host="localhost", @@ -67,7 +71,7 @@ def __init__( opaque_id=None, **kwargs ): - if not REQUESTS_AVAILABLE: # pragma: nocover + if not _REQUESTS_AVAILABLE: # pragma: nocover raise ValueError( "You must have 'requests' installed to use RequestsHttpConnection" ) diff --git a/elastic_transport/connection/http_urllib3.py b/elastic_transport/connection/http_urllib3.py index 87dbb18..bd5c336 100644 --- a/elastic_transport/connection/http_urllib3.py +++ b/elastic_transport/connection/http_urllib3.py @@ -25,7 +25,7 @@ from ..compat import urlencode from ..exceptions import ConnectionError, ConnectionTimeout -from ..utils import DEFAULT, normalize_headers +from ..utils import DEFAULT, client_meta_version, normalize_headers from .base import Connection CA_CERTS = None @@ -70,6 +70,8 @@ class Urllib3HttpConnection(Connection): For tracing all requests made by this transport. """ + HTTP_CLIENT_META = ("ur", client_meta_version(urllib3.__version__)) + def __init__( self, host="localhost", diff --git a/elastic_transport/transport.py b/elastic_transport/transport.py index e08bc69..fa763de 100644 --- a/elastic_transport/transport.py +++ b/elastic_transport/transport.py @@ -15,13 +15,16 @@ # specific language governing permissions and limitations # under the License. +from platform import python_version + +from ._version import __version__ from .compat import string_types, urlparse from .connection import RequestsHttpConnection, Urllib3HttpConnection from .connection_pool import ConnectionPool, DummyConnectionPool, EmptyConnectionPool from .exceptions import ConnectionError, ConnectionTimeout, TransportError from .response import DictResponse, ListResponse, Response from .serializer import DEFAULT_SERIALIZERS, Deserializer -from .utils import DEFAULT, normalize_headers +from .utils import DEFAULT, client_meta_version, normalize_headers # Allows for using a connection_class by name rather than import. CONNECTION_CLASS_NAMES = { @@ -89,6 +92,19 @@ def __init__( ) connection_class = CONNECTION_CLASS_NAMES[connection_class] + # Create the default metadata for the x-elastic-client-meta + # HTTP header. Only requires adding the (service, service_version) + # tuple to the beginning of the client_meta + self.transport_client_meta = ( + ("py", client_meta_version(python_version())), + ("t", client_meta_version(__version__)), + ) + + # Grab the 'HTTP_CLIENT_META' property from the connection class + http_client_meta = getattr(connection_class, "HTTP_CLIENT_META", None) + if http_client_meta: + self.transport_client_meta += (http_client_meta,) + # serialization config _serializers = DEFAULT_SERIALIZERS.copy() # if custom serializers map has been supplied, override the defaults with it diff --git a/elastic_transport/utils.py b/elastic_transport/utils.py index 614912c..f424d08 100644 --- a/elastic_transport/utils.py +++ b/elastic_transport/utils.py @@ -16,6 +16,7 @@ # under the License. import binascii +import re from collections import namedtuple from platform import python_version @@ -39,6 +40,16 @@ def create_user_agent(name, version): ) +def client_meta_version(ver): + """Converts a Python package version to a meta version. + Meta version simply adds a 'p' suffix for all pre-releases + """ + ver, ver_is_pre = re.match(r"^([0-9][0-9.]*[0-9]|[0-9])(.*)$", ver).groups() + if ver_is_pre: + ver += "p" + return ver + + def normalize_headers(headers): """Normalizes HTTP headers to be lowercase to ensure there are no case-collisions deeper within the stack. diff --git a/tests/test_transport.py b/tests/test_transport.py index 7ee4d6d..4d05e17 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -16,6 +16,8 @@ # specific language governing permissions and limitations # under the License. +import re + import mock import pytest @@ -398,3 +400,21 @@ def test_head_response_ignore_status(status): assert resp == False # noqa assert resp.body is False assert bool(resp) is False + + +@pytest.mark.parametrize( + "connection_class", + ["urllib3", "requests", Urllib3HttpConnection, RequestsHttpConnection], +) +def test_transport_client_meta_connection_class(connection_class): + t = Transport(connection_class=connection_class) + assert t.transport_client_meta[2] == t.connection_class.HTTP_CLIENT_META + assert t.transport_client_meta[2][0] in ("ur", "rq") + assert re.match( + r"^py=[0-9.]+p?,t=[0-9.]+p?,(?:ur|rq)=[0-9.]+p?$", + ",".join("%s=%s" % (k, v) for k, v in t.transport_client_meta), + ) + + t = Transport() + assert t.transport_client_meta[2][0] == "ur" + assert [x[0] for x in t.transport_client_meta[:2]] == ["py", "t"] diff --git a/tests/test_utils.py b/tests/test_utils.py index 91ca083..028f71b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -21,7 +21,11 @@ import pytest from elastic_transport import __version__ -from elastic_transport.utils import create_user_agent, parse_cloud_id +from elastic_transport.utils import ( + client_meta_version, + create_user_agent, + parse_cloud_id, +) def test_create_user_agent(): @@ -33,6 +37,19 @@ def test_create_user_agent(): ) +@pytest.mark.parametrize( + ["version", "meta_version"], + [ + ("7.10.0", "7.10.0"), + ("7.10.0-alpha1", "7.10.0p"), + ("3.9.0b1", "3.9.0p"), + ("3.9.pre1", "3.9p"), + ], +) +def test_client_meta_version(version, meta_version): + assert client_meta_version(version) == meta_version + + def test_parse_cloud_id(): cloud_id = parse_cloud_id( "cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng=="