Repository: googleapis/google-api-python-client

Issue:
Convert to a static/packaged library
The Python discovery client library `google-api-python-client` currently downloads the discovery doc for a service at runtime and constructs a client. Convert the library to a static/packaged library. See the [Node client](https://github.com/googleapis/google-api-nodejs-client/tree/master/src) for an example of this.

The Python client has a [discovery_cache](https://github.com/googleapis/google-api-python-client/tree/master/googleapiclient/discovery_cache) that is only usable with the deprecated auth client (oauth2client) or App Engine Standard's memcache.  One possible solution is adjusting this cache to reference downloaded json instead.


Discussion:

Resolving patch:
diff --git a/describe.py b/describe.py
--- 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,6 +396,7 @@ def document_api(name, version, uri):
   """
     try:
         service = build(name, version)
+        content = get_static_doc(name, version)
     except UnknownApiNameOrVersion as e:
         print("Warning: {} {} found but could not be built.".format(name, version))
         return
@@ -402,12 +404,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)
diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py
--- 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,8 @@ 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 library.
 
   Returns:
     A Resource object with methods for interacting with the service.
@@ -271,9 +274,12 @@ def build(
                 requested_url,
                 discovery_http,
                 cache_discovery,
+                serviceName,
+                version,
                 cache,
                 developerKey,
                 num_retries=num_retries,
+                static_discovery=static_discovery,
             )
             service = build_from_document(
                 content,
@@ -330,7 +336,15 @@ 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,
+    serviceName,
+    version,
+    cache=None,
+    developerKey=None,
+    num_retries=1,
+    static_discovery=True
 ):
     """Retrieves the discovery_doc from cache or the internet.
 
@@ -339,19 +353,23 @@ 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
       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 library.
 
   Returns:
     A unicode string representation of the discovery document.
   """
-    if cache_discovery:
-        from . import discovery_cache
+    from . import discovery_cache
 
+    if cache_discovery:
         if cache is None:
             cache = discovery_cache.autodetect()
         if cache:
@@ -359,6 +377,15 @@ def _retrieve_discovery_doc(
             if content:
                 return content
 
+    # 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
+        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
diff --git a/googleapiclient/discovery_cache/__init__.py b/googleapiclient/discovery_cache/__init__.py
--- 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,29 @@ def autodetect():
         LOGGER.info("file_cache is only supported with oauth2client<4.0.0",
             exc_info=False)
         return None
+
+def get_static_doc(serviceName, version):
+    """Retrieves the discovery document from the directory defined in
+    DISCOVERY_DOC_DIR corresponding to the serviceName and version provided.
+
+    Args:
+        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.
+    """
+
+    content = None
+    doc_name = "{}.{}.json".format(serviceName, version)
+
+    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
+


Test patch:
diff --git a/tests/test_discovery.py b/tests/test_discovery.py
--- 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(
@@ -1148,6 +1155,26 @@ def import_mock(name, *args, **kwargs):
             )
 
 
+class DiscoveryFromStaticDocument(unittest.TestCase):
+    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_retrieve_from_internet_when_static_discovery_false(self):
+        http = HttpMockSequence([({"status": "400"}, "")])
+        with self.assertRaises(HttpError):
+            build("drive", "v3", http=http, cache_discovery=False,
+                      static_discovery=False)
+
+    def test_unknown_api_when_static_discovery_true(self):
+        with self.assertRaises(UnknownApiNameOrVersion):
+            build("doesnotexist", "v3", cache_discovery=False,
+                      static_discovery=True)
+
+
 class DictCache(Cache):
     def __init__(self):
         self.d = {}
@@ -1170,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"
@@ -1187,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)])
@@ -1199,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:
@@ -1242,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"
@@ -1275,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)
@@ -1285,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)
@@ -1294,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)
@@ -1304,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)
@@ -1314,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")
@@ -1322,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"
@@ -1331,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")
 
@@ -1343,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"))
 
@@ -1381,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")
@@ -1392,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)
@@ -1410,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)
@@ -1419,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"))
@@ -1448,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"])
@@ -1461,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"))
@@ -1482,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={})
@@ -1496,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"))
@@ -1531,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={})
@@ -1606,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)
@@ -1665,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)
@@ -1704,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)
@@ -1722,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)
@@ -1764,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"')
@@ -1795,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")
@@ -1827,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):
@@ -1854,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):
@@ -1907,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")
 
@@ -1931,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")
@@ -1981,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)
@@ -2054,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()))
@@ -2063,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)
@@ -2134,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):
@@ -2156,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
--- 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
--- 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 = (