@@ -56,6 +56,8 @@ class Query(object):
5656 :param dataset: The namespace to which to restrict results.
5757 """
5858
59+ _MORE_RESULTS = datastore_pb .QueryResultBatch .MORE_RESULTS_AFTER_LIMIT
60+ _NO_MORE_RESULTS = datastore_pb .QueryResultBatch .NO_MORE_RESULTS
5961 OPERATORS = {
6062 '<=' : datastore_pb .PropertyFilter .LESS_THAN_OR_EQUAL ,
6163 '>=' : datastore_pb .PropertyFilter .GREATER_THAN_OR_EQUAL ,
@@ -69,7 +71,6 @@ def __init__(self, kind=None, dataset=None, namespace=None):
6971 self ._dataset = dataset
7072 self ._namespace = namespace
7173 self ._pb = datastore_pb .Query ()
72- self ._cursor = self ._more_results = None
7374 self ._offset = 0
7475
7576 if kind :
@@ -84,8 +85,6 @@ def _clone(self):
8485 clone = self .__class__ (dataset = self ._dataset ,
8586 namespace = self ._namespace )
8687 clone ._pb .CopyFrom (self ._pb )
87- clone ._cursor = self ._cursor
88- clone ._more_results = self ._more_results
8988 return clone
9089
9190 def namespace (self ):
@@ -239,8 +238,8 @@ def kind(self, *kinds):
239238 :type kinds: string
240239 :param kinds: The entity kinds for which to query.
241240
242- :rtype: string or :class:`Query`
243- :returns: If no arguments, returns the kind.
241+ :rtype: string, list of strings, or :class:`Query`
242+ :returns: If no arguments, returns the kind or list of kinds .
244243 If a kind is provided, returns a clone of the :class:`Query`
245244 with those kinds set.
246245 """
@@ -250,7 +249,13 @@ def kind(self, *kinds):
250249 clone ._pb .kind .add ().name = kind
251250 return clone
252251 else :
253- return self ._pb .kind
252+ # In the proto definition for Query, `kind` is repeated.
253+ kind_names = [kind_expr .name for kind_expr in self ._pb .kind ]
254+ num_kinds = len (kind_names )
255+ if num_kinds == 1 :
256+ return kind_names [0 ]
257+ elif num_kinds > 1 :
258+ return kind_names
254259
255260 def limit (self , limit = None ):
256261 """Get or set the limit of the Query.
@@ -302,8 +307,12 @@ def dataset(self, dataset=None):
302307 else :
303308 return self ._dataset
304309
305- def fetch (self , limit = None ):
306- """Executes the Query and returns all matching entities.
310+ def fetch_page (self , limit = None ):
311+ """Executes the Query and returns matching entities, and paging info.
312+
313+ In addition to the fetched entities, it also returns a cursor to allow
314+ paging through a results set and a boolean `more_results` indicating
315+ if there are any more.
307316
308317 This makes an API call to the Cloud Datastore, sends the Query
309318 as a protobuf, parses the responses to Entity protobufs, and
@@ -315,10 +324,10 @@ def fetch(self, limit=None):
315324 >>> from gcloud import datastore
316325 >>> dataset = datastore.get_dataset('dataset-id')
317326 >>> query = dataset.query('Person').filter('name', '=', 'Sally')
318- >>> query.fetch ()
319- [<Entity object>, <Entity object>, ...]
320- >>> query.fetch (1)
321- [<Entity object>]
327+ >>> query.fetch_page ()
328+ [<Entity object>, <Entity object>, ...], 'cursorbase64', True
329+ >>> query.fetch_page (1)
330+ [<Entity object>], 'cursorbase64', True
322331 >>> query.limit()
323332 None
324333
@@ -328,8 +337,13 @@ def fetch(self, limit=None):
328337 but the limit will be applied to the query
329338 before it is executed.
330339
331- :rtype: list of :class:`gcloud.datastore.entity.Entity`'s
332- :returns: The list of entities matching this query's criteria.
340+ :rtype: tuple of mixed types
341+ :returns: The first entry is a :class:`gcloud.datastore.entity.Entity`
342+ list matching this query's criteria. The second is a base64
343+ encoded cursor for paging and the third is a boolean
344+ indicating if there are more results.
345+ :raises: `ValueError` if more_results is not one of the enums
346+ MORE_RESULTS_AFTER_LIMIT or NO_MORE_RESULTS.
333347 """
334348 clone = self
335349
@@ -350,46 +364,71 @@ def fetch(self, limit=None):
350364 # results. See
351365 # https://github.com/GoogleCloudPlatform/gcloud-python/issues/280
352366 # for discussion.
353- entity_pbs , self . _cursor , self . _more_results = query_results [:3 ]
367+ entity_pbs , cursor_as_bytes , more_results_enum = query_results [:3 ]
354368
355- return [helpers .entity_from_protobuf (entity , dataset = self .dataset ())
356- for entity in entity_pbs ]
369+ entities = [helpers .entity_from_protobuf (entity ,
370+ dataset = self .dataset ())
371+ for entity in entity_pbs ]
357372
358- def cursor (self ):
359- """Returns cursor ID from most recent ``fetch()``.
373+ cursor = base64 .b64encode (cursor_as_bytes )
360374
361- .. warning:: Invoking this method on a query that has not yet
362- been executed will raise a RuntimeError.
375+ if more_results_enum == self ._MORE_RESULTS :
376+ more_results = True
377+ elif more_results_enum == self ._NO_MORE_RESULTS :
378+ more_results = False
379+ else :
380+ # Note this covers the value NOT_FINISHED since this fetch does
381+ # not occur within a batch, we don't expect to see NOT_FINISHED.
382+ raise ValueError ('Unexpected value returned for `more_results`.' )
363383
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 )
384+ return entities , cursor , more_results
371385
372- def more_results (self ):
373- """Returns ``more_results`` flag from most recent ``fetch()``.
386+ def fetch (self , limit = None ):
387+ """Executes the Query and returns matching entities
374388
375- .. warning:: Invoking this method on a query that has not yet
376- been executed will raise a RuntimeError.
389+ This calls `fetch_page()` but does not use the paging information.
377390
378- .. note::
391+ For example::
392+
393+ >>> from gcloud import datastore
394+ >>> dataset = datastore.get_dataset('dataset-id')
395+ >>> query = dataset.query('Person').filter('name', '=', 'Sally')
396+ >>> query.fetch()
397+ [<Entity object>, <Entity object>, ...]
398+ >>> query.fetch(1)
399+ [<Entity object>]
400+ >>> query.limit()
401+ None
379402
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 .
403+ :type limit: integer
404+ :param limit: An optional limit to apply temporarily to this query.
405+ That is, the Query itself won't be altered,
406+ but the limit will be applied to the query
407+ before it is executed .
385408
386- :rtype: :class:`gcloud.datastore.datastore_v1_pb2.
387- QueryResultBatch.MoreResultsType`
388- :returns: enumerated value: are there more results available.
409+ :rtype: list of :class:`gcloud.datastore.entity.Entity`'s
410+ :returns: The list of entities matching this query's criteria.
389411 """
390- if self ._more_results is None :
391- raise RuntimeError ('No results' )
392- return self ._more_results
412+ entities , _ , _ = self .fetch_page (limit = limit )
413+ return entities
414+
415+ @property
416+ def start_cursor (self ):
417+ """Property to encode start cursor bytes as base64."""
418+ if not self ._pb .HasField ('start_cursor' ):
419+ return None
420+
421+ start_as_bytes = self ._pb .start_cursor
422+ return base64 .b64encode (start_as_bytes )
423+
424+ @property
425+ def end_cursor (self ):
426+ """Property to encode end cursor bytes as base64."""
427+ if not self ._pb .HasField ('end_cursor' ):
428+ return None
429+
430+ end_as_bytes = self ._pb .end_cursor
431+ return base64 .b64encode (end_as_bytes )
393432
394433 def with_cursor (self , start_cursor , end_cursor = None ):
395434 """Specifies the starting / ending positions in a query's result set.
0 commit comments