Skip to content
New issue

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

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

Already on GitHub? # to your account

feat: add client_options support for api endpoint override #829

Merged
merged 6 commits into from
Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion googleapiclient/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
# Third-party imports
import httplib2
import uritemplate
import google.api_core.client_options

# Local imports
from googleapiclient import _auth
Expand Down Expand Up @@ -176,6 +177,7 @@ def build(
credentials=None,
cache_discovery=True,
cache=None,
client_options=None,
):
"""Construct a Resource for interacting with an API.

Expand All @@ -202,6 +204,8 @@ def build(
cache_discovery: Boolean, whether or not to cache the discovery doc.
cache: googleapiclient.discovery_cache.base.CacheBase, an optional
cache object for the discovery documents.
client_options: Dictionary or google.api_core.client_options, Client options to set user
options on the client. API endpoint should be set through client_options.

Returns:
A Resource object with methods for interacting with the service.
Expand All @@ -228,6 +232,7 @@ def build(
model=model,
requestBuilder=requestBuilder,
credentials=credentials,
client_options=client_options
)
except HttpError as e:
if e.resp.status == http_client.NOT_FOUND:
Expand Down Expand Up @@ -304,6 +309,7 @@ def build_from_document(
model=None,
requestBuilder=HttpRequest,
credentials=None,
client_options=None
):
"""Create a Resource for interacting with an API.

Expand All @@ -328,6 +334,8 @@ def build_from_document(
credentials: oauth2client.Credentials or
google.auth.credentials.Credentials, credentials to be used for
authentication.
client_options: Dictionary or google.api_core.client_options, Client options to set user
options on the client. API endpoint should be set through client_options.

Returns:
A Resource object with methods for interacting with the service.
Expand All @@ -350,7 +358,16 @@ def build_from_document(
)
raise InvalidJsonError()

base = urljoin(service["rootUrl"], service["servicePath"])
# If an API Endpoint is provided on client options, use that as the base URL
base = urljoin(service['rootUrl'], service["servicePath"])
if client_options:
if type(client_options) == dict:
client_options = google.api_core.client_options.from_dict(
client_options
)
if client_options.api_endpoint:
base = client_options.api_endpoint

schema = Schemas(service)

# If the http client is not specified, then we must construct an http client
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"httplib2>=0.17.0,<1dev",
"google-auth>=1.4.1",
"google-auth-httplib2>=0.0.3",
"google-api-core>=1.13.0,<2dev",
"six>=1.6.1,<2dev",
"uritemplate>=3.0.0,<4dev",
]
Expand Down
144 changes: 98 additions & 46 deletions tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ def test_building_with_base_remembers_base(self):
plus = build_from_document(
discovery, base=base, credentials=self.MOCK_CREDENTIALS
)
self.assertEquals("https://www.googleapis.com/plus/v1/", plus._baseUrl)
self.assertEqual("https://www.googleapis.com/plus/v1/", plus._baseUrl)

def test_building_with_optional_http_with_authorization(self):
discovery = open(datafile("plus.json")).read()
Expand Down Expand Up @@ -503,7 +503,7 @@ def test_building_with_explicit_http(self):
plus = build_from_document(
discovery, base="https://www.googleapis.com/", http=http
)
self.assertEquals(plus._http, http)
self.assertEqual(plus._http, http)

def test_building_with_developer_key_skips_adc(self):
discovery = open(datafile("plus.json")).read()
Expand All @@ -515,6 +515,25 @@ def test_building_with_developer_key_skips_adc(self):
# application default credentials were used.
self.assertNotIsInstance(plus._http, google_auth_httplib2.AuthorizedHttp)

def test_api_endpoint_override_from_client_options(self):
discovery = open(datafile("plus.json")).read()
api_endpoint = "https://foo.googleapis.com/"
options = google.api_core.client_options.ClientOptions(
api_endpoint=api_endpoint
)
plus = build_from_document(discovery, client_options=options)

self.assertEqual(plus._baseUrl, api_endpoint)

def test_api_endpoint_override_from_client_options_dict(self):
discovery = open(datafile("plus.json")).read()
api_endpoint = "https://foo.googleapis.com/"
plus = build_from_document(
discovery, client_options={"api_endpoint": api_endpoint}
)

self.assertEqual(plus._baseUrl, api_endpoint)


class DiscoveryFromHttp(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -588,6 +607,39 @@ def test_discovery_loading_from_v2_discovery_uri(self):
zoo = build("zoo", "v1", http=http, cache_discovery=False)
self.assertTrue(hasattr(zoo, "animals"))

def test_api_endpoint_override_from_client_options(self):
http = HttpMockSequence(
[
({"status": "404"}, "Not found"),
({"status": "200"}, open(datafile("zoo.json"), "rb").read()),
]
)
api_endpoint = "https://foo.googleapis.com/"
options = google.api_core.client_options.ClientOptions(
api_endpoint=api_endpoint
)
zoo = build(
"zoo", "v1", http=http, cache_discovery=False, client_options=options
)
self.assertEqual(zoo._baseUrl, api_endpoint)

def test_api_endpoint_override_from_client_options_dict(self):
http = HttpMockSequence(
[
({"status": "404"}, "Not found"),
({"status": "200"}, open(datafile("zoo.json"), "rb").read()),
]
)
api_endpoint = "https://foo.googleapis.com/"
zoo = build(
"zoo",
"v1",
http=http,
cache_discovery=False,
client_options={"api_endpoint": api_endpoint},
)
self.assertEqual(zoo._baseUrl, api_endpoint)


class DiscoveryFromAppEngineCache(unittest.TestCase):
def test_appengine_memcache(self):
Expand Down Expand Up @@ -928,8 +980,8 @@ 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)
request = zoo.animals().crossbreed(media_body=datafile("small.png"))
self.assertEquals("image/png", request.headers["content-type"])
self.assertEquals(b"PNG", request.body[1:4])
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"})
Expand All @@ -952,8 +1004,8 @@ def test_simple_media_good_upload(self):
zoo = build("zoo", "v1", http=self.http)

request = zoo.animals().insert(media_body=datafile("small.png"))
self.assertEquals("image/png", request.headers["content-type"])
self.assertEquals(b"PNG", request.body[1:4])
self.assertEqual("image/png", request.headers["content-type"])
self.assertEqual(b"PNG", request.body[1:4])
assertUrisEqual(
self,
"https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json",
Expand All @@ -973,8 +1025,8 @@ def test_simple_media_unknown_mimetype(self):
request = zoo.animals().insert(
media_body=datafile("small-png"), media_mime_type="image/png"
)
self.assertEquals("image/png", request.headers["content-type"])
self.assertEquals(b"PNG", request.body[1:4])
self.assertEqual("image/png", request.headers["content-type"])
self.assertEqual(b"PNG", request.body[1:4])
assertUrisEqual(
self,
"https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json",
Expand Down Expand Up @@ -1045,13 +1097,13 @@ def test_resumable_multipart_media_good_upload(self):
media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
request = zoo.animals().insert(media_body=media_upload, body={})
self.assertTrue(request.headers["content-type"].startswith("application/json"))
self.assertEquals('{"data": {}}', request.body)
self.assertEquals(media_upload, request.resumable)
self.assertEqual('{"data": {}}', request.body)
self.assertEqual(media_upload, request.resumable)

self.assertEquals("image/png", request.resumable.mimetype())
self.assertEqual("image/png", request.resumable.mimetype())

self.assertNotEquals(request.body, None)
self.assertEquals(request.resumable_uri, None)
self.assertEqual(request.resumable_uri, None)

http = HttpMockSequence(
[
Expand All @@ -1078,32 +1130,32 @@ def test_resumable_multipart_media_good_upload(self):
)

status, body = request.next_chunk(http=http)
self.assertEquals(None, body)
self.assertEqual(None, body)
self.assertTrue(isinstance(status, MediaUploadProgress))
self.assertEquals(0, status.resumable_progress)
self.assertEqual(0, status.resumable_progress)

# Two requests should have been made and the resumable_uri should have been
# updated for each one.
self.assertEquals(request.resumable_uri, "http://upload.example.com/2")
self.assertEquals(media_upload, request.resumable)
self.assertEquals(0, request.resumable_progress)
self.assertEqual(request.resumable_uri, "http://upload.example.com/2")
self.assertEqual(media_upload, request.resumable)
self.assertEqual(0, request.resumable_progress)

# This next chuck call should upload the first chunk
status, body = request.next_chunk(http=http)
self.assertEquals(request.resumable_uri, "http://upload.example.com/3")
self.assertEquals(media_upload, request.resumable)
self.assertEquals(13, request.resumable_progress)
self.assertEqual(request.resumable_uri, "http://upload.example.com/3")
self.assertEqual(media_upload, request.resumable)
self.assertEqual(13, request.resumable_progress)

# This call will upload the next chunk
status, body = request.next_chunk(http=http)
self.assertEquals(request.resumable_uri, "http://upload.example.com/4")
self.assertEquals(media_upload.size() - 1, request.resumable_progress)
self.assertEquals('{"data": {}}', request.body)
self.assertEqual(request.resumable_uri, "http://upload.example.com/4")
self.assertEqual(media_upload.size() - 1, request.resumable_progress)
self.assertEqual('{"data": {}}', request.body)

# Final call to next_chunk should complete the upload.
status, body = request.next_chunk(http=http)
self.assertEquals(body, {"foo": "bar"})
self.assertEquals(status, None)
self.assertEqual(body, {"foo": "bar"})
self.assertEqual(status, None)

def test_resumable_media_good_upload(self):
"""Not a multipart upload."""
Expand All @@ -1112,12 +1164,12 @@ def test_resumable_media_good_upload(self):

media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
request = zoo.animals().insert(media_body=media_upload, body=None)
self.assertEquals(media_upload, request.resumable)
self.assertEqual(media_upload, request.resumable)

self.assertEquals("image/png", request.resumable.mimetype())
self.assertEqual("image/png", request.resumable.mimetype())

self.assertEquals(request.body, None)
self.assertEquals(request.resumable_uri, None)
self.assertEqual(request.body, None)
self.assertEqual(request.resumable_uri, None)

http = HttpMockSequence(
[
Expand All @@ -1143,26 +1195,26 @@ def test_resumable_media_good_upload(self):
)

status, body = request.next_chunk(http=http)
self.assertEquals(None, body)
self.assertEqual(None, body)
self.assertTrue(isinstance(status, MediaUploadProgress))
self.assertEquals(13, status.resumable_progress)
self.assertEqual(13, status.resumable_progress)

# Two requests should have been made and the resumable_uri should have been
# updated for each one.
self.assertEquals(request.resumable_uri, "http://upload.example.com/2")
self.assertEqual(request.resumable_uri, "http://upload.example.com/2")

self.assertEquals(media_upload, request.resumable)
self.assertEquals(13, request.resumable_progress)
self.assertEqual(media_upload, request.resumable)
self.assertEqual(13, request.resumable_progress)

status, body = request.next_chunk(http=http)
self.assertEquals(request.resumable_uri, "http://upload.example.com/3")
self.assertEquals(media_upload.size() - 1, request.resumable_progress)
self.assertEquals(request.body, None)
self.assertEqual(request.resumable_uri, "http://upload.example.com/3")
self.assertEqual(media_upload.size() - 1, request.resumable_progress)
self.assertEqual(request.body, None)

# Final call to next_chunk should complete the upload.
status, body = request.next_chunk(http=http)
self.assertEquals(body, {"foo": "bar"})
self.assertEquals(status, None)
self.assertEqual(body, {"foo": "bar"})
self.assertEqual(status, None)

def test_resumable_media_good_upload_from_execute(self):
"""Not a multipart upload."""
Expand Down Expand Up @@ -1201,7 +1253,7 @@ def test_resumable_media_good_upload_from_execute(self):
)

body = request.execute(http=http)
self.assertEquals(body, {"foo": "bar"})
self.assertEqual(body, {"foo": "bar"})

def test_resumable_media_fail_unknown_response_code_first_request(self):
"""Not a multipart upload."""
Expand Down Expand Up @@ -1247,7 +1299,7 @@ def test_resumable_media_fail_unknown_response_code_subsequent_request(self):
)

status, body = request.next_chunk(http=http)
self.assertEquals(
self.assertEqual(
status.resumable_progress,
7,
"Should have first checked length and then tried to PUT more.",
Expand Down Expand Up @@ -1571,9 +1623,9 @@ def test_resumable_media_upload_no_content(self):
media_upload = MediaFileUpload(datafile("empty"), resumable=True)
request = zoo.animals().insert(media_body=media_upload, body=None)

self.assertEquals(media_upload, request.resumable)
self.assertEquals(request.body, None)
self.assertEquals(request.resumable_uri, None)
self.assertEqual(media_upload, request.resumable)
self.assertEqual(request.body, None)
self.assertEqual(request.resumable_uri, None)

http = HttpMockSequence(
[
Expand All @@ -1590,9 +1642,9 @@ def test_resumable_media_upload_no_content(self):
)

status, body = request.next_chunk(http=http)
self.assertEquals(None, body)
self.assertEqual(None, body)
self.assertTrue(isinstance(status, MediaUploadProgress))
self.assertEquals(0, status.progress())
self.assertEqual(0, status.progress())


class Next(unittest.TestCase):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,7 @@ def setUp(self):

def test_id_to_from_content_id_header(self):
batch = BatchHttpRequest()
self.assertEquals("12", batch._header_to_id(batch._id_to_header("12")))
self.assertEqual("12", batch._header_to_id(batch._id_to_header("12")))

def test_invalid_content_id_header(self):
batch = BatchHttpRequest()
Expand Down Expand Up @@ -1646,7 +1646,7 @@ def test_build_http_default_timeout_can_be_overridden(self):
def test_build_http_default_timeout_can_be_set_to_zero(self):
socket.setdefaulttimeout(0)
http = build_http()
self.assertEquals(http.timeout, 0)
self.assertEqual(http.timeout, 0)

def test_build_http_default_308_is_excluded_as_redirect(self):
http = build_http()
Expand Down