Source code for gcloud.datastore.query
import copy
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
from gcloud.datastore import helpers
from gcloud.datastore.entity import Entity
# TODO: Figure out how to properly handle namespaces.
[docs]class Query(object):
"""A Query against the Cloud Datastore.
This class serves as an abstraction for creating
a query over data stored in the Cloud Datastore.
Each :class:`Query` object is immutable,
and a clone is returned whenever
any part of the query is modified::
>>> query = Query('MyKind')
>>> limited_query = query.limit(10)
>>> query.limit() == 10
False
>>> limited_query.limit() == 10
True
You typically won't construct a :class:`Query`
by initializing it like ``Query('MyKind', dataset=...)``
but instead use the helper
:func:`gcloud.datastore.dataset.Dataset.query` method
which generates a query that can be executed
without any additional work::
>>> from gcloud import datastore
>>> dataset = datastore.get_dataset('dataset-id', email, key_path)
>>> query = dataset.query('MyKind')
:type kind: string
:param kind: The kind to query.
:type dataset: :class:`gcloud.datastore.dataset.Dataset`
:param dataset: The dataset to query.
"""
OPERATORS = {
'<': datastore_pb.PropertyFilter.LESS_THAN,
'<=': datastore_pb.PropertyFilter.LESS_THAN_OR_EQUAL,
'>': datastore_pb.PropertyFilter.GREATER_THAN,
'>=': datastore_pb.PropertyFilter.GREATER_THAN_OR_EQUAL,
'=': datastore_pb.PropertyFilter.EQUAL,
}
"""Mapping of operator strings and their protobuf equivalents."""
def __init__(self, kind=None, dataset=None):
self._dataset = dataset
self._pb = datastore_pb.Query()
if kind:
self._pb.kind.add().name = kind
def _clone(self):
# TODO(jjg): Double check that this makes sense...
clone = copy.deepcopy(self)
clone._dataset = self._dataset # Shallow copy the dataset.
return clone
[docs] def to_protobuf(self):
"""Convert the :class:`Query` instance to a :class:`gcloud.datastore.datastore_v1_pb2.Query`.
:rtype: :class:`gclouddatstore.datastore_v1_pb2.Query`
:returns: A Query protobuf that can be sent to the protobuf API.
"""
return self._pb
[docs] def filter(self, expression, value):
"""Filter the query based on an expression and a value.
This will return a clone of the current :class:`Query`
filtered by the expression and value provided.
Expressions take the form of::
.filter('<property> <operator>', <value>)
where property is a property stored on the entity in the datastore
and operator is one of ``OPERATORS``
(ie, ``=``, ``<``, ``<=``, ``>``, ``>=``)::
>>> query = Query('Person')
>>> filtered_query = query.filter('name =', 'James')
>>> filtered_query = query.filter('age >', 50)
Because each call to ``.filter()`` returns a cloned ``Query`` object
we are able to string these together::
>>> query = Query('Person').filter('name =', 'James').filter('age >', 50)
:type expression: string
:param expression: An expression of a property and an operator (ie, ``=``).
:type value: integer, string, boolean, float, None, datetime
:param value: The value to filter on.
:rtype: :class:`Query`
:returns: A Query filtered by the expression and value provided.
"""
clone = self._clone()
# Take an expression like 'property >=', and parse it into useful pieces.
property_name, operator = None, None
expression = expression.strip()
for operator_string in self.OPERATORS:
if expression.endswith(operator_string):
operator = self.OPERATORS[operator_string]
property_name = expression[0:-len(operator_string)].strip()
if not operator or not property_name:
raise ValueError('Invalid expression: "%s"' % expression)
# Build a composite filter AND'd together.
composite_filter = clone._pb.filter.composite_filter
composite_filter.operator = datastore_pb.CompositeFilter.AND
# Add the specific filter
property_filter = composite_filter.filter.add().property_filter
property_filter.property.name = property_name
property_filter.operator = operator
# Set the value to filter on based on the type.
attr_name, pb_value = helpers.get_protobuf_attribute_and_value(value)
setattr(property_filter.value, attr_name, pb_value)
return clone
[docs] def kind(self, *kinds):
"""Get or set the Kind of the Query.
.. note::
This is an **additive** operation.
That is, if the Query is set for kinds A and B,
and you call ``.kind('C')``,
it will query for kinds A, B, *and*, C.
:type kinds: string
:param kinds: The entity kinds for which to query.
:rtype: string or :class:`Query`
:returns: If no arguments, returns the kind.
If a kind is provided, returns a clone of the :class:`Query`
with those kinds set.
"""
# TODO: Do we want this to be additive?
# If not, clear the _pb.kind attribute.
if kinds:
clone = self._clone()
for kind in kinds:
clone._pb.kind.add().name = kind
return clone
else:
return self._pb.kind
[docs] def limit(self, limit=None):
"""Get or set the limit of the Query.
This is the maximum number of rows (Entities) to return for this Query.
This is a hybrid getter / setter, used as::
>>> query = Query('Person')
>>> query = query.limit(100) # Set the limit to 100 rows.
>>> query.limit() # Get the limit for this query.
100
:rtype: integer, None, or :class:`Query`
:returns: If no arguments, returns the current limit.
If a limit is provided, returns a clone of the :class:`Query`
with that limit set.
"""
if limit:
clone = self._clone()
clone._pb.limit = limit
return clone
else:
return self._pb.limit
[docs] def dataset(self, dataset=None):
"""Get or set the :class:`gcloud.datastore.dataset.Dataset` for this Query.
This is the dataset against which the Query will be run.
This is a hybrid getter / setter, used as::
>>> query = Query('Person')
>>> query = query.dataset(my_dataset) # Set the dataset.
>>> query.dataset() # Get the current dataset.
<Dataset object>
:rtype: :class:`gcloud.datastore.dataset.Dataset`, None, or :class:`Query`
:returns: If no arguments, returns the current dataset.
If a dataset is provided, returns a clone of the :class:`Query`
with that dataset set.
"""
if dataset:
clone = self._clone()
clone._dataset = dataset
return clone
else:
return self._dataset
[docs] def fetch(self, limit=None):
"""Executes the Query and returns all matching entities.
This makes an API call to the Cloud Datastore,
sends the Query as a protobuf,
parses the responses to Entity protobufs,
and then converts them to :class:`gcloud.datastore.entity.Entity` objects.
For example::
>>> from gcloud import datastore
>>> dataset = datastore.get_dataset('dataset-id', email, key_path)
>>> query = dataset.query('Person').filter('name =', 'Sally')
>>> query.fetch()
[<Entity object>, <Entity object>, ...]
>>> query.fetch(1)
[<Entity object>]
>>> query.limit()
None
:type limit: integer
:param limit: An optional limit to apply temporarily to this query.
That is, the Query itself won't be altered,
but the limit will be applied to the query
before it is executed.
:rtype: list of :class:`gcloud.datastore.entity.Entity`'s
:returns: The list of entities matching this query's criteria.
"""
clone = self
if limit:
clone = self.limit(limit)
entity_pbs = self.dataset().connection().run_query(
query_pb=clone.to_protobuf(), dataset_id=self.dataset().id())
return [Entity.from_protobuf(entity) for entity in entity_pbs]