Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core, BigQuery: refactor 'client_info' support. #7849

Merged
merged 14 commits into from
May 7, 2019
Merged
Show file tree
Hide file tree
Changes from 13 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
96 changes: 96 additions & 0 deletions api_core/google/api_core/client_info.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a unit test for this? It'd be good to be doubly sure that it doesn't blow up when _GRPC_VERSION is None.

_GRPC_VERSION = None


class ClientInfo(object):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tswast In order to support the API libraries which do not install grpc, I had to split out a base ClientInfo class which is importable if 'grpc' is absent. The one in google.api_core.gapic_v1.client_info subclasses and adds the to_grpc_metadata method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Good catch.

"""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()
54 changes: 2 additions & 52 deletions api_core/google/api_core/gapic_v1/client_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand Down
53 changes: 0 additions & 53 deletions api_core/tests/unit/gapic/test_client_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
69 changes: 69 additions & 0 deletions api_core/tests/unit/test_client_info.py
Original file line number Diff line number Diff line change
@@ -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"
36 changes: 6 additions & 30 deletions bigquery/google/cloud/bigquery/_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand All @@ -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."""
Expand All @@ -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
6 changes: 3 additions & 3 deletions bigquery/google/cloud/bigquery/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Loading