1# Copyright 2014 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Convenience wrapper for invoking APIs/factories w/ a project."""
15
16import os
17import warnings
18
19import google.api_core.client_options
20from google.auth.credentials import AnonymousCredentials  # type: ignore
21from google.cloud._helpers import _LocalStack  # type: ignore
22from google.cloud._helpers import _determine_default_project as _base_default_project  # type: ignore
23from google.cloud.client import ClientWithProject  # type: ignore
24from google.cloud.datastore.version import __version__
25from google.cloud.datastore import helpers
26from google.cloud.datastore._http import HTTPDatastoreAPI
27from google.cloud.datastore.batch import Batch
28from google.cloud.datastore.entity import Entity
29from google.cloud.datastore.key import Key
30from google.cloud.datastore.query import Query
31from google.cloud.datastore.transaction import Transaction
32
33try:
34    from google.cloud.datastore._gapic import make_datastore_api
35except ImportError:  # pragma: NO COVER
36    from google.api_core import client_info as api_core_client_info
37
38    def make_datastore_api(client):
39        raise RuntimeError("No gRPC available")
40
41    _HAVE_GRPC = False
42    _CLIENT_INFO = api_core_client_info.ClientInfo(client_library_version=__version__)
43else:
44    from google.api_core.gapic_v1 import client_info
45
46    _HAVE_GRPC = True
47    _CLIENT_INFO = client_info.ClientInfo(
48        client_library_version=__version__, gapic_version=__version__
49    )
50
51
52_MAX_LOOPS = 128
53"""Maximum number of iterations to wait for deferred keys."""
54_DATASTORE_BASE_URL = "https://datastore.googleapis.com"
55"""Datastore API request URL base."""
56
57DATASTORE_EMULATOR_HOST = "DATASTORE_EMULATOR_HOST"
58"""Environment variable defining host for datastore emulator server."""
59DATASTORE_DATASET = "DATASTORE_DATASET"
60"""Environment variable defining default dataset ID under GCD."""
61DISABLE_GRPC = "GOOGLE_CLOUD_DISABLE_GRPC"
62"""Environment variable acting as flag to disable gRPC."""
63
64_RESERVE_IDS_DEPRECATED_MESSAGE = """\
65Client.reserve_ids is deprecated. Please use \
66Client.reserve_ids_multi or Client.reserve_ids_sequential"""
67
68
69_USE_GRPC = _HAVE_GRPC and not os.getenv(DISABLE_GRPC, False)
70
71
72def _get_gcd_project():
73    """Gets the GCD application ID if it can be inferred."""
74    return os.getenv(DATASTORE_DATASET)
75
76
77def _determine_default_project(project=None):
78    """Determine default project explicitly or implicitly as fall-back.
79
80    In implicit case, supports four environments. In order of precedence, the
81    implicit environments are:
82
83    * DATASTORE_DATASET environment variable (for ``gcd`` / emulator testing)
84    * GOOGLE_CLOUD_PROJECT environment variable
85    * Google App Engine application ID
86    * Google Compute Engine project ID (from metadata server)
87
88    :type project: str
89    :param project: Optional. The project to use as default.
90
91    :rtype: str or ``NoneType``
92    :returns: Default project if it can be determined.
93    """
94    if project is None:
95        project = _get_gcd_project()
96
97    if project is None:
98        project = _base_default_project(project=project)
99
100    return project
101
102
103def _make_retry_timeout_kwargs(retry, timeout):
104    """Helper: make optional retry / timeout kwargs dict."""
105    kwargs = {}
106
107    if retry is not None:
108        kwargs["retry"] = retry
109
110    if timeout is not None:
111        kwargs["timeout"] = timeout
112
113    return kwargs
114
115
116def _extended_lookup(
117    datastore_api,
118    project,
119    key_pbs,
120    missing=None,
121    deferred=None,
122    eventual=False,
123    transaction_id=None,
124    retry=None,
125    timeout=None,
126):
127    """Repeat lookup until all keys found (unless stop requested).
128
129    Helper function for :meth:`Client.get_multi`.
130
131    :type datastore_api:
132        :class:`google.cloud.datastore._http.HTTPDatastoreAPI`
133        or :class:`google.cloud.datastore_v1.gapic.DatastoreClient`
134    :param datastore_api: The datastore API object used to connect
135                          to datastore.
136
137    :type project: str
138    :param project: The project to make the request for.
139
140    :type key_pbs: list of :class:`.entity_pb2.Key`
141    :param key_pbs: The keys to retrieve from the datastore.
142
143    :type missing: list
144    :param missing: (Optional) If a list is passed, the key-only entity
145                    protobufs returned by the backend as "missing" will be
146                    copied into it.
147
148    :type deferred: list
149    :param deferred: (Optional) If a list is passed, the key protobufs returned
150                     by the backend as "deferred" will be copied into it.
151
152    :type eventual: bool
153    :param eventual: If False (the default), request ``STRONG`` read
154                     consistency.  If True, request ``EVENTUAL`` read
155                     consistency.
156
157    :type transaction_id: str
158    :param transaction_id: If passed, make the request in the scope of
159                           the given transaction.  Incompatible with
160                           ``eventual==True``.
161
162    :type retry: :class:`google.api_core.retry.Retry`
163    :param retry:
164        A retry object used to retry requests. If ``None`` is specified,
165        requests will be retried using a default configuration.
166
167    :type timeout: float
168    :param timeout:
169        Time, in seconds, to wait for the request to complete.
170        Note that if ``retry`` is specified, the timeout applies
171        to each individual attempt.
172
173    :rtype: list of :class:`.entity_pb2.Entity`
174    :returns: The requested entities.
175    :raises: :class:`ValueError` if missing / deferred are not null or
176             empty list.
177    """
178    if missing is not None and missing != []:
179        raise ValueError("missing must be None or an empty list")
180
181    if deferred is not None and deferred != []:
182        raise ValueError("deferred must be None or an empty list")
183
184    kwargs = _make_retry_timeout_kwargs(retry, timeout)
185
186    results = []
187
188    loop_num = 0
189    read_options = helpers.get_read_options(eventual, transaction_id)
190    while loop_num < _MAX_LOOPS:  # loop against possible deferred.
191        loop_num += 1
192        lookup_response = datastore_api.lookup(
193            request={
194                "project_id": project,
195                "keys": key_pbs,
196                "read_options": read_options,
197            },
198            **kwargs,
199        )
200
201        # Accumulate the new results.
202        results.extend(result.entity for result in lookup_response.found)
203
204        if missing is not None:
205            missing.extend(result.entity for result in lookup_response.missing)
206
207        if deferred is not None:
208            deferred.extend(lookup_response.deferred)
209            break
210
211        if len(lookup_response.deferred) == 0:
212            break
213
214        # We have deferred keys, and the user didn't ask to know about
215        # them, so retry (but only with the deferred ones).
216        key_pbs = lookup_response.deferred
217
218    return results
219
220
221class Client(ClientWithProject):
222    """Convenience wrapper for invoking APIs/factories w/ a project.
223
224    .. doctest::
225
226       >>> from google.cloud import datastore
227       >>> client = datastore.Client()
228
229    :type project: str
230    :param project: (Optional) The project to pass to proxied API methods.
231
232    :type namespace: str
233    :param namespace: (Optional) namespace to pass to proxied API methods.
234
235    :type credentials: :class:`~google.auth.credentials.Credentials`
236    :param credentials: (Optional) The OAuth2 Credentials to use for this
237                        client. If not passed (and if no ``_http`` object is
238                        passed), falls back to the default inferred from the
239                        environment.
240
241    :type client_info: :class:`google.api_core.gapic_v1.client_info.ClientInfo`
242                       or :class:`google.api_core.client_info.ClientInfo`
243    :param client_info: (Optional) The client info used to send a user-agent
244                        string along with API requests. If ``None``, then
245                        default info will be used. Generally,
246                        you only need to set this if you're developing your
247                        own library or partner tool.
248
249    :type client_options: :class:`~google.api_core.client_options.ClientOptions`
250                          or :class:`dict`
251    :param client_options: (Optional) Client options used to set user options on the
252                           client. API Endpoint should be set through client_options.
253
254    :type _http: :class:`~requests.Session`
255    :param _http: (Optional) HTTP object to make requests. Can be any object
256                  that defines ``request()`` with the same interface as
257                  :meth:`requests.Session.request`. If not passed, an
258                  ``_http`` object is created that is bound to the
259                  ``credentials`` for the current object.
260                  This parameter should be considered private, and could
261                  change in the future.
262
263    :type _use_grpc: bool
264    :param _use_grpc: (Optional) Explicitly specifies whether
265                      to use the gRPC transport (via GAX) or HTTP. If unset,
266                      falls back to the ``GOOGLE_CLOUD_DISABLE_GRPC``
267                      environment variable.
268                      This parameter should be considered private, and could
269                      change in the future.
270    """
271
272    SCOPE = ("https://www.googleapis.com/auth/datastore",)
273    """The scopes required for authenticating as a Cloud Datastore consumer."""
274
275    def __init__(
276        self,
277        project=None,
278        namespace=None,
279        credentials=None,
280        client_info=_CLIENT_INFO,
281        client_options=None,
282        _http=None,
283        _use_grpc=None,
284    ):
285        emulator_host = os.getenv(DATASTORE_EMULATOR_HOST)
286
287        if emulator_host is not None:
288            if credentials is not None:
289                raise ValueError(
290                    "Explicit credentials are incompatible with the emulator"
291                )
292            credentials = AnonymousCredentials()
293
294        super(Client, self).__init__(
295            project=project,
296            credentials=credentials,
297            client_options=client_options,
298            _http=_http,
299        )
300        self.namespace = namespace
301        self._client_info = client_info
302        self._client_options = client_options
303        self._batch_stack = _LocalStack()
304        self._datastore_api_internal = None
305
306        if _use_grpc is None:
307            self._use_grpc = _USE_GRPC
308        else:
309            self._use_grpc = _use_grpc
310
311        if emulator_host is not None:
312            self._base_url = "http://" + emulator_host
313        else:
314            api_endpoint = _DATASTORE_BASE_URL
315            if client_options:
316                if type(client_options) == dict:
317                    client_options = google.api_core.client_options.from_dict(
318                        client_options
319                    )
320                if client_options.api_endpoint:
321                    api_endpoint = client_options.api_endpoint
322            self._base_url = api_endpoint
323
324    @staticmethod
325    def _determine_default(project):
326        """Helper:  override default project detection."""
327        return _determine_default_project(project)
328
329    @property
330    def base_url(self):
331        """Getter for API base URL."""
332        return self._base_url
333
334    @base_url.setter
335    def base_url(self, value):
336        """Setter for API base URL."""
337        self._base_url = value
338
339    @property
340    def _datastore_api(self):
341        """Getter for a wrapped API object."""
342        if self._datastore_api_internal is None:
343            if self._use_grpc:
344                self._datastore_api_internal = make_datastore_api(self)
345            else:
346                self._datastore_api_internal = HTTPDatastoreAPI(self)
347        return self._datastore_api_internal
348
349    def _push_batch(self, batch):
350        """Push a batch/transaction onto our stack.
351
352        "Protected", intended for use by batch / transaction context mgrs.
353
354        :type batch: :class:`google.cloud.datastore.batch.Batch`, or an object
355                     implementing its API.
356        :param batch: newly-active batch/transaction.
357        """
358        self._batch_stack.push(batch)
359
360    def _pop_batch(self):
361        """Pop a batch/transaction from our stack.
362
363        "Protected", intended for use by batch / transaction context mgrs.
364
365        :raises: IndexError if the stack is empty.
366        :rtype: :class:`google.cloud.datastore.batch.Batch`, or an object
367                 implementing its API.
368        :returns: the top-most batch/transaction, after removing it.
369        """
370        return self._batch_stack.pop()
371
372    @property
373    def current_batch(self):
374        """Currently-active batch.
375
376        :rtype: :class:`google.cloud.datastore.batch.Batch`, or an object
377                implementing its API, or ``NoneType`` (if no batch is active).
378        :returns: The batch/transaction at the top of the batch stack.
379        """
380        return self._batch_stack.top
381
382    @property
383    def current_transaction(self):
384        """Currently-active transaction.
385
386        :rtype: :class:`google.cloud.datastore.transaction.Transaction`, or an
387                object implementing its API, or ``NoneType`` (if no transaction
388                is active).
389        :returns: The transaction at the top of the batch stack.
390        """
391        transaction = self.current_batch
392        if isinstance(transaction, Transaction):
393            return transaction
394
395    def get(
396        self,
397        key,
398        missing=None,
399        deferred=None,
400        transaction=None,
401        eventual=False,
402        retry=None,
403        timeout=None,
404    ):
405        """Retrieve an entity from a single key (if it exists).
406
407        .. note::
408
409           This is just a thin wrapper over :meth:`get_multi`.
410           The backend API does not make a distinction between a single key or
411           multiple keys in a lookup request.
412
413        :type key: :class:`google.cloud.datastore.key.Key`
414        :param key: The key to be retrieved from the datastore.
415
416        :type missing: list
417        :param missing: (Optional) If a list is passed, the key-only entities
418                        returned by the backend as "missing" will be copied
419                        into it.
420
421        :type deferred: list
422        :param deferred: (Optional) If a list is passed, the keys returned
423                         by the backend as "deferred" will be copied into it.
424
425        :type transaction:
426            :class:`~google.cloud.datastore.transaction.Transaction`
427        :param transaction: (Optional) Transaction to use for read consistency.
428                            If not passed, uses current transaction, if set.
429
430        :type eventual: bool
431        :param eventual: (Optional) Defaults to strongly consistent (False).
432                         Setting True will use eventual consistency, but cannot
433                         be used inside a transaction or will raise ValueError.
434
435        :type retry: :class:`google.api_core.retry.Retry`
436        :param retry:
437            A retry object used to retry requests. If ``None`` is specified,
438            requests will be retried using a default configuration.
439
440        :type timeout: float
441        :param timeout:
442            Time, in seconds, to wait for the request to complete.
443            Note that if ``retry`` is specified, the timeout applies
444            to each individual attempt.
445
446        :rtype: :class:`google.cloud.datastore.entity.Entity` or ``NoneType``
447        :returns: The requested entity if it exists.
448
449        :raises: :class:`ValueError` if eventual is True and in a transaction.
450        """
451        entities = self.get_multi(
452            keys=[key],
453            missing=missing,
454            deferred=deferred,
455            transaction=transaction,
456            eventual=eventual,
457            retry=retry,
458            timeout=timeout,
459        )
460        if entities:
461            return entities[0]
462
463    def get_multi(
464        self,
465        keys,
466        missing=None,
467        deferred=None,
468        transaction=None,
469        eventual=False,
470        retry=None,
471        timeout=None,
472    ):
473        """Retrieve entities, along with their attributes.
474
475        :type keys: list of :class:`google.cloud.datastore.key.Key`
476        :param keys: The keys to be retrieved from the datastore.
477
478        :type missing: list
479        :param missing: (Optional) If a list is passed, the key-only entities
480                        returned by the backend as "missing" will be copied
481                        into it. If the list is not empty, an error will occur.
482
483        :type deferred: list
484        :param deferred: (Optional) If a list is passed, the keys returned
485                         by the backend as "deferred" will be copied into it.
486                         If the list is not empty, an error will occur.
487
488        :type transaction:
489            :class:`~google.cloud.datastore.transaction.Transaction`
490        :param transaction: (Optional) Transaction to use for read consistency.
491                            If not passed, uses current transaction, if set.
492
493        :type eventual: bool
494        :param eventual: (Optional) Defaults to strongly consistent (False).
495                         Setting True will use eventual consistency, but cannot
496                         be used inside a transaction or will raise ValueError.
497
498        :type retry: :class:`google.api_core.retry.Retry`
499        :param retry:
500            A retry object used to retry requests. If ``None`` is specified,
501            requests will be retried using a default configuration.
502
503        :type timeout: float
504        :param timeout:
505            Time, in seconds, to wait for the request to complete.
506            Note that if ``retry`` is specified, the timeout applies
507            to each individual attempt.
508
509        :rtype: list of :class:`google.cloud.datastore.entity.Entity`
510        :returns: The requested entities.
511        :raises: :class:`ValueError` if one or more of ``keys`` has a project
512                 which does not match our project.
513        :raises: :class:`ValueError` if eventual is True and in a transaction.
514        """
515        if not keys:
516            return []
517
518        ids = set(key.project for key in keys)
519        for current_id in ids:
520            if current_id != self.project:
521                raise ValueError("Keys do not match project")
522
523        if transaction is None:
524            transaction = self.current_transaction
525
526        entity_pbs = _extended_lookup(
527            datastore_api=self._datastore_api,
528            project=self.project,
529            key_pbs=[key.to_protobuf() for key in keys],
530            eventual=eventual,
531            missing=missing,
532            deferred=deferred,
533            transaction_id=transaction and transaction.id,
534            retry=retry,
535            timeout=timeout,
536        )
537
538        if missing is not None:
539            missing[:] = [
540                helpers.entity_from_protobuf(missed_pb) for missed_pb in missing
541            ]
542
543        if deferred is not None:
544            deferred[:] = [
545                helpers.key_from_protobuf(deferred_pb) for deferred_pb in deferred
546            ]
547
548        return [helpers.entity_from_protobuf(entity_pb._pb) for entity_pb in entity_pbs]
549
550    def put(self, entity, retry=None, timeout=None):
551        """Save an entity in the Cloud Datastore.
552
553        .. note::
554
555           This is just a thin wrapper over :meth:`put_multi`.
556           The backend API does not make a distinction between a single
557           entity or multiple entities in a commit request.
558
559        :type entity: :class:`google.cloud.datastore.entity.Entity`
560        :param entity: The entity to be saved to the datastore.
561
562        :type retry: :class:`google.api_core.retry.Retry`
563        :param retry:
564            A retry object used to retry requests. If ``None`` is specified,
565            requests will be retried using a default configuration.
566            Only meaningful outside of another batch / transaction.
567
568        :type timeout: float
569        :param timeout:
570            Time, in seconds, to wait for the request to complete.
571            Note that if ``retry`` is specified, the timeout applies
572            to each individual attempt.  Only meaningful outside of another
573            batch / transaction.
574        """
575        self.put_multi(entities=[entity], retry=retry, timeout=timeout)
576
577    def put_multi(self, entities, retry=None, timeout=None):
578        """Save entities in the Cloud Datastore.
579
580        :type entities: list of :class:`google.cloud.datastore.entity.Entity`
581        :param entities: The entities to be saved to the datastore.
582
583        :type retry: :class:`google.api_core.retry.Retry`
584        :param retry:
585            A retry object used to retry requests. If ``None`` is specified,
586            requests will be retried using a default configuration.
587            Only meaningful outside of another batch / transaction.
588
589        :type timeout: float
590        :param timeout:
591            Time, in seconds, to wait for the request to complete.
592            Note that if ``retry`` is specified, the timeout applies
593            to each individual attempt.  Only meaningful outside of another
594            batch / transaction.
595
596        :raises: :class:`ValueError` if ``entities`` is a single entity.
597        """
598        if isinstance(entities, Entity):
599            raise ValueError("Pass a sequence of entities")
600
601        if not entities:
602            return
603
604        current = self.current_batch
605        in_batch = current is not None
606
607        if not in_batch:
608            current = self.batch()
609            current.begin()
610
611        for entity in entities:
612            current.put(entity)
613
614        if not in_batch:
615            current.commit(retry=retry, timeout=timeout)
616
617    def delete(self, key, retry=None, timeout=None):
618        """Delete the key in the Cloud Datastore.
619
620        .. note::
621
622           This is just a thin wrapper over :meth:`delete_multi`.
623           The backend API does not make a distinction between a single key or
624           multiple keys in a commit request.
625
626        :type key: :class:`google.cloud.datastore.key.Key`, :class:`google.cloud.datastore.entity.Entity`
627
628        :param key: The key to be deleted from the datastore.
629
630        :type retry: :class:`google.api_core.retry.Retry`
631        :param retry:
632            A retry object used to retry requests. If ``None`` is specified,
633            requests will be retried using a default configuration.
634            Only meaningful outside of another batch / transaction.
635
636        :type timeout: float
637        :param timeout:
638            Time, in seconds, to wait for the request to complete.
639            Note that if ``retry`` is specified, the timeout applies
640            to each individual attempt.  Only meaningful outside of another
641            batch / transaction.
642        """
643        self.delete_multi(keys=[key], retry=retry, timeout=timeout)
644
645    def delete_multi(self, keys, retry=None, timeout=None):
646        """Delete keys from the Cloud Datastore.
647
648        :type keys: list of :class:`google.cloud.datastore.key.Key`, :class:`google.cloud.datastore.entity.Entity`
649        :param keys: The keys to be deleted from the Datastore.
650
651        :type retry: :class:`google.api_core.retry.Retry`
652        :param retry:
653            A retry object used to retry requests. If ``None`` is specified,
654            requests will be retried using a default configuration.
655            Only meaningful outside of another batch / transaction.
656
657        :type timeout: float
658        :param timeout:
659            Time, in seconds, to wait for the request to complete.
660            Note that if ``retry`` is specified, the timeout applies
661            to each individual attempt.  Only meaningful outside of another
662            batch / transaction.
663        """
664        if not keys:
665            return
666
667        # We allow partial keys to attempt a delete, the backend will fail.
668        current = self.current_batch
669        in_batch = current is not None
670
671        if not in_batch:
672            current = self.batch()
673            current.begin()
674
675        for key in keys:
676            if isinstance(key, Entity):
677                # If the key is in fact an Entity, the key can be extracted.
678                key = key.key
679            current.delete(key)
680
681        if not in_batch:
682            current.commit(retry=retry, timeout=timeout)
683
684    def allocate_ids(self, incomplete_key, num_ids, retry=None, timeout=None):
685        """Allocate a list of IDs from a partial key.
686
687        :type incomplete_key: :class:`google.cloud.datastore.key.Key`
688        :param incomplete_key: Partial key to use as base for allocated IDs.
689
690        :type num_ids: int
691        :param num_ids: The number of IDs to allocate.
692
693        :type retry: :class:`google.api_core.retry.Retry`
694        :param retry:
695            A retry object used to retry requests. If ``None`` is specified,
696            requests will be retried using a default configuration.
697
698        :type timeout: float
699        :param timeout:
700            Time, in seconds, to wait for the request to complete.
701            Note that if ``retry`` is specified, the timeout applies
702            to each individual attempt.
703
704        :rtype: list of :class:`google.cloud.datastore.key.Key`
705        :returns: The (complete) keys allocated with ``incomplete_key`` as
706                  root.
707        :raises: :class:`ValueError` if ``incomplete_key`` is not a
708                 partial key.
709        """
710        if not incomplete_key.is_partial:
711            raise ValueError(("Key is not partial.", incomplete_key))
712
713        incomplete_key_pb = incomplete_key.to_protobuf()
714        incomplete_key_pbs = [incomplete_key_pb] * num_ids
715
716        kwargs = _make_retry_timeout_kwargs(retry, timeout)
717
718        response_pb = self._datastore_api.allocate_ids(
719            request={"project_id": incomplete_key.project, "keys": incomplete_key_pbs},
720            **kwargs,
721        )
722        allocated_ids = [
723            allocated_key_pb.path[-1].id for allocated_key_pb in response_pb.keys
724        ]
725        return [
726            incomplete_key.completed_key(allocated_id) for allocated_id in allocated_ids
727        ]
728
729    def key(self, *path_args, **kwargs):
730        """Proxy to :class:`google.cloud.datastore.key.Key`.
731
732        Passes our ``project``.
733        """
734        if "project" in kwargs:
735            raise TypeError("Cannot pass project")
736        kwargs["project"] = self.project
737        if "namespace" not in kwargs:
738            kwargs["namespace"] = self.namespace
739        return Key(*path_args, **kwargs)
740
741    def entity(self, key=None, exclude_from_indexes=()):
742        """Proxy to :class:`google.cloud.datastore.entity.Entity`."""
743        return Entity(key=key, exclude_from_indexes=exclude_from_indexes)
744
745    def batch(self):
746        """Proxy to :class:`google.cloud.datastore.batch.Batch`."""
747        return Batch(self)
748
749    def transaction(self, **kwargs):
750        """Proxy to :class:`google.cloud.datastore.transaction.Transaction`.
751
752        :param kwargs: Keyword arguments to be passed in.
753        """
754        return Transaction(self, **kwargs)
755
756    def query(self, **kwargs):
757        """Proxy to :class:`google.cloud.datastore.query.Query`.
758
759        Passes our ``project``.
760
761        Using query to search a datastore:
762
763        .. testsetup:: query
764
765            import uuid
766
767            from google.cloud import datastore
768
769            unique = str(uuid.uuid4())[0:8]
770            client = datastore.Client(namespace='ns{}'.format(unique))
771
772            def do_something_with(entity):
773                pass
774
775        .. doctest:: query
776
777            >>> query = client.query(kind='MyKind')
778            >>> query.add_filter('property', '=', 'val')
779            <google.cloud.datastore.query.Query object at ...>
780
781        Using the query iterator
782
783        .. doctest:: query
784
785            >>> filters = [('property', '=', 'val')]
786            >>> query = client.query(kind='MyKind', filters=filters)
787            >>> query_iter = query.fetch()
788            >>> for entity in query_iter:
789            ...     do_something_with(entity)
790
791        or manually page through results
792
793        .. doctest:: query
794
795            >>> query_iter = query.fetch()
796            >>> pages = query_iter.pages
797            >>>
798            >>> first_page = next(pages)
799            >>> first_page_entities = list(first_page)
800            >>> query_iter.next_page_token is None
801            True
802
803        :param kwargs: Parameters for initializing and instance of
804                       :class:`~google.cloud.datastore.query.Query`.
805
806        :rtype: :class:`~google.cloud.datastore.query.Query`
807        :returns: A query object.
808        """
809        if "client" in kwargs:
810            raise TypeError("Cannot pass client")
811        if "project" in kwargs:
812            raise TypeError("Cannot pass project")
813        kwargs["project"] = self.project
814        if "namespace" not in kwargs:
815            kwargs["namespace"] = self.namespace
816        return Query(self, **kwargs)
817
818    def reserve_ids_sequential(self, complete_key, num_ids, retry=None, timeout=None):
819        """Reserve a list of IDs sequentially from a complete key.
820
821        This will reserve the key passed as `complete_key` as well as
822        additional keys derived by incrementing the last ID in the path of
823        `complete_key` sequentially to obtain the number of keys specified in
824        `num_ids`.
825
826        :type complete_key: :class:`google.cloud.datastore.key.Key`
827        :param complete_key:
828            Complete key to use as base for reserved IDs. Key must use a
829            numeric ID and not a string name.
830
831        :type num_ids: int
832        :param num_ids: The number of IDs to reserve.
833
834        :type retry: :class:`google.api_core.retry.Retry`
835        :param retry:
836            A retry object used to retry requests. If ``None`` is specified,
837            requests will be retried using a default configuration.
838
839        :type timeout: float
840        :param timeout:
841            Time, in seconds, to wait for the request to complete.
842            Note that if ``retry`` is specified, the timeout applies
843            to each individual attempt.
844
845        :rtype: class:`NoneType`
846        :returns: None
847        :raises: :class:`ValueError` if `complete_key`` is not a
848                 Complete key.
849        """
850        if complete_key.is_partial:
851            raise ValueError(("Key is not Complete.", complete_key))
852
853        if complete_key.id is None:
854            raise ValueError(("Key must use numeric id.", complete_key))
855
856        if not isinstance(num_ids, int):
857            raise ValueError(("num_ids is not a valid integer.", num_ids))
858
859        key_class = type(complete_key)
860        namespace = complete_key._namespace
861        project = complete_key._project
862        flat_path = list(complete_key._flat_path[:-1])
863        start_id = complete_key._flat_path[-1]
864
865        key_pbs = []
866        for id in range(start_id, start_id + num_ids):
867            path = flat_path + [id]
868            key = key_class(*path, project=project, namespace=namespace)
869            key_pbs.append(key.to_protobuf())
870
871        kwargs = _make_retry_timeout_kwargs(retry, timeout)
872        self._datastore_api.reserve_ids(
873            request={"project_id": complete_key.project, "keys": key_pbs}, **kwargs
874        )
875        return None
876
877    def reserve_ids(self, complete_key, num_ids, retry=None, timeout=None):
878        """Reserve a list of IDs sequentially from a complete key.
879
880        DEPRECATED. Alias for :meth:`reserve_ids_sequential`.
881
882        Please use either :meth:`reserve_ids_multi` (recommended) or
883        :meth:`reserve_ids_sequential`.
884        """
885        warnings.warn(_RESERVE_IDS_DEPRECATED_MESSAGE, DeprecationWarning)
886        return self.reserve_ids_sequential(
887            complete_key, num_ids, retry=retry, timeout=timeout
888        )
889
890    def reserve_ids_multi(self, complete_keys, retry=None, timeout=None):
891        """Reserve IDs from a list of complete keys.
892
893        :type complete_keys: `list` of :class:`google.cloud.datastore.key.Key`
894        :param complete_keys:
895            Complete keys for which to reserve IDs.
896
897        :type retry: :class:`google.api_core.retry.Retry`
898        :param retry:
899            A retry object used to retry requests. If ``None`` is specified,
900            requests will be retried using a default configuration.
901
902        :type timeout: float
903        :param timeout:
904            Time, in seconds, to wait for the request to complete.
905            Note that if ``retry`` is specified, the timeout applies
906            to each individual attempt.
907
908        :rtype: class:`NoneType`
909        :returns: None
910        :raises: :class:`ValueError` if any of `complete_keys`` is not a
911                 Complete key.
912        """
913        for complete_key in complete_keys:
914            if complete_key.is_partial:
915                raise ValueError(("Key is not Complete.", complete_key))
916
917        kwargs = _make_retry_timeout_kwargs(retry, timeout)
918        key_pbs = [key.to_protobuf() for key in complete_keys]
919        self._datastore_api.reserve_ids(
920            request={"project_id": complete_keys[0].project, "keys": key_pbs}, **kwargs
921        )
922
923        return None
924