Skip to content

Commit 3f124a5

Browse files
authored
Merge pull request googleapis#3105 from dhermes/fix-2746
Add basic helpers needed for GAPIC client in datastore.
2 parents 5dffc63 + 70b8b91 commit 3f124a5

File tree

6 files changed

+183
-7
lines changed

6 files changed

+183
-7
lines changed

datastore/google/cloud/datastore/_gax.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717

1818
import contextlib
1919

20+
from google.cloud.gapic.datastore.v1 import datastore_client
2021
from google.cloud.proto.datastore.v1 import datastore_pb2_grpc
2122
from google.gax.utils import metrics
2223
from grpc import StatusCode
2324

2425
from google.cloud._helpers import make_insecure_stub
26+
from google.cloud._helpers import make_secure_channel
2527
from google.cloud._helpers import make_secure_stub
28+
from google.cloud._http import DEFAULT_USER_AGENT
2629
from google.cloud import exceptions
2730

2831
from google.cloud.datastore import __version__
@@ -204,3 +207,19 @@ def allocate_ids(self, project, request_pb):
204207
request_pb.project_id = project
205208
with _grpc_catch_rendezvous():
206209
return self._stub.AllocateIds(request_pb)
210+
211+
212+
def make_datastore_api(client):
213+
"""Create an instance of the GAPIC Datastore API.
214+
215+
:type client: :class:`~google.cloud.datastore.client.Client`
216+
:param client: The client that holds configuration details.
217+
218+
:rtype: :class:`.datastore.v1.datastore_client.DatastoreClient`
219+
:returns: A datastore API instance with the proper credentials.
220+
"""
221+
channel = make_secure_channel(
222+
client._credentials, DEFAULT_USER_AGENT,
223+
datastore_client.DatastoreClient.SERVICE_ADDRESS)
224+
return datastore_client.DatastoreClient(
225+
channel=channel, lib_name='gccl', lib_version=__version__)

datastore/google/cloud/datastore/_http.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,19 @@ def allocate_ids(self, project, key_pbs):
467467
return self._datastore_api.allocate_ids(project, request)
468468

469469

470+
class HTTPDatastoreAPI(object):
471+
"""An API object that sends proto-over-HTTP requests.
472+
473+
Intended to provide the same methods as the GAPIC ``DatastoreClient``.
474+
475+
:type client: :class:`~google.cloud.datastore.client.Client`
476+
:param client: The client that provides configuration.
477+
"""
478+
479+
def __init__(self, client):
480+
self.client = client
481+
482+
470483
def _set_read_options(request, eventual, transaction_id):
471484
"""Validate rules for read options, and assign to the request.
472485

datastore/google/cloud/datastore/client.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,30 @@
1919
from google.cloud._helpers import (
2020
_determine_default_project as _base_default_project)
2121
from google.cloud.client import ClientWithProject
22+
from google.cloud.environment_vars import DISABLE_GRPC
23+
from google.cloud.environment_vars import GCD_DATASET
24+
2225
from google.cloud.datastore._http import Connection
26+
from google.cloud.datastore._http import HTTPDatastoreAPI
2327
from google.cloud.datastore import helpers
2428
from google.cloud.datastore.batch import Batch
2529
from google.cloud.datastore.entity import Entity
2630
from google.cloud.datastore.key import Key
2731
from google.cloud.datastore.query import Query
2832
from google.cloud.datastore.transaction import Transaction
29-
from google.cloud.environment_vars import GCD_DATASET
33+
try:
34+
from google.cloud.datastore._gax import make_datastore_api
35+
_HAVE_GRPC = True
36+
except ImportError: # pragma: NO COVER
37+
make_datastore_api = None
38+
_HAVE_GRPC = False
3039

3140

3241
_MAX_LOOPS = 128
3342
"""Maximum number of iterations to wait for deferred keys."""
3443

44+
_USE_GAX = _HAVE_GRPC and not os.getenv(DISABLE_GRPC, False)
45+
3546

3647
def _get_gcd_project():
3748
"""Gets the GCD application ID if it can be inferred."""
@@ -169,24 +180,45 @@ class Client(ClientWithProject):
169180
:meth:`~httplib2.Http.request`. If not passed, an
170181
``http`` object is created that is bound to the
171182
``credentials`` for the current object.
183+
184+
:type use_gax: bool
185+
:param use_gax: (Optional) Explicitly specifies whether
186+
to use the gRPC transport (via GAX) or HTTP. If unset,
187+
falls back to the ``GOOGLE_CLOUD_DISABLE_GRPC`` environment
188+
variable.
172189
"""
173190

174191
SCOPE = ('https://www.googleapis.com/auth/datastore',)
175192
"""The scopes required for authenticating as a Cloud Datastore consumer."""
176193

177194
def __init__(self, project=None, namespace=None,
178-
credentials=None, http=None):
195+
credentials=None, http=None, use_gax=None):
179196
super(Client, self).__init__(
180197
project=project, credentials=credentials, http=http)
181198
self._connection = Connection(self)
182199
self.namespace = namespace
183200
self._batch_stack = _LocalStack()
201+
self._datastore_api_internal = None
202+
if use_gax is None:
203+
self._use_gax = _USE_GAX
204+
else:
205+
self._use_gax = use_gax
184206

185207
@staticmethod
186208
def _determine_default(project):
187209
"""Helper: override default project detection."""
188210
return _determine_default_project(project)
189211

212+
@property
213+
def _datastore_api(self):
214+
"""Getter for a wrapped API object."""
215+
if self._datastore_api_internal is None:
216+
if self._use_gax:
217+
self._datastore_api_internal = make_datastore_api(self)
218+
else:
219+
self._datastore_api_internal = HTTPDatastoreAPI(self)
220+
return self._datastore_api_internal
221+
190222
def _push_batch(self, batch):
191223
"""Push a batch/transaction onto our stack.
192224

datastore/unit_tests/test__gax.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,37 @@ def test_allocate_ids(self):
269269
[(request_pb, 'AllocateIds')])
270270

271271

272+
@unittest.skipUnless(_HAVE_GRPC, 'No gRPC')
273+
class Test_make_datastore_api(unittest.TestCase):
274+
275+
def _call_fut(self, client):
276+
from google.cloud.datastore._gax import make_datastore_api
277+
278+
return make_datastore_api(client)
279+
280+
@mock.patch(
281+
'google.cloud.gapic.datastore.v1.datastore_client.DatastoreClient',
282+
SERVICE_ADDRESS='datastore.mock.mock',
283+
return_value=mock.sentinel.ds_client)
284+
@mock.patch('google.cloud.datastore._gax.make_secure_channel',
285+
return_value=mock.sentinel.channel)
286+
def test_it(self, make_chan, mock_klass):
287+
from google.cloud._http import DEFAULT_USER_AGENT
288+
from google.cloud.datastore import __version__
289+
290+
client = mock.Mock(
291+
_credentials=mock.sentinel.credentials, spec=['_credentials'])
292+
ds_api = self._call_fut(client)
293+
self.assertIs(ds_api, mock.sentinel.ds_client)
294+
295+
make_chan.assert_called_once_with(
296+
mock.sentinel.credentials, DEFAULT_USER_AGENT,
297+
mock_klass.SERVICE_ADDRESS)
298+
mock_klass.assert_called_once_with(
299+
channel=mock.sentinel.channel, lib_name='gccl',
300+
lib_version=__version__)
301+
302+
272303
class _GRPCStub(object):
273304

274305
def __init__(self, return_val=None, side_effect=Exception):

datastore/unit_tests/test__http.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,23 @@ def test_allocate_ids_non_empty(self):
914914
self.assertEqual(key_before, key_after)
915915

916916

917+
class TestHTTPDatastoreAPI(unittest.TestCase):
918+
919+
@staticmethod
920+
def _get_target_class():
921+
from google.cloud.datastore._http import HTTPDatastoreAPI
922+
923+
return HTTPDatastoreAPI
924+
925+
def _make_one(self, *args, **kwargs):
926+
return self._get_target_class()(*args, **kwargs)
927+
928+
def test_constructor(self):
929+
client = object()
930+
ds_api = self._make_one(client)
931+
self.assertIs(ds_api.client, client)
932+
933+
917934
class Http(object):
918935

919936
_called_with = None

datastore/unit_tests/test_client.py

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,14 @@ def _get_target_class():
138138
return Client
139139

140140
def _make_one(self, project=PROJECT, namespace=None,
141-
credentials=None, http=None):
141+
credentials=None, http=None, use_gax=None):
142142
return self._get_target_class()(project=project,
143143
namespace=namespace,
144144
credentials=credentials,
145-
http=http)
145+
http=http,
146+
use_gax=use_gax)
146147

147-
def test_ctor_w_project_no_environ(self):
148+
def test_constructor_w_project_no_environ(self):
148149
# Some environments (e.g. AppVeyor CI) run in GCE, so
149150
# this test would fail artificially.
150151
patch = mock.patch(
@@ -153,7 +154,7 @@ def test_ctor_w_project_no_environ(self):
153154
with patch:
154155
self.assertRaises(EnvironmentError, self._make_one, None)
155156

156-
def test_ctor_w_implicit_inputs(self):
157+
def test_constructor_w_implicit_inputs(self):
157158
OTHER = 'other'
158159
creds = _make_credentials()
159160
default_called = []
@@ -183,7 +184,7 @@ def fallback_mock(project):
183184
self.assertIsNone(client.current_transaction)
184185
self.assertEqual(default_called, [None])
185186

186-
def test_ctor_w_explicit_inputs(self):
187+
def test_constructor_w_explicit_inputs(self):
187188
OTHER = 'other'
188189
NAMESPACE = 'namespace'
189190
creds = _make_credentials()
@@ -200,6 +201,69 @@ def test_ctor_w_explicit_inputs(self):
200201
self.assertIsNone(client.current_batch)
201202
self.assertEqual(list(client._batch_stack), [])
202203

204+
def test_constructor_use_gax_default(self):
205+
import google.cloud.datastore.client as MUT
206+
207+
project = 'PROJECT'
208+
creds = _make_credentials()
209+
http = object()
210+
211+
with mock.patch.object(MUT, '_USE_GAX', new=True):
212+
client1 = self._make_one(
213+
project=project, credentials=creds, http=http)
214+
self.assertTrue(client1._use_gax)
215+
# Explicitly over-ride the environment.
216+
client2 = self._make_one(
217+
project=project, credentials=creds, http=http,
218+
use_gax=False)
219+
self.assertFalse(client2._use_gax)
220+
221+
with mock.patch.object(MUT, '_USE_GAX', new=False):
222+
client3 = self._make_one(
223+
project=project, credentials=creds, http=http)
224+
self.assertFalse(client3._use_gax)
225+
# Explicitly over-ride the environment.
226+
client4 = self._make_one(
227+
project=project, credentials=creds, http=http,
228+
use_gax=True)
229+
self.assertTrue(client4._use_gax)
230+
231+
def test__datastore_api_property_gax(self):
232+
client = self._make_one(
233+
project='prahj-ekt', credentials=_make_credentials(),
234+
http=object(), use_gax=True)
235+
236+
self.assertIsNone(client._datastore_api_internal)
237+
patch = mock.patch(
238+
'google.cloud.datastore.client.make_datastore_api',
239+
return_value=mock.sentinel.ds_api)
240+
with patch as make_api:
241+
ds_api = client._datastore_api
242+
self.assertIs(ds_api, mock.sentinel.ds_api)
243+
make_api.assert_called_once_with(client)
244+
self.assertIs(
245+
client._datastore_api_internal, mock.sentinel.ds_api)
246+
# Make sure the cached value is used.
247+
self.assertEqual(make_api.call_count, 1)
248+
self.assertIs(
249+
client._datastore_api, mock.sentinel.ds_api)
250+
self.assertEqual(make_api.call_count, 1)
251+
252+
def test__datastore_api_property_http(self):
253+
from google.cloud.datastore._http import HTTPDatastoreAPI
254+
255+
client = self._make_one(
256+
project='prahj-ekt', credentials=_make_credentials(),
257+
http=object(), use_gax=False)
258+
259+
self.assertIsNone(client._datastore_api_internal)
260+
ds_api = client._datastore_api
261+
self.assertIsInstance(ds_api, HTTPDatastoreAPI)
262+
self.assertIs(ds_api.client, client)
263+
# Make sure the cached value is used.
264+
self.assertIs(client._datastore_api_internal, ds_api)
265+
self.assertIs(client._datastore_api, ds_api)
266+
203267
def test__push_batch_and__pop_batch(self):
204268
creds = _make_credentials()
205269
client = self._make_one(credentials=creds)

0 commit comments

Comments
 (0)