diff --git a/api_core/google/api_core/client_info.py b/api_core/google/api_core/client_info.py new file mode 100644 index 000000000000..b196b7a987e4 --- /dev/null +++ b/api_core/google/api_core/client_info.py @@ -0,0 +1,96 @@ +# Copyright 2017 Google LLC +# +# 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. + +"""Helpers for providing client information. + +Client information is used to send information about the calling client, +such as the library and Python version, to API services. +""" + +import platform + +import pkg_resources + +_PY_VERSION = platform.python_version() +_API_CORE_VERSION = pkg_resources.get_distribution("google-api-core").version + +try: + _GRPC_VERSION = pkg_resources.get_distribution("grpcio").version +except pkg_resources.DistributionNotFound: # pragma: NO COVER + _GRPC_VERSION = None + + +class ClientInfo(object): + """Client information used to generate a user-agent for API calls. + + This user-agent information is sent along with API calls to allow the + receiving service to do analytics on which versions of Python and Google + libraries are being used. + + Args: + python_version (str): The Python interpreter version, for example, + ``'2.7.13'``. + grpc_version (Optional[str]): The gRPC library version. + api_core_version (str): The google-api-core library version. + gapic_version (Optional[str]): The sversion of gapic-generated client + library, if the library was generated by gapic. + client_library_version (Optional[str]): The version of the client + library, generally used if the client library was not generated + by gapic or if additional functionality was built on top of + a gapic client library. + user_agent (Optional[str]): Prefix to the user agent header. This is + used to supply information such as application name or partner tool. + Recommended format: ``application-or-tool-ID/major.minor.version``. + """ + + def __init__( + self, + python_version=_PY_VERSION, + grpc_version=_GRPC_VERSION, + api_core_version=_API_CORE_VERSION, + gapic_version=None, + client_library_version=None, + user_agent=None, + ): + self.python_version = python_version + self.grpc_version = grpc_version + self.api_core_version = api_core_version + self.gapic_version = gapic_version + self.client_library_version = client_library_version + self.user_agent = user_agent + + def to_user_agent(self): + """Returns the user-agent string for this client info.""" + + # Note: the order here is important as the internal metrics system + # expects these items to be in specific locations. + ua = "" + + if self.user_agent is not None: + ua += "{user_agent} " + + ua += "gl-python/{python_version} " + + if self.grpc_version is not None: + ua += "grpc/{grpc_version} " + + ua += "gax/{api_core_version} " + + if self.gapic_version is not None: + ua += "gapic/{gapic_version} " + + if self.client_library_version is not None: + ua += "gccl/{client_library_version} " + + return ua.format(**self.__dict__).strip() diff --git a/api_core/google/api_core/gapic_v1/client_info.py b/api_core/google/api_core/gapic_v1/client_info.py index 069e0194ab31..bdc2ce440de3 100644 --- a/api_core/google/api_core/gapic_v1/client_info.py +++ b/api_core/google/api_core/gapic_v1/client_info.py @@ -18,22 +18,13 @@ such as the library and Python version, to API services. """ -import platform +from google.api_core import client_info -import pkg_resources - -_PY_VERSION = platform.python_version() -_API_CORE_VERSION = pkg_resources.get_distribution("google-api-core").version - -try: - _GRPC_VERSION = pkg_resources.get_distribution("grpcio").version -except pkg_resources.DistributionNotFound: # pragma: NO COVER - _GRPC_VERSION = None METRICS_METADATA_KEY = "x-goog-api-client" -class ClientInfo(object): +class ClientInfo(client_info.ClientInfo): """Client information used to generate a user-agent for API calls. This user-agent information is sent along with API calls to allow the @@ -56,47 +47,6 @@ class ClientInfo(object): Recommended format: ``application-or-tool-ID/major.minor.version``. """ - def __init__( - self, - python_version=_PY_VERSION, - grpc_version=_GRPC_VERSION, - api_core_version=_API_CORE_VERSION, - gapic_version=None, - client_library_version=None, - user_agent=None, - ): - self.python_version = python_version - self.grpc_version = grpc_version - self.api_core_version = api_core_version - self.gapic_version = gapic_version - self.client_library_version = client_library_version - self.user_agent = user_agent - - def to_user_agent(self): - """Returns the user-agent string for this client info.""" - - # Note: the order here is important as the internal metrics system - # expects these items to be in specific locations. - ua = "" - - if self.user_agent is not None: - ua += "{user_agent} " - - ua += "gl-python/{python_version} " - - if self.grpc_version is not None: - ua += "grpc/{grpc_version} " - - ua += "gax/{api_core_version} " - - if self.gapic_version is not None: - ua += "gapic/{gapic_version} " - - if self.client_library_version is not None: - ua += "gccl/{client_library_version} " - - return ua.format(**self.__dict__).strip() - def to_grpc_metadata(self): """Returns the gRPC metadata for this client info.""" return (METRICS_METADATA_KEY, self.to_user_agent()) diff --git a/api_core/tests/unit/gapic/test_client_info.py b/api_core/tests/unit/gapic/test_client_info.py index 0cca47905988..64080ffdbccd 100644 --- a/api_core/tests/unit/gapic/test_client_info.py +++ b/api_core/tests/unit/gapic/test_client_info.py @@ -16,59 +16,6 @@ from google.api_core.gapic_v1 import client_info -def test_constructor_defaults(): - info = client_info.ClientInfo() - - assert info.python_version is not None - assert info.grpc_version is not None - assert info.api_core_version is not None - assert info.gapic_version is None - assert info.client_library_version is None - - -def test_constructor_options(): - info = client_info.ClientInfo( - python_version="1", - grpc_version="2", - api_core_version="3", - gapic_version="4", - client_library_version="5", - user_agent="6" - ) - - assert info.python_version == "1" - assert info.grpc_version == "2" - assert info.api_core_version == "3" - assert info.gapic_version == "4" - assert info.client_library_version == "5" - assert info.user_agent == "6" - - -def test_to_user_agent_minimal(): - info = client_info.ClientInfo( - python_version="1", api_core_version="2", grpc_version=None - ) - - user_agent = info.to_user_agent() - - assert user_agent == "gl-python/1 gax/2" - - -def test_to_user_agent_full(): - info = client_info.ClientInfo( - python_version="1", - grpc_version="2", - api_core_version="3", - gapic_version="4", - client_library_version="5", - user_agent="app-name/1.0", - ) - - user_agent = info.to_user_agent() - - assert user_agent == "app-name/1.0 gl-python/1 grpc/2 gax/3 gapic/4 gccl/5" - - def test_to_grpc_metadata(): info = client_info.ClientInfo() diff --git a/api_core/tests/unit/test_client_info.py b/api_core/tests/unit/test_client_info.py new file mode 100644 index 000000000000..0eb17c5feb1c --- /dev/null +++ b/api_core/tests/unit/test_client_info.py @@ -0,0 +1,69 @@ +# Copyright 2017 Google LLC +# +# 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 google.api_core import client_info + + +def test_constructor_defaults(): + info = client_info.ClientInfo() + + assert info.python_version is not None + assert info.grpc_version is not None + assert info.api_core_version is not None + assert info.gapic_version is None + assert info.client_library_version is None + + +def test_constructor_options(): + info = client_info.ClientInfo( + python_version="1", + grpc_version="2", + api_core_version="3", + gapic_version="4", + client_library_version="5", + user_agent="6" + ) + + assert info.python_version == "1" + assert info.grpc_version == "2" + assert info.api_core_version == "3" + assert info.gapic_version == "4" + assert info.client_library_version == "5" + assert info.user_agent == "6" + + +def test_to_user_agent_minimal(): + info = client_info.ClientInfo( + python_version="1", api_core_version="2", grpc_version=None + ) + + user_agent = info.to_user_agent() + + assert user_agent == "gl-python/1 gax/2" + + +def test_to_user_agent_full(): + info = client_info.ClientInfo( + python_version="1", + grpc_version="2", + api_core_version="3", + gapic_version="4", + client_library_version="5", + user_agent="app-name/1.0", + ) + + user_agent = info.to_user_agent() + + assert user_agent == "app-name/1.0 gl-python/1 grpc/2 gax/3 gapic/4 gccl/5" diff --git a/bigquery/google/cloud/bigquery/_http.py b/bigquery/google/cloud/bigquery/_http.py index 0e5475f5f54b..643b24920bee 100644 --- a/bigquery/google/cloud/bigquery/_http.py +++ b/bigquery/google/cloud/bigquery/_http.py @@ -14,7 +14,6 @@ """Create / interact with Google BigQuery connections.""" -import google.api_core.gapic_v1.client_info from google.cloud import _http from google.cloud.bigquery import __version__ @@ -25,20 +24,16 @@ class Connection(_http.JSONConnection): :type client: :class:`~google.cloud.bigquery.client.Client` :param client: The client that owns the current connection. + + :type client_info: :class:`~google.api_core.client_info.ClientInfo` + :param client_info: (Optional) instance used to generate user agent. """ def __init__(self, client, client_info=None): - super(Connection, self).__init__(client) + super(Connection, self).__init__(client, client_info) - if client_info is None: - client_info = google.api_core.gapic_v1.client_info.ClientInfo( - gapic_version=__version__, client_library_version=__version__ - ) - else: - client_info.gapic_version = __version__ - client_info.client_library_version = __version__ - self._client_info = client_info - self._extra_headers = {} + self._client_info.gapic_version = __version__ + self._client_info.client_library_version = __version__ API_BASE_URL = "https://www.googleapis.com" """The base of the API call URL.""" @@ -48,22 +43,3 @@ def __init__(self, client, client_info=None): API_URL_TEMPLATE = "{api_base_url}/bigquery/{api_version}{path}" """A template for the URL of a particular API call.""" - - @property - def USER_AGENT(self): - return self._client_info.to_user_agent() - - @USER_AGENT.setter - def USER_AGENT(self, value): - self._client_info.user_agent = value - - @property - def _EXTRA_HEADERS(self): - self._extra_headers[ - _http.CLIENT_INFO_HEADER - ] = self._client_info.to_user_agent() - return self._extra_headers - - @_EXTRA_HEADERS.setter - def _EXTRA_HEADERS(self, value): - self._extra_headers = value diff --git a/bigquery/google/cloud/bigquery/client.py b/bigquery/google/cloud/bigquery/client.py index db53dab9ef11..f61c18f11bd4 100644 --- a/bigquery/google/cloud/bigquery/client.py +++ b/bigquery/google/cloud/bigquery/client.py @@ -128,7 +128,7 @@ class Client(ClientWithProject): default_query_job_config (google.cloud.bigquery.job.QueryJobConfig): (Optional) Default ``QueryJobConfig``. Will be merged into job configs passed into the ``query`` method. - client_info (google.api_core.gapic_v1.client_info.ClientInfo): + client_info (google.api_core.client_info.ClientInfo): The client info used to send a user-agent string along with API requests. If ``None``, then default info will be used. Generally, you only need to set this if you're developing your own library @@ -1363,7 +1363,7 @@ def _initiate_resumable_upload(self, stream, metadata, num_retries): """ chunk_size = _DEFAULT_CHUNKSIZE transport = self._http - headers = _get_upload_headers(self._connection.USER_AGENT) + headers = _get_upload_headers(self._connection.user_agent) upload_url = _RESUMABLE_URL_TEMPLATE.format(project=self.project) # TODO: modify ResumableUpload to take a retry.Retry object # that it can use for the initial RPC. @@ -1409,7 +1409,7 @@ def _do_multipart_upload(self, stream, metadata, size, num_retries): msg = _READ_LESS_THAN_SIZE.format(size, len(data)) raise ValueError(msg) - headers = _get_upload_headers(self._connection.USER_AGENT) + headers = _get_upload_headers(self._connection.user_agent) upload_url = _MULTIPART_URL_TEMPLATE.format(project=self.project) upload = MultipartUpload(upload_url, headers=headers) diff --git a/bigquery/tests/unit/test__http.py b/bigquery/tests/unit/test__http.py index d7d25ea445a0..939b5668e1e2 100644 --- a/bigquery/tests/unit/test__http.py +++ b/bigquery/tests/unit/test__http.py @@ -57,49 +57,21 @@ def test_user_agent(self): client = mock.Mock(_http=http, spec=["_http"]) conn = self._make_one(client) - conn.USER_AGENT = "my-application/1.2.3" + conn.user_agent = "my-application/1.2.3" req_data = "req-data-boring" result = conn.api_request("GET", "/rainbow", data=req_data, expect_json=False) self.assertEqual(result, data) expected_headers = { "Accept-Encoding": "gzip", - base_http.CLIENT_INFO_HEADER: conn.USER_AGENT, - "User-Agent": conn.USER_AGENT, - } - expected_uri = conn.build_api_url("/rainbow") - http.request.assert_called_once_with( - data=req_data, headers=expected_headers, method="GET", url=expected_uri - ) - self.assertIn("my-application/1.2.3", conn.USER_AGENT) - - def test_extra_headers(self): - from google.cloud import _http as base_http - - http = mock.create_autospec(requests.Session, instance=True) - response = requests.Response() - response.status_code = 200 - data = b"brent-spiner" - response._content = data - http.request.return_value = response - client = mock.Mock(_http=http, spec=["_http"]) - - conn = self._make_one(client) - conn._EXTRA_HEADERS["x-test-header"] = "a test value" - req_data = "req-data-boring" - result = conn.api_request("GET", "/rainbow", data=req_data, expect_json=False) - self.assertEqual(result, data) - - expected_headers = { - "Accept-Encoding": "gzip", - base_http.CLIENT_INFO_HEADER: conn.USER_AGENT, - "User-Agent": conn.USER_AGENT, - "x-test-header": "a test value", + base_http.CLIENT_INFO_HEADER: conn.user_agent, + "User-Agent": conn.user_agent, } expected_uri = conn.build_api_url("/rainbow") http.request.assert_called_once_with( data=req_data, headers=expected_headers, method="GET", url=expected_uri ) + self.assertIn("my-application/1.2.3", conn.user_agent) def test_extra_headers_replace(self): from google.cloud import _http as base_http @@ -113,15 +85,15 @@ def test_extra_headers_replace(self): client = mock.Mock(_http=http, spec=["_http"]) conn = self._make_one(client) - conn._EXTRA_HEADERS = {"x-test-header": "a test value"} + conn.extra_headers = {"x-test-header": "a test value"} req_data = "req-data-boring" result = conn.api_request("GET", "/rainbow", data=req_data, expect_json=False) self.assertEqual(result, data) expected_headers = { "Accept-Encoding": "gzip", - base_http.CLIENT_INFO_HEADER: conn.USER_AGENT, - "User-Agent": conn.USER_AGENT, + base_http.CLIENT_INFO_HEADER: conn.user_agent, + "User-Agent": conn.user_agent, "x-test-header": "a test value", } expected_uri = conn.build_api_url("/rainbow") diff --git a/bigquery/tests/unit/test_client.py b/bigquery/tests/unit/test_client.py index 08c36e0ac277..13889f90d7e8 100644 --- a/bigquery/tests/unit/test_client.py +++ b/bigquery/tests/unit/test_client.py @@ -54,7 +54,7 @@ def _make_connection(*responses): from google.cloud.exceptions import NotFound mock_conn = mock.create_autospec(google.cloud.bigquery._http.Connection) - mock_conn.USER_AGENT = "testing 1.2.3" + mock_conn.user_agent = "testing 1.2.3" mock_conn.api_request.side_effect = list(responses) + [NotFound("miss")] return mock_conn @@ -2752,7 +2752,7 @@ def _initiate_resumable_upload_helper(self, num_retries=None): + "/jobs?uploadType=resumable" ) self.assertEqual(upload.upload_url, upload_url) - expected_headers = _get_upload_headers(conn.USER_AGENT) + expected_headers = _get_upload_headers(conn.user_agent) self.assertEqual(upload._headers, expected_headers) self.assertFalse(upload.finished) self.assertEqual(upload._chunk_size, _DEFAULT_CHUNKSIZE) @@ -2830,7 +2830,7 @@ def _do_multipart_upload_success_helper(self, get_boundary, num_retries=None): + b"\r\n" + b"--==0==--" ) - headers = _get_upload_headers(conn.USER_AGENT) + headers = _get_upload_headers(conn.user_agent) headers["content-type"] = b'multipart/related; boundary="==0=="' fake_transport.request.assert_called_once_with( "POST", upload_url, data=payload, headers=headers @@ -4518,7 +4518,9 @@ def test_list_rows_with_missing_schema(self): self.assertIsNone(rows[2].age, msg=repr(table)) def test_list_rows_error(self): - client = self._make_one() + creds = _make_credentials() + http = object() + client = self._make_one(project=self.PROJECT, credentials=creds, _http=http) # neither Table nor tableReference with self.assertRaises(TypeError): diff --git a/core/google/cloud/_http.py b/core/google/cloud/_http.py index d7441e502995..653e43138e3a 100644 --- a/core/google/cloud/_http.py +++ b/core/google/cloud/_http.py @@ -16,10 +16,12 @@ import json import platform +import warnings from pkg_resources import get_distribution from six.moves.urllib.parse import urlencode +from google.api_core.client_info import ClientInfo from google.cloud import exceptions @@ -34,24 +36,98 @@ CLIENT_INFO_HEADER = "X-Goog-API-Client" CLIENT_INFO_TEMPLATE = "gl-python/" + platform.python_version() + " gccl/{}" +_USER_AGENT_ALL_CAPS_DEPRECATED = """\ +The 'USER_AGENT' class-level attribute is deprecated. Please use +'user_agent' instead. +""" + +_EXTRA_HEADERS_ALL_CAPS_DEPRECATED = """\ +The '_EXTRA_HEADERS' class-level attribute is deprecated. Please use +'extra_headers' instead. +""" + class Connection(object): """A generic connection to Google Cloud Platform. :type client: :class:`~google.cloud.client.Client` :param client: The client that owns the current connection. - """ - - USER_AGENT = DEFAULT_USER_AGENT - _EXTRA_HEADERS = {} - """Headers to be sent with every request. - Intended to be over-ridden by subclasses. + :type client_info: :class:`~google.api_core.client_info.ClientInfo` + :param client_info: (Optional) instance used to generate user agent. """ - def __init__(self, client): + _user_agent = DEFAULT_USER_AGENT + + def __init__(self, client, client_info=None): self._client = client + if client_info is None: + client_info = ClientInfo() + + self._client_info = client_info + self._extra_headers = {} + + @property + def USER_AGENT(self): + """Deprecated: get / set user agent sent by connection. + + :rtype: str + :returns: user agent + """ + warnings.warn( + _USER_AGENT_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2) + return self.user_agent + + @USER_AGENT.setter + def USER_AGENT(self, value): + warnings.warn( + _USER_AGENT_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2) + self.user_agent = value + + @property + def user_agent(self): + """Get / set user agent sent by connection. + + :rtype: str + :returns: user agent + """ + return self._client_info.to_user_agent() + + @user_agent.setter + def user_agent(self, value): + self._client_info.user_agent = value + + @property + def _EXTRA_HEADERS(self): + """Deprecated: get / set extra headers sent by connection. + + :rtype: dict + :returns: header keys / values + """ + warnings.warn( + _EXTRA_HEADERS_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2) + return self.extra_headers + + @_EXTRA_HEADERS.setter + def _EXTRA_HEADERS(self, value): + warnings.warn( + _EXTRA_HEADERS_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2) + self.extra_headers = value + + @property + def extra_headers(self): + """Get / set extra headers sent by connection. + + :rtype: dict + :returns: header keys / values + """ + return self._extra_headers + + @extra_headers.setter + def extra_headers(self, value): + self._extra_headers = value + @property def credentials(self): """Getter for current credentials. @@ -181,13 +257,14 @@ def _make_request( :returns: The HTTP response. """ headers = headers or {} - headers.update(self._EXTRA_HEADERS) + headers.update(self.extra_headers) headers["Accept-Encoding"] = "gzip" if content_type: headers["Content-Type"] = content_type - headers["User-Agent"] = self.USER_AGENT + headers[CLIENT_INFO_HEADER] = self.user_agent + headers["User-Agent"] = self.user_agent return self._do_request(method, url, headers, data, target_object) diff --git a/core/tests/unit/test__http.py b/core/tests/unit/test__http.py index 7c8aec215f22..d50494e8eadc 100644 --- a/core/tests/unit/test__http.py +++ b/core/tests/unit/test__http.py @@ -14,6 +14,7 @@ import json import unittest +import warnings import mock import requests @@ -30,10 +31,90 @@ def _get_target_class(): def _make_one(self, *args, **kw): return self._get_target_class()(*args, **kw) - def test_constructor(self): + def test_constructor_defaults(self): + from google.api_core.client_info import ClientInfo + client = object() conn = self._make_one(client) self.assertIs(conn._client, client) + self.assertIsInstance(conn._client_info, ClientInfo) + + def test_constructor_explicit(self): + client = object() + client_info = object() + conn = self._make_one(client, client_info=client_info) + self.assertIs(conn._client, client) + + def test_user_agent_all_caps_getter_deprecated(self): + client = object() + conn = self._make_one(client) + + with mock.patch.object(warnings, "warn", autospec=True) as warn: + self.assertEqual(conn.USER_AGENT, conn._client_info.to_user_agent()) + + warn.assert_called_once_with(mock.ANY, DeprecationWarning, stacklevel=2) + + def test_user_agent_all_caps_setter_deprecated(self): + conn = self._make_one(object()) + user_agent = "testing" + + with mock.patch.object(warnings, "warn", autospec=True) as warn: + conn.USER_AGENT = user_agent + + self.assertEqual(conn._client_info.user_agent, user_agent) + warn.assert_called_once_with(mock.ANY, DeprecationWarning, stacklevel=2) + + def test_user_agent_getter(self): + conn = self._make_one(object()) + self.assertEqual(conn.user_agent, conn._client_info.to_user_agent()) + + def test_user_agent_setter(self): + conn = self._make_one(object()) + user_agent = "testing" + conn.user_agent = user_agent + self.assertEqual(conn._client_info.user_agent, user_agent) + + def test_extra_headers_all_caps_getter_deprecated(self): + client = object() + conn = self._make_one(client) + expected = conn._extra_headers = {"foo": "bar"} + + with mock.patch.object(warnings, "warn", autospec=True) as warn: + self.assertEqual(conn._EXTRA_HEADERS, expected) + + warn.assert_called_once_with(mock.ANY, DeprecationWarning, stacklevel=2) + + def test_extra_headers_all_caps_setter_deprecated(self): + conn = self._make_one(object()) + extra_headers = {"foo": "bar"} + + with mock.patch.object(warnings, "warn", autospec=True) as warn: + conn._EXTRA_HEADERS = extra_headers + + self.assertEqual(conn._extra_headers, extra_headers) + warn.assert_called_once_with(mock.ANY, DeprecationWarning, stacklevel=2) + + def test_extra_headers_getter_default(self): + conn = self._make_one(object()) + expected = {} + self.assertEqual(conn.extra_headers, expected) + + def test_extra_headers_getter_overridden(self): + conn = self._make_one(object()) + expected = conn._extra_headers = {"foo": "bar"} + self.assertEqual(conn.extra_headers, expected) + + def test_extra_headers_item_assignment(self): + conn = self._make_one(object()) + expected = {"foo": "bar"} + conn.extra_headers["foo"] = "bar" + self.assertEqual(conn._extra_headers, expected) + + def test_extra_headers_setter(self): + conn = self._make_one(object()) + expected = {"foo": "bar"} + conn.extra_headers = expected + self.assertEqual(conn._extra_headers, expected) def test_credentials_property(self): client = mock.Mock(spec=["_credentials"]) @@ -45,15 +126,6 @@ def test_http_property(self): conn = self._make_one(client) self.assertIs(conn.http, client._http) - def test_user_agent_format(self): - from pkg_resources import get_distribution - - expected_ua = "gcloud-python/{0}".format( - get_distribution("google-cloud-core").version - ) - conn = self._make_one(object()) - self.assertEqual(conn.USER_AGENT, expected_ua) - def make_response(status=http_client.OK, content=b"", headers={}): response = requests.Response() @@ -127,6 +199,8 @@ def test_build_api_url_w_extra_query_params(self): self.assertEqual(parms["qux"], ["quux", "corge"]) def test__make_request_no_data_no_content_type_no_headers(self): + from google.cloud._http import CLIENT_INFO_HEADER + http = make_requests_session([make_response()]) client = mock.Mock(_http=http, spec=["_http"]) conn = self._make_one(client) @@ -137,12 +211,18 @@ def test__make_request_no_data_no_content_type_no_headers(self): self.assertEqual(response.status_code, http_client.OK) self.assertEqual(response.content, b"") - expected_headers = {"Accept-Encoding": "gzip", "User-Agent": conn.USER_AGENT} + expected_headers = { + "Accept-Encoding": "gzip", + "User-Agent": conn.user_agent, + CLIENT_INFO_HEADER: conn.user_agent, + } http.request.assert_called_once_with( method="GET", url=url, headers=expected_headers, data=None ) def test__make_request_w_data_no_extra_headers(self): + from google.cloud._http import CLIENT_INFO_HEADER + http = make_requests_session([make_response()]) client = mock.Mock(_http=http, spec=["_http"]) conn = self._make_one(client) @@ -154,13 +234,16 @@ def test__make_request_w_data_no_extra_headers(self): expected_headers = { "Accept-Encoding": "gzip", "Content-Type": "application/json", - "User-Agent": conn.USER_AGENT, + "User-Agent": conn.user_agent, + CLIENT_INFO_HEADER: conn.user_agent, } http.request.assert_called_once_with( method="GET", url=url, headers=expected_headers, data=data ) def test__make_request_w_extra_headers(self): + from google.cloud._http import CLIENT_INFO_HEADER + http = make_requests_session([make_response()]) client = mock.Mock(_http=http, spec=["_http"]) conn = self._make_one(client) @@ -171,13 +254,16 @@ def test__make_request_w_extra_headers(self): expected_headers = { "Accept-Encoding": "gzip", "X-Foo": "foo", - "User-Agent": conn.USER_AGENT, + "User-Agent": conn.user_agent, + CLIENT_INFO_HEADER: conn.user_agent, } http.request.assert_called_once_with( method="GET", url=url, headers=expected_headers, data=None ) def test_api_request_defaults(self): + from google.cloud._http import CLIENT_INFO_HEADER + http = make_requests_session( [make_response(content=b"{}", headers=self.JSON_HEADERS)] ) @@ -187,7 +273,11 @@ def test_api_request_defaults(self): self.assertEqual(conn.api_request("GET", path), {}) - expected_headers = {"Accept-Encoding": "gzip", "User-Agent": conn.USER_AGENT} + expected_headers = { + "Accept-Encoding": "gzip", + "User-Agent": conn.user_agent, + CLIENT_INFO_HEADER: conn.user_agent, + } expected_url = "{base}/mock/{version}{path}".format( base=conn.API_BASE_URL, version=conn.API_VERSION, path=path ) @@ -215,6 +305,7 @@ def test_api_request_wo_json_expected(self): def test_api_request_w_query_params(self): from six.moves.urllib.parse import parse_qs from six.moves.urllib.parse import urlsplit + from google.cloud._http import CLIENT_INFO_HEADER http = make_requests_session([self.EMPTY_JSON_RESPONSE]) client = mock.Mock(_http=http, spec=["_http"]) @@ -224,7 +315,11 @@ def test_api_request_w_query_params(self): self.assertEqual(result, {}) - expected_headers = {"Accept-Encoding": "gzip", "User-Agent": conn.USER_AGENT} + expected_headers = { + "Accept-Encoding": "gzip", + "User-Agent": conn.user_agent, + CLIENT_INFO_HEADER: conn.user_agent, + } http.request.assert_called_once_with( method="GET", url=mock.ANY, headers=expected_headers, data=None ) @@ -240,6 +335,8 @@ def test_api_request_w_query_params(self): self.assertEqual(parms["baz"], ["qux", "quux"]) def test_api_request_w_headers(self): + from google.cloud._http import CLIENT_INFO_HEADER + http = make_requests_session([self.EMPTY_JSON_RESPONSE]) client = mock.Mock(_http=http, spec=["_http"]) conn = self._make_mock_one(client) @@ -249,18 +346,21 @@ def test_api_request_w_headers(self): expected_headers = { "Accept-Encoding": "gzip", - "User-Agent": conn.USER_AGENT, + "User-Agent": conn.user_agent, "X-Foo": "bar", + CLIENT_INFO_HEADER: conn.user_agent, } http.request.assert_called_once_with( method="GET", url=mock.ANY, headers=expected_headers, data=None ) def test_api_request_w_extra_headers(self): + from google.cloud._http import CLIENT_INFO_HEADER + http = make_requests_session([self.EMPTY_JSON_RESPONSE]) client = mock.Mock(_http=http, spec=["_http"]) conn = self._make_mock_one(client) - conn._EXTRA_HEADERS = { + conn.extra_headers = { "X-Baz": "dax-quux", "X-Foo": "not-bar", # Collision with ``headers``. } @@ -271,15 +371,18 @@ def test_api_request_w_extra_headers(self): expected_headers = { "Accept-Encoding": "gzip", - "User-Agent": conn.USER_AGENT, + "User-Agent": conn.user_agent, "X-Foo": "not-bar", # The one passed-in is overridden. "X-Baz": "dax-quux", + CLIENT_INFO_HEADER: conn.user_agent, } http.request.assert_called_once_with( method="GET", url=mock.ANY, headers=expected_headers, data=None ) def test_api_request_w_data(self): + from google.cloud._http import CLIENT_INFO_HEADER + http = make_requests_session([self.EMPTY_JSON_RESPONSE]) client = mock.Mock(_http=http, spec=["_http"]) conn = self._make_mock_one(client) @@ -292,7 +395,8 @@ def test_api_request_w_data(self): expected_headers = { "Accept-Encoding": "gzip", "Content-Type": "application/json", - "User-Agent": conn.USER_AGENT, + "User-Agent": conn.user_agent, + CLIENT_INFO_HEADER: conn.user_agent, } http.request.assert_called_once_with( diff --git a/docs/core/client_info.rst b/docs/core/client_info.rst new file mode 100644 index 000000000000..e976b1863c7c --- /dev/null +++ b/docs/core/client_info.rst @@ -0,0 +1,11 @@ +Client Information Helpers +========================== + +.. automodule:: google.api_core.client_info + :members: + :show-inheritance: + +.. automodule:: google.api_core.gapic_v1.client_info + :members: + :show-inheritance: + diff --git a/docs/core/index.rst b/docs/core/index.rst index 5b44ddcb071d..45c68ad08ee2 100644 --- a/docs/core/index.rst +++ b/docs/core/index.rst @@ -5,15 +5,16 @@ Core config auth client + client_info exceptions helpers - retry - timeout - page_iterator iam operation operations_client + page_iterator path_template + retry + timeout Changelog ~~~~~~~~~