Skip to content

Commit 1cf3cbc

Browse files
authored
feat: add client_options support for api endpoint override (#829)
* feat: add client_options support for api endpoint override * chore: replace all `assertEquals` with `assertEqual`
1 parent 814c282 commit 1cf3cbc

File tree

4 files changed

+119
-49
lines changed

4 files changed

+119
-49
lines changed

googleapiclient/discovery.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
# Third-party imports
4747
import httplib2
4848
import uritemplate
49+
import google.api_core.client_options
4950

5051
# Local imports
5152
from googleapiclient import _auth
@@ -176,6 +177,7 @@ def build(
176177
credentials=None,
177178
cache_discovery=True,
178179
cache=None,
180+
client_options=None,
179181
):
180182
"""Construct a Resource for interacting with an API.
181183
@@ -202,6 +204,8 @@ def build(
202204
cache_discovery: Boolean, whether or not to cache the discovery doc.
203205
cache: googleapiclient.discovery_cache.base.CacheBase, an optional
204206
cache object for the discovery documents.
207+
client_options: Dictionary or google.api_core.client_options, Client options to set user
208+
options on the client. API endpoint should be set through client_options.
205209
206210
Returns:
207211
A Resource object with methods for interacting with the service.
@@ -228,6 +232,7 @@ def build(
228232
model=model,
229233
requestBuilder=requestBuilder,
230234
credentials=credentials,
235+
client_options=client_options
231236
)
232237
except HttpError as e:
233238
if e.resp.status == http_client.NOT_FOUND:
@@ -304,6 +309,7 @@ def build_from_document(
304309
model=None,
305310
requestBuilder=HttpRequest,
306311
credentials=None,
312+
client_options=None
307313
):
308314
"""Create a Resource for interacting with an API.
309315
@@ -328,6 +334,8 @@ def build_from_document(
328334
credentials: oauth2client.Credentials or
329335
google.auth.credentials.Credentials, credentials to be used for
330336
authentication.
337+
client_options: Dictionary or google.api_core.client_options, Client options to set user
338+
options on the client. API endpoint should be set through client_options.
331339
332340
Returns:
333341
A Resource object with methods for interacting with the service.
@@ -350,7 +358,16 @@ def build_from_document(
350358
)
351359
raise InvalidJsonError()
352360

353-
base = urljoin(service["rootUrl"], service["servicePath"])
361+
# If an API Endpoint is provided on client options, use that as the base URL
362+
base = urljoin(service['rootUrl'], service["servicePath"])
363+
if client_options:
364+
if type(client_options) == dict:
365+
client_options = google.api_core.client_options.from_dict(
366+
client_options
367+
)
368+
if client_options.api_endpoint:
369+
base = client_options.api_endpoint
370+
354371
schema = Schemas(service)
355372

356373
# If the http client is not specified, then we must construct an http client

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"httplib2>=0.17.0,<1dev",
3737
"google-auth>=1.4.1",
3838
"google-auth-httplib2>=0.0.3",
39+
"google-api-core>=1.13.0,<2dev",
3940
"six>=1.6.1,<2dev",
4041
"uritemplate>=3.0.0,<4dev",
4142
]

tests/test_discovery.py

+98-46
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ def test_building_with_base_remembers_base(self):
466466
plus = build_from_document(
467467
discovery, base=base, credentials=self.MOCK_CREDENTIALS
468468
)
469-
self.assertEquals("https://www.googleapis.com/plus/v1/", plus._baseUrl)
469+
self.assertEqual("https://www.googleapis.com/plus/v1/", plus._baseUrl)
470470

471471
def test_building_with_optional_http_with_authorization(self):
472472
discovery = open(datafile("plus.json")).read()
@@ -503,7 +503,7 @@ def test_building_with_explicit_http(self):
503503
plus = build_from_document(
504504
discovery, base="https://www.googleapis.com/", http=http
505505
)
506-
self.assertEquals(plus._http, http)
506+
self.assertEqual(plus._http, http)
507507

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

518+
def test_api_endpoint_override_from_client_options(self):
519+
discovery = open(datafile("plus.json")).read()
520+
api_endpoint = "https://foo.googleapis.com/"
521+
options = google.api_core.client_options.ClientOptions(
522+
api_endpoint=api_endpoint
523+
)
524+
plus = build_from_document(discovery, client_options=options)
525+
526+
self.assertEqual(plus._baseUrl, api_endpoint)
527+
528+
def test_api_endpoint_override_from_client_options_dict(self):
529+
discovery = open(datafile("plus.json")).read()
530+
api_endpoint = "https://foo.googleapis.com/"
531+
plus = build_from_document(
532+
discovery, client_options={"api_endpoint": api_endpoint}
533+
)
534+
535+
self.assertEqual(plus._baseUrl, api_endpoint)
536+
518537

519538
class DiscoveryFromHttp(unittest.TestCase):
520539
def setUp(self):
@@ -588,6 +607,39 @@ def test_discovery_loading_from_v2_discovery_uri(self):
588607
zoo = build("zoo", "v1", http=http, cache_discovery=False)
589608
self.assertTrue(hasattr(zoo, "animals"))
590609

610+
def test_api_endpoint_override_from_client_options(self):
611+
http = HttpMockSequence(
612+
[
613+
({"status": "404"}, "Not found"),
614+
({"status": "200"}, open(datafile("zoo.json"), "rb").read()),
615+
]
616+
)
617+
api_endpoint = "https://foo.googleapis.com/"
618+
options = google.api_core.client_options.ClientOptions(
619+
api_endpoint=api_endpoint
620+
)
621+
zoo = build(
622+
"zoo", "v1", http=http, cache_discovery=False, client_options=options
623+
)
624+
self.assertEqual(zoo._baseUrl, api_endpoint)
625+
626+
def test_api_endpoint_override_from_client_options_dict(self):
627+
http = HttpMockSequence(
628+
[
629+
({"status": "404"}, "Not found"),
630+
({"status": "200"}, open(datafile("zoo.json"), "rb").read()),
631+
]
632+
)
633+
api_endpoint = "https://foo.googleapis.com/"
634+
zoo = build(
635+
"zoo",
636+
"v1",
637+
http=http,
638+
cache_discovery=False,
639+
client_options={"api_endpoint": api_endpoint},
640+
)
641+
self.assertEqual(zoo._baseUrl, api_endpoint)
642+
591643

592644
class DiscoveryFromAppEngineCache(unittest.TestCase):
593645
def test_appengine_memcache(self):
@@ -928,8 +980,8 @@ def test_simple_media_upload_no_max_size_provided(self):
928980
self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
929981
zoo = build("zoo", "v1", http=self.http)
930982
request = zoo.animals().crossbreed(media_body=datafile("small.png"))
931-
self.assertEquals("image/png", request.headers["content-type"])
932-
self.assertEquals(b"PNG", request.body[1:4])
983+
self.assertEqual("image/png", request.headers["content-type"])
984+
self.assertEqual(b"PNG", request.body[1:4])
933985

934986
def test_simple_media_raise_correct_exceptions(self):
935987
self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
@@ -952,8 +1004,8 @@ def test_simple_media_good_upload(self):
9521004
zoo = build("zoo", "v1", http=self.http)
9531005

9541006
request = zoo.animals().insert(media_body=datafile("small.png"))
955-
self.assertEquals("image/png", request.headers["content-type"])
956-
self.assertEquals(b"PNG", request.body[1:4])
1007+
self.assertEqual("image/png", request.headers["content-type"])
1008+
self.assertEqual(b"PNG", request.body[1:4])
9571009
assertUrisEqual(
9581010
self,
9591011
"https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json",
@@ -973,8 +1025,8 @@ def test_simple_media_unknown_mimetype(self):
9731025
request = zoo.animals().insert(
9741026
media_body=datafile("small-png"), media_mime_type="image/png"
9751027
)
976-
self.assertEquals("image/png", request.headers["content-type"])
977-
self.assertEquals(b"PNG", request.body[1:4])
1028+
self.assertEqual("image/png", request.headers["content-type"])
1029+
self.assertEqual(b"PNG", request.body[1:4])
9781030
assertUrisEqual(
9791031
self,
9801032
"https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json",
@@ -1045,13 +1097,13 @@ def test_resumable_multipart_media_good_upload(self):
10451097
media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
10461098
request = zoo.animals().insert(media_body=media_upload, body={})
10471099
self.assertTrue(request.headers["content-type"].startswith("application/json"))
1048-
self.assertEquals('{"data": {}}', request.body)
1049-
self.assertEquals(media_upload, request.resumable)
1100+
self.assertEqual('{"data": {}}', request.body)
1101+
self.assertEqual(media_upload, request.resumable)
10501102

1051-
self.assertEquals("image/png", request.resumable.mimetype())
1103+
self.assertEqual("image/png", request.resumable.mimetype())
10521104

10531105
self.assertNotEquals(request.body, None)
1054-
self.assertEquals(request.resumable_uri, None)
1106+
self.assertEqual(request.resumable_uri, None)
10551107

10561108
http = HttpMockSequence(
10571109
[
@@ -1078,32 +1130,32 @@ def test_resumable_multipart_media_good_upload(self):
10781130
)
10791131

10801132
status, body = request.next_chunk(http=http)
1081-
self.assertEquals(None, body)
1133+
self.assertEqual(None, body)
10821134
self.assertTrue(isinstance(status, MediaUploadProgress))
1083-
self.assertEquals(0, status.resumable_progress)
1135+
self.assertEqual(0, status.resumable_progress)
10841136

10851137
# Two requests should have been made and the resumable_uri should have been
10861138
# updated for each one.
1087-
self.assertEquals(request.resumable_uri, "http://upload.example.com/2")
1088-
self.assertEquals(media_upload, request.resumable)
1089-
self.assertEquals(0, request.resumable_progress)
1139+
self.assertEqual(request.resumable_uri, "http://upload.example.com/2")
1140+
self.assertEqual(media_upload, request.resumable)
1141+
self.assertEqual(0, request.resumable_progress)
10901142

10911143
# This next chuck call should upload the first chunk
10921144
status, body = request.next_chunk(http=http)
1093-
self.assertEquals(request.resumable_uri, "http://upload.example.com/3")
1094-
self.assertEquals(media_upload, request.resumable)
1095-
self.assertEquals(13, request.resumable_progress)
1145+
self.assertEqual(request.resumable_uri, "http://upload.example.com/3")
1146+
self.assertEqual(media_upload, request.resumable)
1147+
self.assertEqual(13, request.resumable_progress)
10961148

10971149
# This call will upload the next chunk
10981150
status, body = request.next_chunk(http=http)
1099-
self.assertEquals(request.resumable_uri, "http://upload.example.com/4")
1100-
self.assertEquals(media_upload.size() - 1, request.resumable_progress)
1101-
self.assertEquals('{"data": {}}', request.body)
1151+
self.assertEqual(request.resumable_uri, "http://upload.example.com/4")
1152+
self.assertEqual(media_upload.size() - 1, request.resumable_progress)
1153+
self.assertEqual('{"data": {}}', request.body)
11021154

11031155
# Final call to next_chunk should complete the upload.
11041156
status, body = request.next_chunk(http=http)
1105-
self.assertEquals(body, {"foo": "bar"})
1106-
self.assertEquals(status, None)
1157+
self.assertEqual(body, {"foo": "bar"})
1158+
self.assertEqual(status, None)
11071159

11081160
def test_resumable_media_good_upload(self):
11091161
"""Not a multipart upload."""
@@ -1112,12 +1164,12 @@ def test_resumable_media_good_upload(self):
11121164

11131165
media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
11141166
request = zoo.animals().insert(media_body=media_upload, body=None)
1115-
self.assertEquals(media_upload, request.resumable)
1167+
self.assertEqual(media_upload, request.resumable)
11161168

1117-
self.assertEquals("image/png", request.resumable.mimetype())
1169+
self.assertEqual("image/png", request.resumable.mimetype())
11181170

1119-
self.assertEquals(request.body, None)
1120-
self.assertEquals(request.resumable_uri, None)
1171+
self.assertEqual(request.body, None)
1172+
self.assertEqual(request.resumable_uri, None)
11211173

11221174
http = HttpMockSequence(
11231175
[
@@ -1143,26 +1195,26 @@ def test_resumable_media_good_upload(self):
11431195
)
11441196

11451197
status, body = request.next_chunk(http=http)
1146-
self.assertEquals(None, body)
1198+
self.assertEqual(None, body)
11471199
self.assertTrue(isinstance(status, MediaUploadProgress))
1148-
self.assertEquals(13, status.resumable_progress)
1200+
self.assertEqual(13, status.resumable_progress)
11491201

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

1154-
self.assertEquals(media_upload, request.resumable)
1155-
self.assertEquals(13, request.resumable_progress)
1206+
self.assertEqual(media_upload, request.resumable)
1207+
self.assertEqual(13, request.resumable_progress)
11561208

11571209
status, body = request.next_chunk(http=http)
1158-
self.assertEquals(request.resumable_uri, "http://upload.example.com/3")
1159-
self.assertEquals(media_upload.size() - 1, request.resumable_progress)
1160-
self.assertEquals(request.body, None)
1210+
self.assertEqual(request.resumable_uri, "http://upload.example.com/3")
1211+
self.assertEqual(media_upload.size() - 1, request.resumable_progress)
1212+
self.assertEqual(request.body, None)
11611213

11621214
# Final call to next_chunk should complete the upload.
11631215
status, body = request.next_chunk(http=http)
1164-
self.assertEquals(body, {"foo": "bar"})
1165-
self.assertEquals(status, None)
1216+
self.assertEqual(body, {"foo": "bar"})
1217+
self.assertEqual(status, None)
11661218

11671219
def test_resumable_media_good_upload_from_execute(self):
11681220
"""Not a multipart upload."""
@@ -1201,7 +1253,7 @@ def test_resumable_media_good_upload_from_execute(self):
12011253
)
12021254

12031255
body = request.execute(http=http)
1204-
self.assertEquals(body, {"foo": "bar"})
1256+
self.assertEqual(body, {"foo": "bar"})
12051257

12061258
def test_resumable_media_fail_unknown_response_code_first_request(self):
12071259
"""Not a multipart upload."""
@@ -1247,7 +1299,7 @@ def test_resumable_media_fail_unknown_response_code_subsequent_request(self):
12471299
)
12481300

12491301
status, body = request.next_chunk(http=http)
1250-
self.assertEquals(
1302+
self.assertEqual(
12511303
status.resumable_progress,
12521304
7,
12531305
"Should have first checked length and then tried to PUT more.",
@@ -1571,9 +1623,9 @@ def test_resumable_media_upload_no_content(self):
15711623
media_upload = MediaFileUpload(datafile("empty"), resumable=True)
15721624
request = zoo.animals().insert(media_body=media_upload, body=None)
15731625

1574-
self.assertEquals(media_upload, request.resumable)
1575-
self.assertEquals(request.body, None)
1576-
self.assertEquals(request.resumable_uri, None)
1626+
self.assertEqual(media_upload, request.resumable)
1627+
self.assertEqual(request.body, None)
1628+
self.assertEqual(request.resumable_uri, None)
15771629

15781630
http = HttpMockSequence(
15791631
[
@@ -1590,9 +1642,9 @@ def test_resumable_media_upload_no_content(self):
15901642
)
15911643

15921644
status, body = request.next_chunk(http=http)
1593-
self.assertEquals(None, body)
1645+
self.assertEqual(None, body)
15941646
self.assertTrue(isinstance(status, MediaUploadProgress))
1595-
self.assertEquals(0, status.progress())
1647+
self.assertEqual(0, status.progress())
15961648

15971649

15981650
class Next(unittest.TestCase):

tests/test_http.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,7 @@ def setUp(self):
11221122

11231123
def test_id_to_from_content_id_header(self):
11241124
batch = BatchHttpRequest()
1125-
self.assertEquals("12", batch._header_to_id(batch._id_to_header("12")))
1125+
self.assertEqual("12", batch._header_to_id(batch._id_to_header("12")))
11261126

11271127
def test_invalid_content_id_header(self):
11281128
batch = BatchHttpRequest()
@@ -1646,7 +1646,7 @@ def test_build_http_default_timeout_can_be_overridden(self):
16461646
def test_build_http_default_timeout_can_be_set_to_zero(self):
16471647
socket.setdefaulttimeout(0)
16481648
http = build_http()
1649-
self.assertEquals(http.timeout, 0)
1649+
self.assertEqual(http.timeout, 0)
16501650

16511651
def test_build_http_default_308_is_excluded_as_redirect(self):
16521652
http = build_http()

0 commit comments

Comments
 (0)