From 4b8f07cec48eeadfd6f40db59c75f1de01763826 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 26 Nov 2020 02:20:29 +0000 Subject: [PATCH 01/15] feat: Add support for static discovery documents --- googleapiclient/discovery.py | 60 ++++++++++++++------- googleapiclient/discovery_cache/__init__.py | 36 ++++++++++++- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py index b5d46968fa1..dff35b55b0c 100644 --- a/googleapiclient/discovery.py +++ b/googleapiclient/discovery.py @@ -193,6 +193,7 @@ def build( adc_cert_path=None, adc_key_path=None, num_retries=1, + static_discovery=True, ): """Construct a Resource for interacting with an API. @@ -246,6 +247,9 @@ def build( https://google.aip.dev/auth/4114 num_retries: Integer, number of times to retry discovery with randomized exponential backoff in case of intermittent/connection issues. + static_discovery: Boolean, whether or not to use the static discovery docs + included in the package when the discovery doc is not available in the + cache. Returns: A Resource object with methods for interacting with the service. @@ -274,6 +278,7 @@ def build( cache, developerKey, num_retries=num_retries, + static_discovery=static_discovery, ) service = build_from_document( content, @@ -330,7 +335,13 @@ def _discovery_service_uri_options(discoveryServiceUrl, version): def _retrieve_discovery_doc( - url, http, cache_discovery, cache=None, developerKey=None, num_retries=1 + url, + http, + cache_discovery, + cache=None, + developerKey=None, + num_retries=1, + static_discovery=True ): """Retrieves the discovery_doc from cache or the internet. @@ -345,13 +356,18 @@ def _retrieve_discovery_doc( from the API Console. num_retries: Integer, number of times to retry discovery with randomized exponential backoff in case of intermittent/connection issues. + static_discovery: Boolean, whether or not to use the static discovery docs + included in the package when the discovery doc is not available in the + cache. Returns: A unicode string representation of the discovery document. """ - if cache_discovery: - from . import discovery_cache + from . import discovery_cache + + content = None + if cache_discovery: if cache is None: cache = discovery_cache.autodetect() if cache: @@ -359,21 +375,29 @@ def _retrieve_discovery_doc( if content: return content - actual_url = url - # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment - # variable that contains the network address of the client sending the - # request. If it exists then add that to the request for the discovery - # document to avoid exceeding the quota on discovery requests. - if "REMOTE_ADDR" in os.environ: - actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"]) - if developerKey: - actual_url = _add_query_parameter(url, "key", developerKey) - logger.debug("URL being requested: GET %s", actual_url) - - # Execute this request with retries build into HttpRequest - # Note that it will already raise an error if we don't get a 2xx response - req = HttpRequest(http, HttpRequest.null_postproc, actual_url) - resp, content = req.execute(num_retries=num_retries) + # At this point, the discovery document was not found in the cache so + # we can attempt to retreive the static discovery document from the library. + if static_discovery: + content = discovery_cache.get_static_doc(url) + + # If the content is None, retrieve the discovery doc from the internet + # because it is not in the cache or the static doc directory. + if content is None: + actual_url = url + # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment + # variable that contains the network address of the client sending the + # request. If it exists then add that to the request for the discovery + # document to avoid exceeding the quota on discovery requests. + if "REMOTE_ADDR" in os.environ: + actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"]) + if developerKey: + actual_url = _add_query_parameter(url, "key", developerKey) + logger.debug("URL being requested: GET %s", actual_url) + + # Execute this request with retries build into HttpRequest + # Note that it will already raise an error if we don't get a 2xx response + req = HttpRequest(http, HttpRequest.null_postproc, actual_url) + resp, content = req.execute(num_retries=num_retries) try: content = content.decode("utf-8") diff --git a/googleapiclient/discovery_cache/__init__.py b/googleapiclient/discovery_cache/__init__.py index 197f6bc0a1a..54227b27938 100644 --- a/googleapiclient/discovery_cache/__init__.py +++ b/googleapiclient/discovery_cache/__init__.py @@ -23,7 +23,8 @@ LOGGER = logging.getLogger(__name__) DISCOVERY_DOC_MAX_AGE = 60 * 60 * 24 # 1 day - +DISCOVERY_DOC_DIR = os.path.join(os.path.dirname( + os.path.realpath(__file__)), 'documents') def autodetect(): """Detects an appropriate cache module and returns it. @@ -48,3 +49,36 @@ def autodetect(): LOGGER.info("file_cache is only supported with oauth2client<4.0.0", exc_info=False) return None + +def get_static_doc(uri): + """Retrieves the discovery document from the directory defined in + DISCOVERY_DOC_STATIC_DIR corresponding to the uri provided. + + Args: + uri: string, The URI of the discovery document in the format + https://{domain}/discovery/{discoveryVer}/apis/{api}/{apiVersion}/rest + + Returns: + A string containing the contents of the JSON discovery document, + otherwise None if the JSON discovery document was not found. + """ + + doc_name = None + content = None + + # Extract the {apiVersion} and {api} from the uri which are the 2nd and 3rd + # last parts of the uri respectively. + # https://www.googleapis.com/discovery/v1/apis/{api}/{apiVersion}/rest + uri_parts = uri.split('/') + if len(uri_parts) > 3: + doc_name = "{}.{}.json".format(uri_parts[-3], uri_parts[-2]) + + try: + with open(os.path.join(DISCOVERY_DOC_DIR, doc_name), 'r') as f: + content = f.read() + except FileNotFoundError: + # File does not exist. Nothing to do here. + pass + + return content + From e9539907db77893ed1627ac6203384a70230502b Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 11 Dec 2020 16:43:49 +0000 Subject: [PATCH 02/15] Use serviceName and version from build() --- googleapiclient/discovery.py | 8 +++++- googleapiclient/discovery_cache/__init__.py | 27 ++++++++------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py index dff35b55b0c..fc7397a930e 100644 --- a/googleapiclient/discovery.py +++ b/googleapiclient/discovery.py @@ -275,6 +275,8 @@ def build( requested_url, discovery_http, cache_discovery, + serviceName, + version, cache, developerKey, num_retries=num_retries, @@ -338,6 +340,8 @@ def _retrieve_discovery_doc( url, http, cache_discovery, + serviceName, + version, cache=None, developerKey=None, num_retries=1, @@ -350,6 +354,8 @@ def _retrieve_discovery_doc( http: httplib2.Http, An instance of httplib2.Http or something that acts like it through which HTTP requests will be made. cache_discovery: Boolean, whether or not to cache the discovery doc. + serviceName: string, name of the service. + version: string, the version of the service. cache: googleapiclient.discovery_cache.base.Cache, an optional cache object for the discovery documents. developerKey: string, Key for controlling API usage, generated @@ -378,7 +384,7 @@ def _retrieve_discovery_doc( # At this point, the discovery document was not found in the cache so # we can attempt to retreive the static discovery document from the library. if static_discovery: - content = discovery_cache.get_static_doc(url) + content = discovery_cache.get_static_doc(serviceName, version) # If the content is None, retrieve the discovery doc from the internet # because it is not in the cache or the static doc directory. diff --git a/googleapiclient/discovery_cache/__init__.py b/googleapiclient/discovery_cache/__init__.py index 54227b27938..886bef3e539 100644 --- a/googleapiclient/discovery_cache/__init__.py +++ b/googleapiclient/discovery_cache/__init__.py @@ -50,35 +50,28 @@ def autodetect(): exc_info=False) return None -def get_static_doc(uri): +def get_static_doc(serviceName, version): """Retrieves the discovery document from the directory defined in DISCOVERY_DOC_STATIC_DIR corresponding to the uri provided. Args: - uri: string, The URI of the discovery document in the format - https://{domain}/discovery/{discoveryVer}/apis/{api}/{apiVersion}/rest + serviceName: string, name of the service. + version: string, the version of the service. Returns: A string containing the contents of the JSON discovery document, otherwise None if the JSON discovery document was not found. """ - doc_name = None content = None + doc_name = "{}.{}.json".format(serviceName, version) - # Extract the {apiVersion} and {api} from the uri which are the 2nd and 3rd - # last parts of the uri respectively. - # https://www.googleapis.com/discovery/v1/apis/{api}/{apiVersion}/rest - uri_parts = uri.split('/') - if len(uri_parts) > 3: - doc_name = "{}.{}.json".format(uri_parts[-3], uri_parts[-2]) - - try: - with open(os.path.join(DISCOVERY_DOC_DIR, doc_name), 'r') as f: - content = f.read() - except FileNotFoundError: - # File does not exist. Nothing to do here. - pass + try: + with open(os.path.join(DISCOVERY_DOC_DIR, doc_name), 'r') as f: + content = f.read() + except FileNotFoundError: + # File does not exist. Nothing to do here. + pass return content From 4fe4fec98cc85e4a99c16da8a11437513eaa0f4a Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 11 Dec 2020 16:46:56 +0000 Subject: [PATCH 03/15] Fix typo --- googleapiclient/discovery_cache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/googleapiclient/discovery_cache/__init__.py b/googleapiclient/discovery_cache/__init__.py index 886bef3e539..fdd84f2b6ce 100644 --- a/googleapiclient/discovery_cache/__init__.py +++ b/googleapiclient/discovery_cache/__init__.py @@ -52,7 +52,7 @@ def autodetect(): def get_static_doc(serviceName, version): """Retrieves the discovery document from the directory defined in - DISCOVERY_DOC_STATIC_DIR corresponding to the uri provided. + DISCOVERY_DOC_DIR corresponding to the uri provided. Args: serviceName: string, name of the service. From cec0d4c8b29d12b246fa2b20023295ffdad8d1c6 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 12 Dec 2020 01:01:01 +0000 Subject: [PATCH 04/15] Add tests for retrieving static discovery docs --- tests/test_discovery.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index c6bc59937b8..fde24a6bd3d 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -1148,6 +1148,19 @@ def import_mock(name, *args, **kwargs): ) +class DiscoveryFromStaticDocument(unittest.TestCase): + def test_can_build_from_static_document_when_enabled(self): + http = HttpMockSequence([({"status": "400"}, "")]) + drive = build("drive", "v3", http=http, cache_discovery=False, static_discovery=True) + self.assertIsNotNone(drive) + self.assertTrue(hasattr(drive, "files")) + + def test_disable_build_from_static_document(self): + http = HttpMockSequence([({"status": "400"}, "")]) + with self.assertRaises(HttpError): + build("drive", "v3", http=http, cache_discovery=False, static_discovery=False) + + class DictCache(Cache): def __init__(self): self.d = {} From a55e09d083adf82fccd4c746ce521a01d2d860d6 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 12 Dec 2020 01:09:48 +0000 Subject: [PATCH 05/15] fix doc string --- googleapiclient/discovery_cache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/googleapiclient/discovery_cache/__init__.py b/googleapiclient/discovery_cache/__init__.py index fdd84f2b6ce..3f59e73bb1c 100644 --- a/googleapiclient/discovery_cache/__init__.py +++ b/googleapiclient/discovery_cache/__init__.py @@ -52,7 +52,7 @@ def autodetect(): def get_static_doc(serviceName, version): """Retrieves the discovery document from the directory defined in - DISCOVERY_DOC_DIR corresponding to the uri provided. + DISCOVERY_DOC_DIR corresponding to the serviceName and version provided. Args: serviceName: string, name of the service. From 9b049bd8df3237d7e7ef7614adf0dbc16195f366 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 12 Dec 2020 22:18:57 +0000 Subject: [PATCH 06/15] Test retrieving doc from internet w/o local copy --- tests/test_discovery.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index fde24a6bd3d..f528bdf0c5c 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -1151,14 +1151,22 @@ def import_mock(name, *args, **kwargs): class DiscoveryFromStaticDocument(unittest.TestCase): def test_can_build_from_static_document_when_enabled(self): http = HttpMockSequence([({"status": "400"}, "")]) - drive = build("drive", "v3", http=http, cache_discovery=False, static_discovery=True) + drive = build("drive", "v3", http=http, cache_discovery=False, + static_discovery=True) self.assertIsNotNone(drive) self.assertTrue(hasattr(drive, "files")) def test_disable_build_from_static_document(self): http = HttpMockSequence([({"status": "400"}, "")]) with self.assertRaises(HttpError): - build("drive", "v3", http=http, cache_discovery=False, static_discovery=False) + build("drive", "v3", http=http, cache_discovery=False, + static_discovery=False) + + def test_retrieve_from_internet_when_static_doc_does_not_exist(self): + http = HttpMockSequence([({"status": "400"}, "")]) + with self.assertRaises(HttpError): + build("doesnotexist", "v3", http=http, cache_discovery=False, + static_discovery=True) class DictCache(Cache): From e4cf59c781174002c30594f7321706ecf800d637 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 12 Dec 2020 22:19:59 +0000 Subject: [PATCH 07/15] Return content if discovery doc exists locally --- googleapiclient/discovery.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py index fc7397a930e..ee75ca46ba8 100644 --- a/googleapiclient/discovery.py +++ b/googleapiclient/discovery.py @@ -385,6 +385,8 @@ def _retrieve_discovery_doc( # we can attempt to retreive the static discovery document from the library. if static_discovery: content = discovery_cache.get_static_doc(serviceName, version) + if content: + return content # If the content is None, retrieve the discovery doc from the internet # because it is not in the cache or the static doc directory. From ea0237aaebfa9873eb9a3c124eb06b473c0a3763 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 12 Dec 2020 22:20:38 +0000 Subject: [PATCH 08/15] Check for HTTP 404 in case API is not available --- describe.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/describe.py b/describe.py index 3d9d7a46a74..cb72f616b39 100755 --- a/describe.py +++ b/describe.py @@ -395,6 +395,16 @@ def document_api(name, version, uri): """ try: service = build(name, version) + + http = build_http() + response, content = http.request( + uri or uritemplate.expand( + FLAGS.discovery_uri_template, {"api": name, "apiVersion": version} + ) + ) + if 'status' in response: + if response['status'] == '404': + raise UnknownApiNameOrVersion except UnknownApiNameOrVersion as e: print("Warning: {} {} found but could not be built.".format(name, version)) return @@ -402,12 +412,6 @@ def document_api(name, version, uri): print("Warning: {} {} returned {}.".format(name, version, e)) return - http = build_http() - response, content = http.request( - uri or uritemplate.expand( - FLAGS.discovery_uri_template, {"api": name, "apiVersion": version} - ) - ) discovery = json.loads(content) version = safe_version(version) From 356512f41ba900533765ab86784e9c4471a647a2 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 12 Dec 2020 22:27:27 +0000 Subject: [PATCH 09/15] Rename tests --- tests/test_discovery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index f528bdf0c5c..227b6a030c5 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -1149,14 +1149,14 @@ def import_mock(name, *args, **kwargs): class DiscoveryFromStaticDocument(unittest.TestCase): - def test_can_build_from_static_document_when_enabled(self): + def test_retrieve_from_local_when_static_discovery_true(self): http = HttpMockSequence([({"status": "400"}, "")]) drive = build("drive", "v3", http=http, cache_discovery=False, static_discovery=True) self.assertIsNotNone(drive) self.assertTrue(hasattr(drive, "files")) - def test_disable_build_from_static_document(self): + def test_retrieve_from_internet_when_static_discovery_false(self): http = HttpMockSequence([({"status": "400"}, "")]) with self.assertRaises(HttpError): build("drive", "v3", http=http, cache_discovery=False, From a774407fa16108615631aeec6c30c2e8d8c2cd8d Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 17 Dec 2020 13:01:15 +0000 Subject: [PATCH 10/15] Update docs to include `static_discovery` param --- docs/start.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/start.md b/docs/start.md index e84db43cd87..414e85fcedb 100644 --- a/docs/start.md +++ b/docs/start.md @@ -19,24 +19,24 @@ It is important to understand the basics of how API authentication and authoriza These API calls do not access any private user data. Your application must authenticate itself as an application belonging to your Google Cloud project. This is needed to measure project usage for accounting purposes. **API key**: To authenticate your application, use an [API key](https://cloud.google.com/docs/authentication/api-keys) for your Google Cloud Console project. Every simple access call your application makes must include this key. - + > **Warning**: Keep your API key private. If someone obtains your key, they could use it to consume your quota or incur charges against your Google Cloud project. - + ### 2. Authorized API access (OAuth 2.0) These API calls access private user data. Before you can call them, the user that has access to the private data must grant your application access. Therefore, your application must be authenticated, the user must grant access for your application, and the user must be authenticated in order to grant that access. All of this is accomplished with [OAuth 2.0](https://developers.google.com/identity/protocols/OAuth2) and libraries written for it. * **Scope**: Each API defines one or more scopes that declare a set of operations permitted. For example, an API might have read-only and read-write scopes. When your application requests access to user data, the request must include one or more scopes. The user needs to approve the scope of access your application is requesting. A list of accessible OAuth 2.0 scopes can be [found here](https://developers.google.com/identity/protocols/oauth2/scopes). * **Refresh and access tokens**: When a user grants your application access, the OAuth 2.0 authorization server provides your application with refresh and access tokens. These tokens are only valid for the scope requested. Your application uses access tokens to authorize API calls. Access tokens expire, but refresh tokens do not. Your application can use a refresh token to acquire a new access token. - + > **Warning**: Keep refresh and access tokens private. If someone obtains your tokens, they could use them to access private user data. - + * **Client ID and client secret**: These strings uniquely identify your application and are used to acquire tokens. They are created for your Google Cloud project on the [API Access pane](https://console.developers.google.com/apis/credentials) of the Google Cloud. There are several types of client IDs, so be sure to get the correct type for your application: - + * Web application client IDs * Installed application client IDs * [Service Account](https://developers.google.com/identity/protocols/OAuth2ServiceAccount) client IDs - + > **Warning**: Keep your client secret private. If someone obtains your client secret, they could use it to consume your quota, incur charges against your Google Cloud project, and request access to user data. ## Building and calling a service @@ -45,7 +45,7 @@ This section describes how to build an API-specific service object, make calls t ### Build the service object -Whether you are using simple or authorized API access, you use the [build()](http://googleapis.github.io/google-api-python-client/docs/epy/googleapiclient.discovery-module.html#build) function to create a service object. It takes an API name and API version as arguments. You can see the list of all API versions on the [Supported APIs](dyn/index.md) page. The service object is constructed with methods specific to the given API. +Whether you are using simple or authorized API access, you use the [build()](http://googleapis.github.io/google-api-python-client/docs/epy/googleapiclient.discovery-module.html#build) function to create a service object. It takes an API name and API version as arguments. You can see the list of all API versions on the [Supported APIs](dyn/index.md) page. When `build()` is called, a service object will attempt to be constructed with methods specific to the given API. `httplib2`, the underlying transport library, makes all connections persistent by default. Use the service object with a context manager or call `close` to avoid leaving sockets open. @@ -65,6 +65,17 @@ with build('drive', 'v3') as service: # ... ``` +**Note**: Under the hood, the `build()` function retrieves a discovery artifact in order to construct the service object. If the `cache_discovery` argument of `build()` is set to `True`, the library will attempt to retrieve the discovery artifact from the legacy cache which is only supported with `oauth2client<4.0`. If the artifact is not available in the legacy cache and the `static_discovery` argument of `build()` is set to `True`, the library will attempt to retieve the discovery artifact from the static copy of the discovery artifacts that are shipped with the library. As a last resort if the discovery artifact is not on disk, the discovery artifact will be retrieved from the internet. The maintainers recommend keeping `static_discovery=True`, which is the default, in order to avoid reliability issues related to transient network errors. The library currently does not detect breaking changes of discovery artifacts, and as a result, user code is not guaranteed to continue to succeed or fail with different versions of `google-api-python-client`. Users can set `static_discovery=False` as workaround if there is an incompatible discovery artifact shipped with the library. In addition, users may roll back the version of `google-api-python-client` if there is a breaking change in a discovery artifact. The maintainers of `google-api-python-client` encourage users to report issues with discovery artifacts so that the discovery artifacts can be corrected. + +In the below example, the discovery artifact for `'drive'` version `'v3'` will only be retrieved from the internet if it is not available in the legacy cache and it is not one of the static discovery artifacts shipped with the library. + +```python +from googleapiclient.discovery import build + +with build('drive', 'v3', cache_discovery=True, static_discovery=True): + # ... +``` + ### Collections Each API service provides access to one or more resources. A set of resources of the same type is called a collection. The names of these collections are specific to the API. The service object is constructed with a function for every collection defined by the API. If the given API has a collection named `stamps`, you create the collection object like this: From 18281da99ddc5e8be49e5d3d3ae7092167ff3018 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 18 Dec 2020 03:53:48 +0000 Subject: [PATCH 11/15] Only request docs if static_discovery=False --- docs/start.md | 11 +------- googleapiclient/discovery.py | 49 ++++++++++++++++-------------------- tests/test_discovery.py | 7 +++--- 3 files changed, 26 insertions(+), 41 deletions(-) diff --git a/docs/start.md b/docs/start.md index 414e85fcedb..3d9cb244469 100644 --- a/docs/start.md +++ b/docs/start.md @@ -65,16 +65,7 @@ with build('drive', 'v3') as service: # ... ``` -**Note**: Under the hood, the `build()` function retrieves a discovery artifact in order to construct the service object. If the `cache_discovery` argument of `build()` is set to `True`, the library will attempt to retrieve the discovery artifact from the legacy cache which is only supported with `oauth2client<4.0`. If the artifact is not available in the legacy cache and the `static_discovery` argument of `build()` is set to `True`, the library will attempt to retieve the discovery artifact from the static copy of the discovery artifacts that are shipped with the library. As a last resort if the discovery artifact is not on disk, the discovery artifact will be retrieved from the internet. The maintainers recommend keeping `static_discovery=True`, which is the default, in order to avoid reliability issues related to transient network errors. The library currently does not detect breaking changes of discovery artifacts, and as a result, user code is not guaranteed to continue to succeed or fail with different versions of `google-api-python-client`. Users can set `static_discovery=False` as workaround if there is an incompatible discovery artifact shipped with the library. In addition, users may roll back the version of `google-api-python-client` if there is a breaking change in a discovery artifact. The maintainers of `google-api-python-client` encourage users to report issues with discovery artifacts so that the discovery artifacts can be corrected. - -In the below example, the discovery artifact for `'drive'` version `'v3'` will only be retrieved from the internet if it is not available in the legacy cache and it is not one of the static discovery artifacts shipped with the library. - -```python -from googleapiclient.discovery import build - -with build('drive', 'v3', cache_discovery=True, static_discovery=True): - # ... -``` +**Note**: Under the hood, the `build()` function retrieves a discovery artifact in order to construct the service object. If the `cache_discovery` argument of `build()` is set to `True`, the library will attempt to retrieve the discovery artifact from the legacy cache which is only supported with `oauth2client<4.0`. If the artifact is not available in the legacy cache and the `static_discovery` argument of `build()` is set to `True`, the library will attempt to retieve the discovery artifact from the static copy of the discovery artifacts that are shipped with the library. The maintainers recommend keeping `static_discovery=True`, which is the default, in order to avoid reliability issues related to transient network errors. The library currently does not detect breaking changes of discovery artifacts, and as a result, user code is not guaranteed to continue to succeed or fail with different versions of `google-api-python-client`. Users can set `static_discovery=False` as workaround if there is an incompatible discovery artifact shipped with the library. In addition, users may roll back the version of `google-api-python-client` if there is a breaking change in a discovery artifact. The maintainers of `google-api-python-client` encourage users to report issues with discovery artifacts so that the discovery artifacts can be corrected. ### Collections diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py index ee75ca46ba8..44473dc3cbd 100644 --- a/googleapiclient/discovery.py +++ b/googleapiclient/discovery.py @@ -248,8 +248,7 @@ def build( num_retries: Integer, number of times to retry discovery with randomized exponential backoff in case of intermittent/connection issues. static_discovery: Boolean, whether or not to use the static discovery docs - included in the package when the discovery doc is not available in the - cache. + included in the library. Returns: A Resource object with methods for interacting with the service. @@ -363,16 +362,13 @@ def _retrieve_discovery_doc( num_retries: Integer, number of times to retry discovery with randomized exponential backoff in case of intermittent/connection issues. static_discovery: Boolean, whether or not to use the static discovery docs - included in the package when the discovery doc is not available in the - cache. + included in the library. Returns: A unicode string representation of the discovery document. """ from . import discovery_cache - content = None - if cache_discovery: if cache is None: cache = discovery_cache.autodetect() @@ -381,31 +377,30 @@ def _retrieve_discovery_doc( if content: return content - # At this point, the discovery document was not found in the cache so - # we can attempt to retreive the static discovery document from the library. + # When `static_discovery=True`, use static discovery artifacts included + # with the library if static_discovery: content = discovery_cache.get_static_doc(serviceName, version) if content: return content - - # If the content is None, retrieve the discovery doc from the internet - # because it is not in the cache or the static doc directory. - if content is None: - actual_url = url - # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment - # variable that contains the network address of the client sending the - # request. If it exists then add that to the request for the discovery - # document to avoid exceeding the quota on discovery requests. - if "REMOTE_ADDR" in os.environ: - actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"]) - if developerKey: - actual_url = _add_query_parameter(url, "key", developerKey) - logger.debug("URL being requested: GET %s", actual_url) - - # Execute this request with retries build into HttpRequest - # Note that it will already raise an error if we don't get a 2xx response - req = HttpRequest(http, HttpRequest.null_postproc, actual_url) - resp, content = req.execute(num_retries=num_retries) + else: + raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version)) + + actual_url = url + # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment + # variable that contains the network address of the client sending the + # request. If it exists then add that to the request for the discovery + # document to avoid exceeding the quota on discovery requests. + if "REMOTE_ADDR" in os.environ: + actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"]) + if developerKey: + actual_url = _add_query_parameter(url, "key", developerKey) + logger.debug("URL being requested: GET %s", actual_url) + + # Execute this request with retries build into HttpRequest + # Note that it will already raise an error if we don't get a 2xx response + req = HttpRequest(http, HttpRequest.null_postproc, actual_url) + resp, content = req.execute(num_retries=num_retries) try: content = content.decode("utf-8") diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 227b6a030c5..49bb51a0a29 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -1162,10 +1162,9 @@ def test_retrieve_from_internet_when_static_discovery_false(self): build("drive", "v3", http=http, cache_discovery=False, static_discovery=False) - def test_retrieve_from_internet_when_static_doc_does_not_exist(self): - http = HttpMockSequence([({"status": "400"}, "")]) - with self.assertRaises(HttpError): - build("doesnotexist", "v3", http=http, cache_discovery=False, + def test_unknown_api_when_static_discovery_true(self): + with self.assertRaises(UnknownApiNameOrVersion): + build("doesnotexist", "v3", cache_discovery=False, static_discovery=True) From cc2461d2f5294e180eb61dff3a22f543752a759e Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 18 Dec 2020 03:54:46 +0000 Subject: [PATCH 12/15] Fix unit tests --- tests/test_discovery.py | 115 +++++++++++++++++++++------------------- tests/test_http.py | 2 +- tests/test_mocks.py | 18 +++---- 3 files changed, 71 insertions(+), 64 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 49bb51a0a29..1a57ad3a2b8 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -447,7 +447,7 @@ def test_discovery_http_is_closed(self): class DiscoveryErrors(unittest.TestCase): def test_tests_should_be_run_with_strict_positional_enforcement(self): try: - plus = build("plus", "v1", None) + plus = build("plus", "v1", None, static_discovery=False) self.fail("should have raised a TypeError exception over missing http=.") except TypeError: pass @@ -455,7 +455,7 @@ def test_tests_should_be_run_with_strict_positional_enforcement(self): def test_failed_to_parse_discovery_json(self): self.http = HttpMock(datafile("malformed.json"), {"status": "200"}) try: - plus = build("plus", "v1", http=self.http, cache_discovery=False) + plus = build("plus", "v1", http=self.http, cache_discovery=False, static_discovery=False) self.fail("should have raised an exception over malformed JSON.") except InvalidJsonError: pass @@ -473,7 +473,7 @@ def test_unknown_api_name_or_version(self): def test_credentials_and_http_mutually_exclusive(self): http = HttpMock(datafile("plus.json"), {"status": "200"}) with self.assertRaises(ValueError): - build("plus", "v1", http=http, credentials=mock.sentinel.credentials) + build("plus", "v1", http=http, credentials=mock.sentinel.credentials, static_discovery=False) def test_credentials_file_and_http_mutually_exclusive(self): http = HttpMock(datafile("plus.json"), {"status": "200"}) @@ -485,6 +485,7 @@ def test_credentials_file_and_http_mutually_exclusive(self): client_options=google.api_core.client_options.ClientOptions( credentials_file="credentials.json" ), + static_discovery=False, ) def test_credentials_and_credentials_file_mutually_exclusive(self): @@ -496,6 +497,7 @@ def test_credentials_and_credentials_file_mutually_exclusive(self): client_options=google.api_core.client_options.ClientOptions( credentials_file="credentials.json" ), + static_discovery=False, ) @@ -912,6 +914,7 @@ def test_userip_is_added_to_discovery_uri(self): http=http, developerKey=None, discoveryServiceUrl="http://example.com", + static_discovery=False, ) self.fail("Should have raised an exception.") except HttpError as e: @@ -930,6 +933,7 @@ def test_userip_missing_is_not_added_to_discovery_uri(self): http=http, developerKey=None, discoveryServiceUrl="http://example.com", + static_discovery=False, ) self.fail("Should have raised an exception.") except HttpError as e: @@ -948,6 +952,7 @@ def test_key_is_added_to_discovery_uri(self): http=http, developerKey="foo", discoveryServiceUrl="http://example.com", + static_discovery=False, ) self.fail("Should have raised an exception.") except HttpError as e: @@ -960,7 +965,7 @@ def test_discovery_loading_from_v2_discovery_uri(self): ({"status": "200"}, read_datafile("zoo.json", "rb")), ] ) - zoo = build("zoo", "v1", http=http, cache_discovery=False) + zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False) self.assertTrue(hasattr(zoo, "animals")) def test_api_endpoint_override_from_client_options(self): @@ -975,7 +980,7 @@ def test_api_endpoint_override_from_client_options(self): api_endpoint=api_endpoint ) zoo = build( - "zoo", "v1", http=http, cache_discovery=False, client_options=options + "zoo", "v1", http=http, cache_discovery=False, client_options=options, static_discovery=False ) self.assertEqual(zoo._baseUrl, api_endpoint) @@ -993,12 +998,13 @@ def test_api_endpoint_override_from_client_options_dict(self): http=http, cache_discovery=False, client_options={"api_endpoint": api_endpoint}, + static_discovery=False, ) self.assertEqual(zoo._baseUrl, api_endpoint) def test_discovery_with_empty_version_uses_v2(self): http = HttpMockSequence([({"status": "200"}, read_datafile("zoo.json", "rb")),]) - build("zoo", version=None, http=http, cache_discovery=False) + build("zoo", version=None, http=http, cache_discovery=False, static_discovery=False) validate_discovery_requests(self, http, "zoo", None, V2_DISCOVERY_URI) def test_discovery_with_empty_version_preserves_custom_uri(self): @@ -1010,12 +1016,13 @@ def test_discovery_with_empty_version_preserves_custom_uri(self): http=http, cache_discovery=False, discoveryServiceUrl=custom_discovery_uri, + static_discovery=False, ) validate_discovery_requests(self, http, "zoo", None, custom_discovery_uri) def test_discovery_with_valid_version_uses_v1(self): http = HttpMockSequence([({"status": "200"}, read_datafile("zoo.json", "rb")),]) - build("zoo", version="v123", http=http, cache_discovery=False) + build("zoo", version="v123", http=http, cache_discovery=False, static_discovery=False) validate_discovery_requests(self, http, "zoo", "v123", V1_DISCOVERY_URI) @@ -1029,7 +1036,7 @@ def test_repeated_500_retries_and_fails(self): ) with self.assertRaises(HttpError): with mock.patch("time.sleep") as mocked_sleep: - build("zoo", "v1", http=http, cache_discovery=False) + build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False) mocked_sleep.assert_called_once() # We also want to verify that we stayed with v1 discovery @@ -1045,7 +1052,7 @@ def test_v2_repeated_500_retries_and_fails(self): ) with self.assertRaises(HttpError): with mock.patch("time.sleep") as mocked_sleep: - build("zoo", "v1", http=http, cache_discovery=False) + build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False) mocked_sleep.assert_called_once() # We also want to verify that we switched to v2 discovery @@ -1059,7 +1066,7 @@ def test_single_500_retries_and_succeeds(self): ] ) with mock.patch("time.sleep") as mocked_sleep: - zoo = build("zoo", "v1", http=http, cache_discovery=False) + zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False) self.assertTrue(hasattr(zoo, "animals")) mocked_sleep.assert_called_once() @@ -1075,7 +1082,7 @@ def test_single_500_then_404_retries_and_succeeds(self): ] ) with mock.patch("time.sleep") as mocked_sleep: - zoo = build("zoo", "v1", http=http, cache_discovery=False) + zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False) self.assertTrue(hasattr(zoo, "animals")) mocked_sleep.assert_called_once() @@ -1111,7 +1118,7 @@ def import_mock(name, *args, **kwargs): self.mocked_api.memcache.get.return_value = None - plus = build("plus", "v1", http=self.http) + plus = build("plus", "v1", http=self.http, static_discovery=False) # memcache.get is called once url = "https://www.googleapis.com/discovery/v1/apis/plus/v1/rest" @@ -1132,7 +1139,7 @@ def import_mock(name, *args, **kwargs): # (Otherwise it should through an error) self.http = HttpMock(None, {"status": "200"}) - plus = build("plus", "v1", http=self.http) + plus = build("plus", "v1", http=self.http, static_discovery=False) # memcache.get is called twice self.mocked_api.memcache.get.assert_has_calls( @@ -1190,7 +1197,7 @@ def test_file_based_cache(self): ): self.http = HttpMock(datafile("plus.json"), {"status": "200"}) - plus = build("plus", "v1", http=self.http) + plus = build("plus", "v1", http=self.http, static_discovery=False) # cache.get is called once url = "https://www.googleapis.com/discovery/v1/apis/plus/v1/rest" @@ -1207,7 +1214,7 @@ def test_file_based_cache(self): # (Otherwise it should through an error) self.http = HttpMock(None, {"status": "200"}) - plus = build("plus", "v1", http=self.http) + plus = build("plus", "v1", http=self.http, static_discovery=False) # cache.get is called twice cache.get.assert_has_calls([mock.call(url), mock.call(url)]) @@ -1219,7 +1226,7 @@ def test_file_based_cache(self): class Discovery(unittest.TestCase): def test_method_error_checking(self): self.http = HttpMock(datafile("plus.json"), {"status": "200"}) - plus = build("plus", "v1", http=self.http) + plus = build("plus", "v1", http=self.http, static_discovery=False) # Missing required parameters try: @@ -1262,7 +1269,7 @@ def _check_query_types(self, request): def test_type_coercion(self): http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=http) + zoo = build("zoo", "v1", http=http, static_discovery=False) request = zoo.query( q="foo", i=1.0, n=1.0, b=0, a=[1, 2, 3], o={"a": 1}, e="bar" @@ -1295,7 +1302,7 @@ def test_type_coercion(self): def test_optional_stack_query_parameters(self): http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=http) + zoo = build("zoo", "v1", http=http, static_discovery=False) request = zoo.query(trace="html", fields="description") parsed = urlparse(request.uri) @@ -1305,7 +1312,7 @@ def test_optional_stack_query_parameters(self): def test_string_params_value_of_none_get_dropped(self): http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=http) + zoo = build("zoo", "v1", http=http, static_discovery=False) request = zoo.query(trace=None, fields="description") parsed = urlparse(request.uri) @@ -1314,7 +1321,7 @@ def test_string_params_value_of_none_get_dropped(self): def test_model_added_query_parameters(self): http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=http) + zoo = build("zoo", "v1", http=http, static_discovery=False) request = zoo.animals().get(name="Lion") parsed = urlparse(request.uri) @@ -1324,7 +1331,7 @@ def test_model_added_query_parameters(self): def test_fallback_to_raw_model(self): http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=http) + zoo = build("zoo", "v1", http=http, static_discovery=False) request = zoo.animals().getmedia(name="Lion") parsed = urlparse(request.uri) @@ -1334,7 +1341,7 @@ def test_fallback_to_raw_model(self): def test_patch(self): http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=http) + zoo = build("zoo", "v1", http=http, static_discovery=False) request = zoo.animals().patch(name="lion", body='{"description": "foo"}') self.assertEqual(request.method, "PATCH") @@ -1342,7 +1349,7 @@ def test_patch(self): def test_batch_request_from_discovery(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) # zoo defines a batchPath - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) batch_request = zoo.new_batch_http_request() self.assertEqual( batch_request._batch_uri, "https://www.googleapis.com/batchZoo" @@ -1351,7 +1358,7 @@ def test_batch_request_from_discovery(self): def test_batch_request_from_default(self): self.http = HttpMock(datafile("plus.json"), {"status": "200"}) # plus does not define a batchPath - plus = build("plus", "v1", http=self.http, cache_discovery=False) + plus = build("plus", "v1", http=self.http, cache_discovery=False, static_discovery=False) batch_request = plus.new_batch_http_request() self.assertEqual(batch_request._batch_uri, "https://www.googleapis.com/batch") @@ -1363,14 +1370,14 @@ def test_tunnel_patch(self): ] ) http = tunnel_patch(http) - zoo = build("zoo", "v1", http=http, cache_discovery=False) + zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False) resp = zoo.animals().patch(name="lion", body='{"description": "foo"}').execute() self.assertTrue("x-http-method-override" in resp) def test_plus_resources(self): self.http = HttpMock(datafile("plus.json"), {"status": "200"}) - plus = build("plus", "v1", http=self.http) + plus = build("plus", "v1", http=self.http, static_discovery=False) self.assertTrue(getattr(plus, "activities")) self.assertTrue(getattr(plus, "people")) @@ -1401,7 +1408,7 @@ def test_full_featured(self): # Zoo should exercise all discovery facets # and should also have no future.json file. self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) self.assertTrue(getattr(zoo, "animals")) request = zoo.animals().list(name="bat", projection="full") @@ -1412,7 +1419,7 @@ def test_full_featured(self): def test_nested_resources(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) self.assertTrue(getattr(zoo, "animals")) request = zoo.my().favorites().list(max_results="5") parsed = urlparse(request.uri) @@ -1430,7 +1437,7 @@ def test_methods_with_reserved_names(self): def test_top_level_functions(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) self.assertTrue(getattr(zoo, "query")) request = zoo.query(q="foo") parsed = urlparse(request.uri) @@ -1439,20 +1446,20 @@ def test_top_level_functions(self): def test_simple_media_uploads(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) doc = getattr(zoo.animals().insert, "__doc__") self.assertTrue("media_body" in doc) def test_simple_media_upload_no_max_size_provided(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) request = zoo.animals().crossbreed(media_body=datafile("small.png")) self.assertEqual("image/png", request.headers["content-type"]) self.assertEqual(b"PNG", request.body[1:4]) def test_simple_media_raise_correct_exceptions(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) try: zoo.animals().insert(media_body=datafile("smiley.png")) @@ -1468,7 +1475,7 @@ def test_simple_media_raise_correct_exceptions(self): def test_simple_media_good_upload(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) request = zoo.animals().insert(media_body=datafile("small.png")) self.assertEqual("image/png", request.headers["content-type"]) @@ -1481,7 +1488,7 @@ def test_simple_media_good_upload(self): def test_simple_media_unknown_mimetype(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) try: zoo.animals().insert(media_body=datafile("small-png")) @@ -1502,7 +1509,7 @@ def test_simple_media_unknown_mimetype(self): def test_multipart_media_raise_correct_exceptions(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) try: zoo.animals().insert(media_body=datafile("smiley.png"), body={}) @@ -1516,9 +1523,9 @@ def test_multipart_media_raise_correct_exceptions(self): except UnacceptableMimeTypeError: pass - def test_multipart_media_good_upload(self): + def test_multipart_media_good_upload(self, static_discovery=False): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) request = zoo.animals().insert(media_body=datafile("small.png"), body={}) self.assertTrue(request.headers["content-type"].startswith("multipart/related")) @@ -1551,14 +1558,14 @@ def test_multipart_media_good_upload(self): def test_media_capable_method_without_media(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) request = zoo.animals().insert(body={}) self.assertTrue(request.headers["content-type"], "application/json") def test_resumable_multipart_media_good_upload(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) media_upload = MediaFileUpload(datafile("small.png"), resumable=True) request = zoo.animals().insert(media_body=media_upload, body={}) @@ -1626,7 +1633,7 @@ def test_resumable_multipart_media_good_upload(self): def test_resumable_media_good_upload(self): """Not a multipart upload.""" self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) media_upload = MediaFileUpload(datafile("small.png"), resumable=True) request = zoo.animals().insert(media_body=media_upload, body=None) @@ -1685,7 +1692,7 @@ def test_resumable_media_good_upload(self): def test_resumable_media_good_upload_from_execute(self): """Not a multipart upload.""" self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) media_upload = MediaFileUpload(datafile("small.png"), resumable=True) request = zoo.animals().insert(media_body=media_upload, body=None) @@ -1724,7 +1731,7 @@ def test_resumable_media_good_upload_from_execute(self): def test_resumable_media_fail_unknown_response_code_first_request(self): """Not a multipart upload.""" self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) media_upload = MediaFileUpload(datafile("small.png"), resumable=True) request = zoo.animals().insert(media_body=media_upload, body=None) @@ -1742,7 +1749,7 @@ def test_resumable_media_fail_unknown_response_code_first_request(self): def test_resumable_media_fail_unknown_response_code_subsequent_request(self): """Not a multipart upload.""" self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) media_upload = MediaFileUpload(datafile("small.png"), resumable=True) request = zoo.animals().insert(media_body=media_upload, body=None) @@ -1784,7 +1791,7 @@ def test_resumable_media_fail_unknown_response_code_subsequent_request(self): def test_media_io_base_stream_unlimited_chunksize_resume(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) # Set up a seekable stream and try to upload in single chunk. fd = BytesIO(b'01234"56789"') @@ -1815,7 +1822,7 @@ def test_media_io_base_stream_unlimited_chunksize_resume(self): def test_media_io_base_stream_chunksize_resume(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) # Set up a seekable stream and try to upload in chunks. fd = BytesIO(b"0123456789") @@ -1847,7 +1854,7 @@ def test_resumable_media_handle_uploads_of_unknown_size(self): ) self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) # Create an upload that doesn't know the full size of the media. class IoBaseUnknownLength(MediaUpload): @@ -1874,7 +1881,7 @@ def getbytes(self, begin, length): def test_resumable_media_no_streaming_on_unsupported_platforms(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) class IoBaseHasStream(MediaUpload): def chunksize(self): @@ -1927,7 +1934,7 @@ def test_resumable_media_handle_uploads_of_unknown_size_eof(self): ) self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) fd = BytesIO(b"data goes here") @@ -1951,7 +1958,7 @@ def test_resumable_media_handle_resume_of_upload_of_unknown_size(self): ) self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) # Create an upload that doesn't know the full size of the media. fd = BytesIO(b"data goes here") @@ -2001,7 +2008,7 @@ def test_pickle(self): ] http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=http) + zoo = build("zoo", "v1", http=http, static_discovery=False) self.assertEqual(sorted(zoo.__dict__.keys()), sorted_resource_keys) pickled_zoo = pickle.dumps(zoo) @@ -2074,7 +2081,7 @@ def test_pickle_with_credentials(self): http = credentials.authorize(http) self.assertTrue(hasattr(http.request, "credentials")) - zoo = build("zoo", "v1", http=http) + zoo = build("zoo", "v1", http=http, static_discovery=False) pickled_zoo = pickle.dumps(zoo) new_zoo = pickle.loads(pickled_zoo) self.assertEqual(sorted(zoo.__dict__.keys()), sorted(new_zoo.__dict__.keys())) @@ -2083,7 +2090,7 @@ def test_pickle_with_credentials(self): def test_resumable_media_upload_no_content(self): self.http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=self.http) + zoo = build("zoo", "v1", http=self.http, static_discovery=False) media_upload = MediaFileUpload(datafile("empty"), resumable=True) request = zoo.animals().insert(media_body=media_upload, body=None) @@ -2154,7 +2161,7 @@ def test_next_successful_with_next_page_token_in_body(self): def test_next_with_method_with_no_properties(self): self.http = HttpMock(datafile("latitude.json"), {"status": "200"}) - service = build("latitude", "v1", http=self.http) + service = build("latitude", "v1", http=self.http, static_discovery=False) service.currentLocation().get() def test_next_nonexistent_with_no_next_page_token(self): @@ -2176,7 +2183,7 @@ def test_next_successful_with_next_page_token_required(self): class MediaGet(unittest.TestCase): def test_get_media(self): http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=http) + zoo = build("zoo", "v1", http=http, static_discovery=False) request = zoo.animals().get_media(name="Lion") parsed = urlparse(request.uri) diff --git a/tests/test_http.py b/tests/test_http.py index 6292dc1bcbb..700a48512c6 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -477,7 +477,7 @@ def test_media_io_base_empty_file(self): class TestMediaIoBaseDownload(unittest.TestCase): def setUp(self): http = HttpMock(datafile("zoo.json"), {"status": "200"}) - zoo = build("zoo", "v1", http=http) + zoo = build("zoo", "v1", http=http, static_discovery=False) self.request = zoo.animals().get_media(name="Lion") self.fd = BytesIO() diff --git a/tests/test_mocks.py b/tests/test_mocks.py index f020f6b3c7d..d0a0ff04e06 100644 --- a/tests/test_mocks.py +++ b/tests/test_mocks.py @@ -48,7 +48,7 @@ def setUp(self): def test_default_response(self): requestBuilder = RequestMockBuilder({}) - plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder) + plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder, static_discovery=False) activity = plus.activities().get(activityId="tag:blah").execute() self.assertEqual({}, activity) @@ -56,7 +56,7 @@ def test_simple_response(self): requestBuilder = RequestMockBuilder( {"plus.activities.get": (None, '{"foo": "bar"}')} ) - plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder) + plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder, static_discovery=False) activity = plus.activities().get(activityId="tag:blah").execute() self.assertEqual({"foo": "bar"}, activity) @@ -64,7 +64,7 @@ def test_simple_response(self): def test_unexpected_call(self): requestBuilder = RequestMockBuilder({}, check_unexpected=True) - plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder) + plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder, static_discovery=False) try: plus.activities().get(activityId="tag:blah").execute() @@ -76,7 +76,7 @@ def test_simple_unexpected_body(self): requestBuilder = RequestMockBuilder( {"zoo.animals.insert": (None, '{"data": {"foo": "bar"}}', None)} ) - zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder) + zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder, static_discovery=False) try: zoo.animals().insert(body="{}").execute() @@ -88,7 +88,7 @@ def test_simple_expected_body(self): requestBuilder = RequestMockBuilder( {"zoo.animals.insert": (None, '{"data": {"foo": "bar"}}', "{}")} ) - zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder) + zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder, static_discovery=False) try: zoo.animals().insert(body="").execute() @@ -106,7 +106,7 @@ def test_simple_wrong_body(self): ) } ) - zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder) + zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder, static_discovery=False) try: zoo.animals().insert(body='{"data": {"foo": "blah"}}').execute() @@ -124,7 +124,7 @@ def test_simple_matching_str_body(self): ) } ) - zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder) + zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder, static_discovery=False) activity = zoo.animals().insert(body={"data": {"foo": "bar"}}).execute() self.assertEqual({"foo": "bar"}, activity) @@ -139,7 +139,7 @@ def test_simple_matching_dict_body(self): ) } ) - zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder) + zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder, static_discovery=False) activity = zoo.animals().insert(body={"data": {"foo": "bar"}}).execute() self.assertEqual({"foo": "bar"}, activity) @@ -149,7 +149,7 @@ def test_errors(self): requestBuilder = RequestMockBuilder( {"plus.activities.list": (errorResponse, b"{}")} ) - plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder) + plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder, static_discovery=False) try: activity = ( From b6b6dbb2e7fac579eed741af4c834641c1e16705 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 14 Jan 2021 01:53:46 +0000 Subject: [PATCH 13/15] Auto generated docs should use static artifacts --- describe.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/describe.py b/describe.py index cb72f616b39..644596f87ce 100755 --- a/describe.py +++ b/describe.py @@ -37,6 +37,7 @@ from googleapiclient.discovery import build from googleapiclient.discovery import build_from_document from googleapiclient.discovery import UnknownApiNameOrVersion +from googleapiclient.discovery_cache import get_static_doc from googleapiclient.http import build_http from googleapiclient.errors import HttpError @@ -395,16 +396,7 @@ def document_api(name, version, uri): """ try: service = build(name, version) - - http = build_http() - response, content = http.request( - uri or uritemplate.expand( - FLAGS.discovery_uri_template, {"api": name, "apiVersion": version} - ) - ) - if 'status' in response: - if response['status'] == '404': - raise UnknownApiNameOrVersion + content = googleapiclient.discovery_cache.get_static_doc(name, version) except UnknownApiNameOrVersion as e: print("Warning: {} {} found but could not be built.".format(name, version)) return From 770ab9f87efc8a99d2c8b5e7c4b15dd74fbc7a58 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 14 Jan 2021 02:24:19 +0000 Subject: [PATCH 14/15] Improve note in start.md for clarity --- docs/start.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/start.md b/docs/start.md index 3d9cb244469..def0c4a481a 100644 --- a/docs/start.md +++ b/docs/start.md @@ -65,7 +65,7 @@ with build('drive', 'v3') as service: # ... ``` -**Note**: Under the hood, the `build()` function retrieves a discovery artifact in order to construct the service object. If the `cache_discovery` argument of `build()` is set to `True`, the library will attempt to retrieve the discovery artifact from the legacy cache which is only supported with `oauth2client<4.0`. If the artifact is not available in the legacy cache and the `static_discovery` argument of `build()` is set to `True`, the library will attempt to retieve the discovery artifact from the static copy of the discovery artifacts that are shipped with the library. The maintainers recommend keeping `static_discovery=True`, which is the default, in order to avoid reliability issues related to transient network errors. The library currently does not detect breaking changes of discovery artifacts, and as a result, user code is not guaranteed to continue to succeed or fail with different versions of `google-api-python-client`. Users can set `static_discovery=False` as workaround if there is an incompatible discovery artifact shipped with the library. In addition, users may roll back the version of `google-api-python-client` if there is a breaking change in a discovery artifact. The maintainers of `google-api-python-client` encourage users to report issues with discovery artifacts so that the discovery artifacts can be corrected. +**Note**: Under the hood, the `build()` function retrieves a discovery artifact in order to construct the service object. If the `cache_discovery` argument of `build()` is set to `True`, the library will attempt to retrieve the discovery artifact from the legacy cache which is only supported with `oauth2client<4.0`. If the artifact is not available in the legacy cache and the `static_discovery` argument of `build()` is set to `True`, which is the default, the library will use the service definition shipped in the library. If always using the latest version of a service definition is more important than reliability, users should set `static_discovery=False` to retrieve the service definition from the internet. ### Collections From 3fa216060568f16a8272d213220082b5f2155ef8 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 14 Jan 2021 02:31:56 +0000 Subject: [PATCH 15/15] Fix build --- describe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/describe.py b/describe.py index 644596f87ce..e53724e0494 100755 --- a/describe.py +++ b/describe.py @@ -396,7 +396,7 @@ def document_api(name, version, uri): """ try: service = build(name, version) - content = googleapiclient.discovery_cache.get_static_doc(name, version) + content = get_static_doc(name, version) except UnknownApiNameOrVersion as e: print("Warning: {} {} found but could not be built.".format(name, version)) return