@@ -56,6 +56,11 @@ class Query(object):
5656 :param dataset: The namespace to which to restrict results.
5757 """
5858
59+ _NOT_FINISHED = datastore_pb .QueryResultBatch .NOT_FINISHED
60+ _FINISHED = (
61+ datastore_pb .QueryResultBatch .NO_MORE_RESULTS ,
62+ datastore_pb .QueryResultBatch .MORE_RESULTS_AFTER_LIMIT ,
63+ )
5964 OPERATORS = {
6065 '<=' : datastore_pb .PropertyFilter .LESS_THAN_OR_EQUAL ,
6166 '>=' : datastore_pb .PropertyFilter .GREATER_THAN_OR_EQUAL ,
@@ -69,7 +74,6 @@ def __init__(self, kind=None, dataset=None, namespace=None):
6974 self ._dataset = dataset
7075 self ._namespace = namespace
7176 self ._pb = datastore_pb .Query ()
72- self ._cursor = self ._more_results = None
7377 self ._offset = 0
7478
7579 if kind :
@@ -84,8 +88,6 @@ def _clone(self):
8488 clone = self .__class__ (dataset = self ._dataset ,
8589 namespace = self ._namespace )
8690 clone ._pb .CopyFrom (self ._pb )
87- clone ._cursor = self ._cursor
88- clone ._more_results = self ._more_results
8991 return clone
9092
9193 def namespace (self ):
@@ -228,29 +230,33 @@ def ancestor(self, ancestor):
228230
229231 return clone
230232
231- def kind (self , * kinds ):
233+ def kind (self , kind = None ):
232234 """Get or set the Kind of the Query.
233235
234- .. note::
235- This is an **additive** operation. That is, if the Query is
236- set for kinds A and B, and you call ``.kind('C')``, it will
237- query for kinds A, B, *and*, C.
238-
239- :type kinds: string
240- :param kinds: The entity kinds for which to query.
236+ :type kind: string
237+ :param kind: Optional. The entity kinds for which to query.
241238
242239 :rtype: string or :class:`Query`
243- :returns: If no arguments, returns the kind.
244- If a kind is provided, returns a clone of the :class:`Query`
245- with those kinds set.
240+ :returns: If `kind` is None, returns the kind. If a kind is provided,
241+ returns a clone of the :class:`Query` with that kind set.
242+ :raises: `ValueError` from the getter if multiple kinds are set on
243+ the query.
246244 """
247- if kinds :
245+ if kind is not None :
246+ kinds = [kind ]
248247 clone = self ._clone ()
249- for kind in kinds :
250- clone ._pb .kind .add ().name = kind
248+ clone ._pb .ClearField ('kind' )
249+ for new_kind in kinds :
250+ clone ._pb .kind .add ().name = new_kind
251251 return clone
252252 else :
253- return self ._pb .kind
253+ # In the proto definition for Query, `kind` is repeated.
254+ kind_names = [kind_expr .name for kind_expr in self ._pb .kind ]
255+ num_kinds = len (kind_names )
256+ if num_kinds == 1 :
257+ return kind_names [0 ]
258+ elif num_kinds > 1 :
259+ raise ValueError ('Only a single kind can be set.' )
254260
255261 def limit (self , limit = None ):
256262 """Get or set the limit of the Query.
@@ -302,8 +308,12 @@ def dataset(self, dataset=None):
302308 else :
303309 return self ._dataset
304310
305- def fetch (self , limit = None ):
306- """Executes the Query and returns all matching entities.
311+ def fetch_page (self , limit = None ):
312+ """Executes the Query and returns matching entities, and paging info.
313+
314+ In addition to the fetched entities, it also returns a cursor to allow
315+ paging through a results set and a boolean `more_results` indicating
316+ if there are any more.
307317
308318 This makes an API call to the Cloud Datastore, sends the Query
309319 as a protobuf, parses the responses to Entity protobufs, and
@@ -315,10 +325,10 @@ def fetch(self, limit=None):
315325 >>> from gcloud import datastore
316326 >>> dataset = datastore.get_dataset('dataset-id')
317327 >>> query = dataset.query('Person').filter('name', '=', 'Sally')
318- >>> query.fetch ()
319- [<Entity object>, <Entity object>, ...]
320- >>> query.fetch (1)
321- [<Entity object>]
328+ >>> query.fetch_page ()
329+ [<Entity object>, <Entity object>, ...], 'cursorbase64', True
330+ >>> query.fetch_page (1)
331+ [<Entity object>], 'cursorbase64', True
322332 >>> query.limit()
323333 None
324334
@@ -328,8 +338,13 @@ def fetch(self, limit=None):
328338 but the limit will be applied to the query
329339 before it is executed.
330340
331- :rtype: list of :class:`gcloud.datastore.entity.Entity`'s
332- :returns: The list of entities matching this query's criteria.
341+ :rtype: tuple of mixed types
342+ :returns: The first entry is a :class:`gcloud.datastore.entity.Entity`
343+ list matching this query's criteria. The second is a base64
344+ encoded cursor for paging and the third is a boolean
345+ indicating if there are more results.
346+ :raises: `ValueError` if more_results is not one of the enums
347+ NOT_FINISHED, MORE_RESULTS_AFTER_LIMIT, NO_MORE_RESULTS.
333348 """
334349 clone = self
335350
@@ -350,46 +365,69 @@ def fetch(self, limit=None):
350365 # results. See
351366 # https://github.com/GoogleCloudPlatform/gcloud-python/issues/280
352367 # for discussion.
353- entity_pbs , self . _cursor , self . _more_results = query_results [:3 ]
368+ entity_pbs , cursor_as_bytes , more_results_enum = query_results [:3 ]
354369
355- return [helpers .entity_from_protobuf (entity , dataset = self .dataset ())
356- for entity in entity_pbs ]
370+ entities = [helpers .entity_from_protobuf (entity ,
371+ dataset = self .dataset ())
372+ for entity in entity_pbs ]
357373
358- def cursor (self ):
359- """Returns cursor ID from most recent ``fetch()``.
374+ cursor = base64 .b64encode (cursor_as_bytes )
360375
361- .. warning:: Invoking this method on a query that has not yet
362- been executed will raise a RuntimeError.
376+ if more_results_enum == self ._NOT_FINISHED :
377+ more_results = True
378+ elif more_results_enum in self ._FINISHED :
379+ more_results = False
380+ else :
381+ raise ValueError ('Unexpected value returned for `more_results`.' )
363382
364- :rtype: string
365- :returns: base64-encoded cursor ID string denoting the last position
366- consumed in the query's result set.
367- """
368- if not self ._cursor :
369- raise RuntimeError ('No cursor' )
370- return base64 .b64encode (self ._cursor )
383+ return entities , cursor , more_results
371384
372- def more_results (self ):
373- """Returns ``more_results`` flag from most recent ``fetch()``.
385+ def fetch (self , limit = None ):
386+ """Executes the Query and returns matching entities
374387
375- .. warning:: Invoking this method on a query that has not yet
376- been executed will raise a RuntimeError.
388+ This calls `fetch_page()` but does not use the paging information.
377389
378- .. note::
390+ For example::
391+
392+ >>> from gcloud import datastore
393+ >>> dataset = datastore.get_dataset('dataset-id')
394+ >>> query = dataset.query('Person').filter('name', '=', 'Sally')
395+ >>> query.fetch()
396+ [<Entity object>, <Entity object>, ...]
397+ >>> query.fetch(1)
398+ [<Entity object>]
399+ >>> query.limit()
400+ None
379401
380- The `more_results` is not currently useful because it is
381- always returned by the back-end as ``MORE_RESULTS_AFTER_LIMIT``
382- even if there are no more results. See
383- https://github.com/GoogleCloudPlatform/gcloud-python/issues/280
384- for discussion .
402+ :type limit: integer
403+ :param limit: An optional limit to apply temporarily to this query.
404+ That is, the Query itself won't be altered,
405+ but the limit will be applied to the query
406+ before it is executed .
385407
386- :rtype: :class:`gcloud.datastore.datastore_v1_pb2.
387- QueryResultBatch.MoreResultsType`
388- :returns: enumerated value: are there more results available.
408+ :rtype: list of :class:`gcloud.datastore.entity.Entity`'s
409+ :returns: The list of entities matching this query's criteria.
389410 """
390- if self ._more_results is None :
391- raise RuntimeError ('No results' )
392- return self ._more_results
411+ entities , _ , _ = self .fetch_page (limit = limit )
412+ return entities
413+
414+ @property
415+ def start_cursor (self ):
416+ """Property to encode start cursor bytes as base64."""
417+ if not self ._pb .HasField ('start_cursor' ):
418+ return None
419+
420+ start_as_bytes = self ._pb .start_cursor
421+ return base64 .b64encode (start_as_bytes )
422+
423+ @property
424+ def end_cursor (self ):
425+ """Property to encode end cursor bytes as base64."""
426+ if not self ._pb .HasField ('end_cursor' ):
427+ return None
428+
429+ end_as_bytes = self ._pb .end_cursor
430+ return base64 .b64encode (end_as_bytes )
393431
394432 def with_cursor (self , start_cursor , end_cursor = None ):
395433 """Specifies the starting / ending positions in a query's result set.
0 commit comments