summaryrefslogtreecommitdiffstats
path: root/google_appengine/google/appengine/api/datastore.py
diff options
context:
space:
mode:
Diffstat (limited to 'google_appengine/google/appengine/api/datastore.py')
-rwxr-xr-xgoogle_appengine/google/appengine/api/datastore.py2170
1 files changed, 2170 insertions, 0 deletions
diff --git a/google_appengine/google/appengine/api/datastore.py b/google_appengine/google/appengine/api/datastore.py
new file mode 100755
index 0000000..6931db8
--- /dev/null
+++ b/google_appengine/google/appengine/api/datastore.py
@@ -0,0 +1,2170 @@
+#!/usr/bin/env python
+#
+# Copyright 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""The Python datastore API used by app developers.
+
+Defines Entity, Query, and Iterator classes, as well as methods for all of the
+datastore's calls. Also defines conversions between the Python classes and
+their PB counterparts.
+
+The datastore errors are defined in the datastore_errors module. That module is
+only required to avoid circular imports. datastore imports datastore_types,
+which needs BadValueError, so it can't be defined in datastore.
+"""
+
+
+
+
+
+
+import heapq
+import itertools
+import logging
+import re
+import string
+import sys
+import traceback
+from xml.sax import saxutils
+
+from google.appengine.api import api_base_pb
+from google.appengine.api import apiproxy_stub_map
+from google.appengine.api import datastore_errors
+from google.appengine.api import datastore_types
+from google.appengine.datastore import datastore_index
+from google.appengine.datastore import datastore_pb
+from google.appengine.runtime import apiproxy_errors
+from google.appengine.datastore import entity_pb
+
+try:
+ from google.appengine.api.labs.taskqueue import taskqueue_service_pb
+except ImportError:
+ from google.appengine.api.taskqueue import taskqueue_service_pb
+
+MAX_ALLOWABLE_QUERIES = 30
+
+DEFAULT_TRANSACTION_RETRIES = 3
+
+_MAX_INDEXED_PROPERTIES = 5000
+
+_MAX_ID_BATCH_SIZE = 1000 * 1000 * 1000
+
+Key = datastore_types.Key
+typename = datastore_types.typename
+
+_txes = {}
+
+
+def NormalizeAndTypeCheck(arg, types):
+ """Normalizes and type checks the given argument.
+
+ Args:
+ arg: an instance, tuple, list, iterator, or generator of the given type(s)
+ types: allowed type or tuple of types
+
+ Returns:
+ A (list, bool) tuple. The list is a normalized, shallow copy of the
+ argument. The boolean is True if the argument was a sequence, False
+ if it was a single object.
+
+ Raises:
+ AssertionError: types includes list or tuple.
+ BadArgumentError: arg is not an instance or sequence of one of the given
+ types.
+ """
+ if not isinstance(types, (list, tuple)):
+ types = (types,)
+
+ assert list not in types and tuple not in types
+
+ if isinstance(arg, types):
+ return ([arg], False)
+ else:
+ try:
+ for val in arg:
+ if not isinstance(val, types):
+ raise datastore_errors.BadArgumentError(
+ 'Expected one of %s; received %s (a %s).' %
+ (types, val, typename(val)))
+ except TypeError:
+ raise datastore_errors.BadArgumentError(
+ 'Expected an instance or sequence of %s; received %s (a %s).' %
+ (types, arg, typename(arg)))
+
+ return (list(arg), True)
+
+
+def NormalizeAndTypeCheckKeys(keys):
+ """Normalizes and type checks that the given argument is a valid key or keys.
+
+ A wrapper around NormalizeAndTypeCheck() that accepts strings, Keys, and
+ Entities, and normalizes to Keys.
+
+ Args:
+ keys: a Key or sequence of Keys
+
+ Returns:
+ A (list of Keys, bool) tuple. See NormalizeAndTypeCheck.
+
+ Raises:
+ BadArgumentError: arg is not an instance or sequence of one of the given
+ types.
+ """
+ keys, multiple = NormalizeAndTypeCheck(keys, (basestring, Entity, Key))
+
+ keys = [_GetCompleteKeyOrError(key) for key in keys]
+
+ return (keys, multiple)
+
+
+def Put(entities):
+ """Store one or more entities in the datastore.
+
+ The entities may be new or previously existing. For new entities, Put() will
+ fill in the app id and key assigned by the datastore.
+
+ If the argument is a single Entity, a single Key will be returned. If the
+ argument is a list of Entity, a list of Keys will be returned.
+
+ Args:
+ entities: Entity or list of Entities
+
+ Returns:
+ Key or list of Keys
+
+ Raises:
+ TransactionFailedError, if the Put could not be committed.
+ """
+ entities, multiple = NormalizeAndTypeCheck(entities, Entity)
+
+ if multiple and not entities:
+ return []
+
+ for entity in entities:
+ if not entity.kind() or not entity.app_id_namespace():
+ raise datastore_errors.BadRequestError(
+ 'App and kind must not be empty, in entity: %s' % entity)
+
+ req = datastore_pb.PutRequest()
+ req.entity_list().extend([e._ToPb() for e in entities])
+
+ keys = [e.key() for e in entities]
+ tx = _MaybeSetupTransaction(req, keys)
+
+ resp = datastore_pb.PutResponse()
+ try:
+ apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Put', req, resp)
+ except apiproxy_errors.ApplicationError, err:
+ raise _ToDatastoreError(err)
+
+ keys = resp.key_list()
+ num_keys = len(keys)
+ num_entities = len(entities)
+ if num_keys != num_entities:
+ raise datastore_errors.InternalError(
+ 'Put accepted %d entities but returned %d keys.' %
+ (num_entities, num_keys))
+
+ for entity, key in zip(entities, keys):
+ entity._Entity__key._Key__reference.CopyFrom(key)
+
+ if tx:
+ tx.entity_group = entities[0].entity_group()
+
+ if multiple:
+ return [Key._FromPb(k) for k in keys]
+ else:
+ return Key._FromPb(resp.key(0))
+
+
+def Get(keys):
+ """Retrieves one or more entities from the datastore.
+
+ Retrieves the entity or entities with the given key(s) from the datastore
+ and returns them as fully populated Entity objects, as defined below. If
+ there is an error, raises a subclass of datastore_errors.Error.
+
+ If keys is a single key or string, an Entity will be returned, or
+ EntityNotFoundError will be raised if no existing entity matches the key.
+
+ However, if keys is a list or tuple, a list of entities will be returned
+ that corresponds to the sequence of keys. It will include entities for keys
+ that were found and None placeholders for keys that were not found.
+
+ Args:
+ # the primary key(s) of the entity(ies) to retrieve
+ keys: Key or string or list of Keys or strings
+
+ Returns:
+ Entity or list of Entity objects
+ """
+ keys, multiple = NormalizeAndTypeCheckKeys(keys)
+
+ if multiple and not keys:
+ return []
+ req = datastore_pb.GetRequest()
+ req.key_list().extend([key._Key__reference for key in keys])
+ _MaybeSetupTransaction(req, keys)
+
+ resp = datastore_pb.GetResponse()
+ try:
+ apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Get', req, resp)
+ except apiproxy_errors.ApplicationError, err:
+ raise _ToDatastoreError(err)
+
+ entities = []
+ for group in resp.entity_list():
+ if group.has_entity():
+ entities.append(Entity._FromPb(group.entity()))
+ else:
+ entities.append(None)
+
+ if multiple:
+ return entities
+ else:
+ if entities[0] is None:
+ raise datastore_errors.EntityNotFoundError()
+ return entities[0]
+
+
+def Delete(keys):
+ """Deletes one or more entities from the datastore. Use with care!
+
+ Deletes the given entity(ies) from the datastore. You can only delete
+ entities from your app. If there is an error, raises a subclass of
+ datastore_errors.Error.
+
+ Args:
+ # the primary key(s) of the entity(ies) to delete
+ keys: Key or string or list of Keys or strings
+
+ Raises:
+ TransactionFailedError, if the Delete could not be committed.
+ """
+ keys, multiple = NormalizeAndTypeCheckKeys(keys)
+
+ if multiple and not keys:
+ return
+
+ req = datastore_pb.DeleteRequest()
+ req.key_list().extend([key._Key__reference for key in keys])
+
+ tx = _MaybeSetupTransaction(req, keys)
+
+ resp = datastore_pb.DeleteResponse()
+ try:
+ apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Delete', req, resp)
+ except apiproxy_errors.ApplicationError, err:
+ raise _ToDatastoreError(err)
+
+
+class Entity(dict):
+ """A datastore entity.
+
+ Includes read-only accessors for app id, kind, and primary key. Also
+ provides dictionary-style access to properties.
+ """
+ def __init__(self, kind, parent=None, _app=None, name=None, id=None,
+ unindexed_properties=[], _namespace=None):
+ """Constructor. Takes the kind and transaction root, which cannot be
+ changed after the entity is constructed, and an optional parent. Raises
+ BadArgumentError or BadKeyError if kind is invalid or parent is not an
+ existing Entity or Key in the datastore.
+
+ Args:
+ # this entity's kind
+ kind: string
+ # if provided, this entity's parent. Its key must be complete.
+ parent: Entity or Key
+ # if provided, this entity's name.
+ name: string
+ # if provided, this entity's id.
+ id: integer
+ # if provided, a sequence of property names that should not be indexed
+ # by the built-in single property indices.
+ unindexed_properties: list or tuple of strings
+ """
+ ref = entity_pb.Reference()
+ _app_namespace = datastore_types.ResolveAppIdNamespace(_app, _namespace)
+ ref.set_app(_app_namespace.to_encoded())
+
+ datastore_types.ValidateString(kind, 'kind',
+ datastore_errors.BadArgumentError)
+ if parent is not None:
+ parent = _GetCompleteKeyOrError(parent)
+ if _app_namespace != parent.app_id_namespace():
+ raise datastore_errors.BadArgumentError(
+ " %s doesn't match parent's app_namespace %s" %
+ (_app_namespace, parent.app_id_namespace()))
+ ref.CopyFrom(parent._Key__reference)
+
+ last_path = ref.mutable_path().add_element()
+ last_path.set_type(kind.encode('utf-8'))
+
+ if name is not None and id is not None:
+ raise datastore_errors.BadArgumentError(
+ "Cannot set both name and id on an Entity")
+
+ if name is not None:
+ datastore_types.ValidateString(name, 'name')
+ last_path.set_name(name.encode('utf-8'))
+
+ if id is not None:
+ datastore_types.ValidateInteger(id, 'id')
+ last_path.set_id(id)
+
+ unindexed_properties, multiple = NormalizeAndTypeCheck(unindexed_properties, basestring)
+ if not multiple:
+ raise datastore_errors.BadArgumentError(
+ 'unindexed_properties must be a sequence; received %s (a %s).' %
+ (unindexed_properties, typename(unindexed_properties)))
+ for prop in unindexed_properties:
+ datastore_types.ValidateProperty(prop, None)
+ self.__unindexed_properties = frozenset(unindexed_properties)
+
+ self.__key = Key._FromPb(ref)
+
+ def app(self):
+ """Returns the name of the application that created this entity, a
+ string or None if not set.
+ """
+ return self.__key.app()
+
+ def namespace(self):
+ """Returns the namespace of this entity, a string or None.
+ """
+ return self.__key.namespace()
+
+ def app_id_namespace(self):
+ """Returns the AppIdNamespace of this entity or None if not set.
+ """
+ return self.__key.app_id_namespace()
+
+ def kind(self):
+ """Returns this entity's kind, a string.
+ """
+ return self.__key.kind()
+
+ def is_saved(self):
+ """Returns if this entity has been saved to the datastore
+ """
+ last_path = self.__key._Key__reference.path().element_list()[-1]
+ return ((last_path.has_name() ^ last_path.has_id()) and
+ self.__key.has_id_or_name())
+
+ def key(self):
+ """Returns this entity's primary key, a Key instance.
+ """
+ return self.__key
+
+ def parent(self):
+ """Returns this entity's parent, as a Key. If this entity has no parent,
+ returns None.
+ """
+ return self.key().parent()
+
+ def entity_group(self):
+ """Returns this entity's entity group as a Key.
+
+ Note that the returned Key will be incomplete if this is a a root entity
+ and its key is incomplete.
+ """
+ return self.key().entity_group()
+
+ def unindexed_properties(self):
+ """Returns this entity's unindexed properties, as a frozenset of strings."""
+ return getattr(self, '_Entity__unindexed_properties', [])
+
+ def __setitem__(self, name, value):
+ """Implements the [] operator. Used to set property value(s).
+
+ If the property name is the empty string or not a string, raises
+ BadPropertyError. If the value is not a supported type, raises
+ BadValueError.
+ """
+ datastore_types.ValidateProperty(name, value)
+ dict.__setitem__(self, name, value)
+
+ def setdefault(self, name, value):
+ """If the property exists, returns its value. Otherwise sets it to value.
+
+ If the property name is the empty string or not a string, raises
+ BadPropertyError. If the value is not a supported type, raises
+ BadValueError.
+ """
+ datastore_types.ValidateProperty(name, value)
+ return dict.setdefault(self, name, value)
+
+ def update(self, other):
+ """Updates this entity's properties from the values in other.
+
+ If any property name is the empty string or not a string, raises
+ BadPropertyError. If any value is not a supported type, raises
+ BadValueError.
+ """
+ for name, value in other.items():
+ self.__setitem__(name, value)
+
+ def copy(self):
+ """The copy method is not supported.
+ """
+ raise NotImplementedError('Entity does not support the copy() method.')
+
+ def ToXml(self):
+ """Returns an XML representation of this entity. Atom and gd:namespace
+ properties are converted to XML according to their respective schemas. For
+ more information, see:
+
+ http://www.atomenabled.org/developers/syndication/
+ http://code.google.com/apis/gdata/common-elements.html
+
+ This is *not* optimized. It shouldn't be used anywhere near code that's
+ performance-critical.
+ """
+ xml = u'<entity kind=%s' % saxutils.quoteattr(self.kind())
+ if self.__key.has_id_or_name():
+ xml += ' key=%s' % saxutils.quoteattr(str(self.__key))
+ xml += '>'
+ if self.__key.has_id_or_name():
+ xml += '\n <key>%s</key>' % self.__key.ToTagUri()
+
+
+ properties = self.keys()
+ if properties:
+ properties.sort()
+ xml += '\n ' + '\n '.join(self._PropertiesToXml(properties))
+
+ xml += '\n</entity>\n'
+ return xml
+
+ def _PropertiesToXml(self, properties):
+ """ Returns a list of the XML representations of each of the given
+ properties. Ignores properties that don't exist in this entity.
+
+ Arg:
+ properties: string or list of strings
+
+ Returns:
+ list of strings
+ """
+ xml_properties = []
+
+ for propname in properties:
+ if not self.has_key(propname):
+ continue
+
+ propname_xml = saxutils.quoteattr(propname)
+
+ values = self[propname]
+ if not isinstance(values, list):
+ values = [values]
+
+ proptype = datastore_types.PropertyTypeName(values[0])
+ proptype_xml = saxutils.quoteattr(proptype)
+
+ escaped_values = self._XmlEscapeValues(propname)
+ open_tag = u'<property name=%s type=%s>' % (propname_xml, proptype_xml)
+ close_tag = u'</property>'
+ xml_properties += [open_tag + val + close_tag for val in escaped_values]
+
+ return xml_properties
+
+ def _XmlEscapeValues(self, property):
+ """ Returns a list of the XML-escaped string values for the given property.
+ Raises an AssertionError if the property doesn't exist.
+
+ Arg:
+ property: string
+
+ Returns:
+ list of strings
+ """
+ assert self.has_key(property)
+ xml = []
+
+ values = self[property]
+ if not isinstance(values, list):
+ values = [values]
+
+ for val in values:
+ if hasattr(val, 'ToXml'):
+ xml.append(val.ToXml())
+ else:
+ if val is None:
+ xml.append('')
+ else:
+ xml.append(saxutils.escape(unicode(val)))
+
+ return xml
+
+ def ToPb(self):
+ """Converts this Entity to its protocol buffer representation.
+
+ Returns:
+ entity_pb.Entity
+ """
+ return self._ToPb(False)
+
+ def _ToPb(self, mark_key_as_saved=True):
+ """Converts this Entity to its protocol buffer representation. Not
+ intended to be used by application developers.
+
+ Returns:
+ entity_pb.Entity
+ """
+
+ pb = entity_pb.EntityProto()
+ pb.mutable_key().CopyFrom(self.key()._ToPb())
+ last_path = pb.key().path().element_list()[-1]
+ if mark_key_as_saved and last_path.has_name() and last_path.has_id():
+ last_path.clear_id()
+
+ group = pb.mutable_entity_group()
+ if self.__key.has_id_or_name():
+ root = pb.key().path().element(0)
+ group.add_element().CopyFrom(root)
+
+ properties = self.items()
+ properties.sort()
+ for (name, values) in properties:
+ properties = datastore_types.ToPropertyPb(name, values)
+ if not isinstance(properties, list):
+ properties = [properties]
+
+ sample = values
+ if isinstance(sample, list):
+ sample = values[0]
+
+ if (isinstance(sample, datastore_types._RAW_PROPERTY_TYPES) or
+ name in self.unindexed_properties()):
+ pb.raw_property_list().extend(properties)
+ else:
+ pb.property_list().extend(properties)
+
+ if pb.property_size() > _MAX_INDEXED_PROPERTIES:
+ raise datastore_errors.BadRequestError(
+ 'Too many indexed properties for entity %r.' % self.key())
+
+ return pb
+
+ @staticmethod
+ def FromPb(pb):
+ """Static factory method. Returns the Entity representation of the
+ given protocol buffer (datastore_pb.Entity).
+
+ Args:
+ pb: datastore_pb.Entity or str encoding of a datastore_pb.Entity
+
+ Returns:
+ Entity: the Entity representation of pb
+ """
+ if isinstance(pb, str):
+ real_pb = entity_pb.EntityProto()
+ real_pb.ParseFromString(pb)
+ pb = real_pb
+
+ return Entity._FromPb(pb, require_valid_key=False)
+
+ @staticmethod
+ def _FromPb(pb, require_valid_key=True):
+ """Static factory method. Returns the Entity representation of the
+ given protocol buffer (datastore_pb.Entity). Not intended to be used by
+ application developers.
+
+ The Entity PB's key must be complete. If it isn't, an AssertionError is
+ raised.
+
+ Args:
+ # a protocol buffer Entity
+ pb: datastore_pb.Entity
+
+ Returns:
+ # the Entity representation of the argument
+ Entity
+ """
+ assert pb.key().path().element_size() > 0
+
+ last_path = pb.key().path().element_list()[-1]
+ if require_valid_key:
+ assert last_path.has_id() ^ last_path.has_name()
+ if last_path.has_id():
+ assert last_path.id() != 0
+ else:
+ assert last_path.has_name()
+ assert last_path.name()
+
+ unindexed_properties = [p.name() for p in pb.raw_property_list()]
+
+ e = Entity(unicode(last_path.type().decode('utf-8')),
+ unindexed_properties=unindexed_properties)
+ ref = e.__key._Key__reference
+ ref.CopyFrom(pb.key())
+
+ temporary_values = {}
+
+ for prop_list in (pb.property_list(), pb.raw_property_list()):
+ for prop in prop_list:
+ try:
+ value = datastore_types.FromPropertyPb(prop)
+ except (AssertionError, AttributeError, TypeError, ValueError), e:
+ raise datastore_errors.Error(
+ 'Property %s is corrupt in the datastore. %s: %s' %
+ (e.__class__, prop.name(), e))
+
+ multiple = prop.multiple()
+ if multiple:
+ value = [value]
+
+ name = prop.name()
+ cur_value = temporary_values.get(name)
+ if cur_value is None:
+ temporary_values[name] = value
+ elif not multiple:
+ raise datastore_errors.Error(
+ 'Property %s is corrupt in the datastore; it has multiple '
+ 'values, but is not marked as multiply valued.' % name)
+ else:
+ cur_value.extend(value)
+
+ for name, value in temporary_values.iteritems():
+ decoded_name = unicode(name.decode('utf-8'))
+
+ datastore_types.ValidateReadProperty(decoded_name, value)
+
+ dict.__setitem__(e, decoded_name, value)
+
+ return e
+
+
+class Query(dict):
+ """A datastore query.
+
+ (Instead of this, consider using appengine.ext.gql.Query! It provides a
+ query language interface on top of the same functionality.)
+
+ Queries are used to retrieve entities that match certain criteria, including
+ app id, kind, and property filters. Results may also be sorted by properties.
+
+ App id and kind are required. Only entities from the given app, of the given
+ type, are returned. If an ancestor is set, with Ancestor(), only entities
+ with that ancestor are returned.
+
+ Property filters are used to provide criteria based on individual property
+ values. A filter compares a specific property in each entity to a given
+ value or list of possible values.
+
+ An entity is returned if its property values match *all* of the query's
+ filters. In other words, filters are combined with AND, not OR. If an
+ entity does not have a value for a property used in a filter, it is not
+ returned.
+
+ Property filters map filter strings of the form '<property name> <operator>'
+ to filter values. Use dictionary accessors to set property filters, like so:
+
+ > query = Query('Person')
+ > query['name ='] = 'Ryan'
+ > query['age >='] = 21
+
+ This query returns all Person entities where the name property is 'Ryan',
+ 'Ken', or 'Bret', and the age property is at least 21.
+
+ Another way to build this query is:
+
+ > query = Query('Person')
+ > query.update({'name =': 'Ryan', 'age >=': 21})
+
+ The supported operators are =, >, <, >=, and <=. Only one inequality
+ filter may be used per query. Any number of equals filters may be used in
+ a single Query.
+
+ A filter value may be a list or tuple of values. This is interpreted as
+ multiple filters with the same filter string and different values, all ANDed
+ together. For example, this query returns everyone with the tags "google"
+ and "app engine":
+
+ > Query('Person', {'tag =': ('google', 'app engine')})
+
+ Result entities can be returned in different orders. Use the Order()
+ method to specify properties that results will be sorted by, and in which
+ direction.
+
+ Note that filters and orderings may be provided at any time before the query
+ is run. When the query is fully specified, Run() runs the query and returns
+ an iterator. The query results can be accessed through the iterator.
+
+ A query object may be reused after it's been run. Its filters and
+ orderings can be changed to create a modified query.
+
+ If you know how many result entities you need, use Get() to fetch them:
+
+ > query = Query('Person', {'age >': 21})
+ > for person in query.Get(4):
+ > print 'I have four pints left. Have one on me, %s!' % person['name']
+
+ If you don't know how many results you need, or if you need them all, you
+ can get an iterator over the results by calling Run():
+
+ > for person in Query('Person', {'age >': 21}).Run():
+ > print 'Have a pint on me, %s!' % person['name']
+
+ Get() is more efficient than Run(), so use Get() whenever possible.
+
+ Finally, the Count() method returns the number of result entities matched by
+ the query. The returned count is cached; successive Count() calls will not
+ re-scan the datastore unless the query is changed.
+ """
+ ASCENDING = datastore_pb.Query_Order.ASCENDING
+ DESCENDING = datastore_pb.Query_Order.DESCENDING
+
+ ORDER_FIRST = datastore_pb.Query.ORDER_FIRST
+ ANCESTOR_FIRST = datastore_pb.Query.ANCESTOR_FIRST
+ FILTER_FIRST = datastore_pb.Query.FILTER_FIRST
+
+ OPERATORS = {'<': datastore_pb.Query_Filter.LESS_THAN,
+ '<=': datastore_pb.Query_Filter.LESS_THAN_OR_EQUAL,
+ '>': datastore_pb.Query_Filter.GREATER_THAN,
+ '>=': datastore_pb.Query_Filter.GREATER_THAN_OR_EQUAL,
+ '=': datastore_pb.Query_Filter.EQUAL,
+ '==': datastore_pb.Query_Filter.EQUAL,
+ }
+ INEQUALITY_OPERATORS = frozenset(['<', '<=', '>', '>='])
+ FILTER_REGEX = re.compile(
+ '^\s*([^\s]+)(\s+(%s)\s*)?$' % '|'.join(OPERATORS.keys()),
+ re.IGNORECASE | re.UNICODE)
+
+ __kind = None
+ __app = None
+ __orderings = None
+ __cached_count = None
+ __hint = None
+ __ancestor = None
+
+ __filter_order = None
+ __filter_counter = 0
+
+ __inequality_prop = None
+ __inequality_count = 0
+
+ def __init__(self, kind=None, filters={}, _app=None, keys_only=False,
+ _namespace=None):
+ """Constructor.
+
+ Raises BadArgumentError if kind is not a string. Raises BadValueError or
+ BadFilterError if filters is not a dictionary of valid filters.
+
+ Args:
+ # kind is required. filters is optional; if provided, it's used
+ # as an initial set of property filters. keys_only defaults to False.
+ kind: string
+ filters: dict
+ keys_only: boolean
+ """
+ if kind is not None:
+ datastore_types.ValidateString(kind, 'kind',
+ datastore_errors.BadArgumentError)
+
+ self.__kind = kind
+ self.__orderings = []
+ self.__filter_order = {}
+ self.update(filters)
+
+ self.__app = datastore_types.ResolveAppIdNamespace(_app,
+ _namespace).to_encoded()
+ self.__keys_only = keys_only
+
+ def Order(self, *orderings):
+ """Specify how the query results should be sorted.
+
+ Result entities will be sorted by the first property argument, then by the
+ second, and so on. For example, this:
+
+ > query = Query('Person')
+ > query.Order('bday', ('age', Query.DESCENDING))
+
+ sorts everyone in order of their birthday, starting with January 1.
+ People with the same birthday are sorted by age, oldest to youngest.
+
+ The direction for each sort property may be provided; if omitted, it
+ defaults to ascending.
+
+ Order() may be called multiple times. Each call resets the sort order
+ from scratch.
+
+ If an inequality filter exists in this Query it must be the first property
+ passed to Order. Any number of sort orders may be used after the
+ inequality filter property. Without inequality filters, any number of
+ filters with different orders may be specified.
+
+ Entities with multiple values for an order property are sorted by their
+ lowest value.
+
+ Note that a sort order implies an existence filter! In other words,
+ Entities without the sort order property are filtered out, and *not*
+ included in the query results.
+
+ If the sort order property has different types in different entities - ie,
+ if bob['id'] is an int and fred['id'] is a string - the entities will be
+ grouped first by the property type, then sorted within type. No attempt is
+ made to compare property values across types.
+
+ Raises BadArgumentError if any argument is of the wrong format.
+
+ Args:
+ # the properties to sort by, in sort order. each argument may be either a
+ # string or (string, direction) 2-tuple.
+
+ Returns:
+ # this query
+ Query
+ """
+ orderings = list(orderings)
+
+ for (order, i) in zip(orderings, range(len(orderings))):
+ if not (isinstance(order, basestring) or
+ (isinstance(order, tuple) and len(order) in [2, 3])):
+ raise datastore_errors.BadArgumentError(
+ 'Order() expects strings or 2- or 3-tuples; received %s (a %s). ' %
+ (order, typename(order)))
+
+ if isinstance(order, basestring):
+ order = (order,)
+
+ datastore_types.ValidateString(order[0], 'sort order property',
+ datastore_errors.BadArgumentError)
+ property = order[0]
+
+ direction = order[-1]
+ if direction not in (Query.ASCENDING, Query.DESCENDING):
+ if len(order) == 3:
+ raise datastore_errors.BadArgumentError(
+ 'Order() expects Query.ASCENDING or DESCENDING; received %s' %
+ str(direction))
+ direction = Query.ASCENDING
+
+ if (self.__kind is None and
+ (property != datastore_types._KEY_SPECIAL_PROPERTY or
+ direction != Query.ASCENDING)):
+ raise datastore_errors.BadArgumentError(
+ 'Only %s ascending orders are supported on kindless queries' %
+ datastore_types._KEY_SPECIAL_PROPERTY)
+
+ orderings[i] = (property, direction)
+
+ if (orderings and self.__inequality_prop and
+ orderings[0][0] != self.__inequality_prop):
+ raise datastore_errors.BadArgumentError(
+ 'First ordering property must be the same as inequality filter '
+ 'property, if specified for this query; received %s, expected %s' %
+ (orderings[0][0], self.__inequality_prop))
+
+ self.__orderings = orderings
+ return self
+
+ def Hint(self, hint):
+ """Sets a hint for how this query should run.
+
+ The query hint gives us information about how best to execute your query.
+ Currently, we can only do one index scan, so the query hint should be used
+ to indicates which index we should scan against.
+
+ Use FILTER_FIRST if your first filter will only match a few results. In
+ this case, it will be most efficient to scan against the index for this
+ property, load the results into memory, and apply the remaining filters
+ and sort orders there.
+
+ Similarly, use ANCESTOR_FIRST if the query's ancestor only has a few
+ descendants. In this case, it will be most efficient to scan all entities
+ below the ancestor and load them into memory first.
+
+ Use ORDER_FIRST if the query has a sort order and the result set is large
+ or you only plan to fetch the first few results. In that case, we
+ shouldn't try to load all of the results into memory; instead, we should
+ scan the index for this property, which is in sorted order.
+
+ Note that hints are currently ignored in the v3 datastore!
+
+ Arg:
+ one of datastore.Query.[ORDER_FIRST, ANCESTOR_FIRST, FILTER_FIRST]
+
+ Returns:
+ # this query
+ Query
+ """
+ if hint not in [self.ORDER_FIRST, self.ANCESTOR_FIRST, self.FILTER_FIRST]:
+ raise datastore_errors.BadArgumentError(
+ 'Query hint must be ORDER_FIRST, ANCESTOR_FIRST, or FILTER_FIRST.')
+
+ self.__hint = hint
+ return self
+
+ def Ancestor(self, ancestor):
+ """Sets an ancestor for this query.
+
+ This restricts the query to only return result entities that are descended
+ from a given entity. In other words, all of the results will have the
+ ancestor as their parent, or parent's parent, or etc.
+
+ Raises BadArgumentError or BadKeyError if parent is not an existing Entity
+ or Key in the datastore.
+
+ Args:
+ # the key must be complete
+ ancestor: Entity or Key
+
+ Returns:
+ # this query
+ Query
+ """
+ self.__ancestor = _GetCompleteKeyOrError(ancestor)
+ return self
+
+ def IsKeysOnly(self):
+ """Returns True if this query is keys only, false otherwise."""
+ return self.__keys_only
+
+ def Run(self):
+ """Runs this query.
+
+ If a filter string is invalid, raises BadFilterError. If a filter value is
+ invalid, raises BadValueError. If an IN filter is provided, and a sort
+ order on another property is provided, raises BadQueryError.
+
+ If you know in advance how many results you want, use Get() instead. It's
+ more efficient.
+
+ Returns:
+ # an iterator that provides access to the query results
+ Iterator
+ """
+ return self._Run()
+
+ def _Run(self, limit=None, offset=None,
+ prefetch_count=None, next_count=None):
+ """Runs this query, with an optional result limit and an optional offset.
+
+ Identical to Run, with the extra optional limit, offset, prefetch_count,
+ next_count parameters. These parameters must be integers >= 0.
+
+ This is not intended to be used by application developers. Use Get()
+ instead!
+ """
+ pb = self._ToPb(limit, offset, prefetch_count)
+ result = datastore_pb.QueryResult()
+
+ try:
+ apiproxy_stub_map.MakeSyncCall('datastore_v3', 'RunQuery', pb, result)
+ except apiproxy_errors.ApplicationError, err:
+ try:
+ _ToDatastoreError(err)
+ except datastore_errors.NeedIndexError, exc:
+ yaml = datastore_index.IndexYamlForQuery(
+ *datastore_index.CompositeIndexForQuery(pb)[1:-1])
+ raise datastore_errors.NeedIndexError(
+ str(exc) + '\nThis query needs this index:\n' + yaml)
+
+ return Iterator(result, batch_size=next_count)
+
+ def Get(self, limit, offset=0):
+ """Fetches and returns a maximum number of results from the query.
+
+ This method fetches and returns a list of resulting entities that matched
+ the query. If the query specified a sort order, entities are returned in
+ that order. Otherwise, the order is undefined.
+
+ The limit argument specifies the maximum number of entities to return. If
+ it's greater than the number of remaining entities, all of the remaining
+ entities are returned. In that case, the length of the returned list will
+ be smaller than limit.
+
+ The offset argument specifies the number of entities that matched the
+ query criteria to skip before starting to return results. The limit is
+ applied after the offset, so if you provide a limit of 10 and an offset of 5
+ and your query matches 20 records, the records whose index is 0 through 4
+ will be skipped and the records whose index is 5 through 14 will be
+ returned.
+
+ The results are always returned as a list. If there are no results left,
+ an empty list is returned.
+
+ If you know in advance how many results you want, this method is more
+ efficient than Run(), since it fetches all of the results at once. (The
+ datastore backend sets the the limit on the underlying
+ scan, which makes the scan significantly faster.)
+
+ Args:
+ # the maximum number of entities to return
+ int or long
+ # the number of entities to skip
+ int or long
+
+ Returns:
+ # a list of entities
+ [Entity, ...]
+ """
+ if not isinstance(limit, (int, long)) or limit <= 0:
+ raise datastore_errors.BadArgumentError(
+ 'Argument to Get named \'limit\' must be an int greater than 0; '
+ 'received %s (a %s)' % (limit, typename(limit)))
+
+ if not isinstance(offset, (int, long)) or offset < 0:
+ raise datastore_errors.BadArgumentError(
+ 'Argument to Get named \'offset\' must be an int greater than or '
+ 'equal to 0; received %s (a %s)' % (offset, typename(offset)))
+
+ return self._Run(limit=limit, offset=offset,
+ prefetch_count=limit)._Get(limit)
+
+ def Count(self, limit=None):
+ """Returns the number of entities that this query matches. The returned
+ count is cached; successive Count() calls will not re-scan the datastore
+ unless the query is changed.
+
+ Args:
+ limit, a number. If there are more results than this, stop short and
+ just return this number. Providing this argument makes the count
+ operation more efficient.
+ Returns:
+ The number of results.
+ """
+ if self.__cached_count:
+ return self.__cached_count
+
+ resp = api_base_pb.Integer64Proto()
+ try:
+ apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Count',
+ self._ToPb(limit=limit), resp)
+ except apiproxy_errors.ApplicationError, err:
+ raise _ToDatastoreError(err)
+ else:
+ self.__cached_count = resp.value()
+
+ return self.__cached_count
+
+ def __iter__(self):
+ raise NotImplementedError(
+ 'Query objects should not be used as iterators. Call Run() first.')
+
+ def __setitem__(self, filter, value):
+ """Implements the [] operator. Used to set filters.
+
+ If the filter string is empty or not a string, raises BadFilterError. If
+ the value is not a supported type, raises BadValueError.
+ """
+ if isinstance(value, tuple):
+ value = list(value)
+
+ datastore_types.ValidateProperty(' ', value, read_only=True)
+ match = self._CheckFilter(filter, value)
+ property = match.group(1)
+ operator = match.group(3)
+
+ dict.__setitem__(self, filter, value)
+
+ if operator in self.INEQUALITY_OPERATORS:
+ if self.__inequality_prop is None:
+ self.__inequality_prop = property
+ else:
+ assert self.__inequality_prop == property
+ self.__inequality_count += 1
+
+ if filter not in self.__filter_order:
+ self.__filter_order[filter] = self.__filter_counter
+ self.__filter_counter += 1
+
+ self.__cached_count = None
+
+ def setdefault(self, filter, value):
+ """If the filter exists, returns its value. Otherwise sets it to value.
+
+ If the property name is the empty string or not a string, raises
+ BadPropertyError. If the value is not a supported type, raises
+ BadValueError.
+ """
+ datastore_types.ValidateProperty(' ', value)
+ self._CheckFilter(filter, value)
+ self.__cached_count = None
+ return dict.setdefault(self, filter, value)
+
+ def __delitem__(self, filter):
+ """Implements the del [] operator. Used to remove filters.
+ """
+ dict.__delitem__(self, filter)
+ del self.__filter_order[filter]
+ self.__cached_count = None
+
+ match = Query.FILTER_REGEX.match(filter)
+ property = match.group(1)
+ operator = match.group(3)
+
+ if operator in self.INEQUALITY_OPERATORS:
+ assert self.__inequality_count >= 1
+ assert property == self.__inequality_prop
+ self.__inequality_count -= 1
+ if self.__inequality_count == 0:
+ self.__inequality_prop = None
+
+ def update(self, other):
+ """Updates this query's filters from the ones in other.
+
+ If any filter string is invalid, raises BadFilterError. If any value is
+ not a supported type, raises BadValueError.
+ """
+ for filter, value in other.items():
+ self.__setitem__(filter, value)
+
+ def copy(self):
+ """The copy method is not supported.
+ """
+ raise NotImplementedError('Query does not support the copy() method.')
+
+ def _CheckFilter(self, filter, values):
+ """Type check a filter string and list of values.
+
+ Raises BadFilterError if the filter string is empty, not a string, or
+ invalid. Raises BadValueError if the value type is not supported.
+
+ Args:
+ filter: String containing the filter text.
+ values: List of associated filter values.
+
+ Returns:
+ re.MatchObject (never None) that matches the 'filter'. Group 1 is the
+ property name, group 3 is the operator. (Group 2 is unused.)
+ """
+ try:
+ match = Query.FILTER_REGEX.match(filter)
+ if not match:
+ raise datastore_errors.BadFilterError(
+ 'Could not parse filter string: %s' % str(filter))
+ except TypeError:
+ raise datastore_errors.BadFilterError(
+ 'Could not parse filter string: %s' % str(filter))
+
+ property = match.group(1)
+ operator = match.group(3)
+ if operator is None:
+ operator = '='
+
+ if isinstance(values, tuple):
+ values = list(values)
+ elif not isinstance(values, list):
+ values = [values]
+ if isinstance(values[0], datastore_types._RAW_PROPERTY_TYPES):
+ raise datastore_errors.BadValueError(
+ 'Filtering on %s properties is not supported.' % typename(values[0]))
+
+ if operator in self.INEQUALITY_OPERATORS:
+ if self.__inequality_prop and property != self.__inequality_prop:
+ raise datastore_errors.BadFilterError(
+ 'Only one property per query may have inequality filters (%s).' %
+ ', '.join(self.INEQUALITY_OPERATORS))
+ elif len(self.__orderings) >= 1 and self.__orderings[0][0] != property:
+ raise datastore_errors.BadFilterError(
+ 'Inequality operators (%s) must be on the same property as the '
+ 'first sort order, if any sort orders are supplied' %
+ ', '.join(self.INEQUALITY_OPERATORS))
+
+ if (self.__kind is None and
+ property != datastore_types._KEY_SPECIAL_PROPERTY):
+ raise datastore_errors.BadFilterError(
+ 'Only %s filters are allowed on kindless queries.' %
+ datastore_types._KEY_SPECIAL_PROPERTY)
+
+ if property in datastore_types._SPECIAL_PROPERTIES:
+ if property == datastore_types._KEY_SPECIAL_PROPERTY:
+ for value in values:
+ if not isinstance(value, Key):
+ raise datastore_errors.BadFilterError(
+ '%s filter value must be a Key; received %s (a %s)' %
+ (datastore_types._KEY_SPECIAL_PROPERTY, value, typename(value)))
+
+ return match
+
+ def _ToPb(self, limit=None, offset=None, count=None):
+ """Converts this Query to its protocol buffer representation. Not
+ intended to be used by application developers. Enforced by hiding the
+ datastore_pb classes.
+
+ Args:
+ # an upper bound on the number of results returned by the query.
+ limit: int
+ # number of results that match the query to skip. limit is applied
+ # after the offset is fulfilled
+ offset: int
+ # the requested initial batch size
+ count: int
+
+ Returns:
+ # the PB representation of this Query
+ datastore_pb.Query
+
+ Raises:
+ BadRequestError if called inside a transaction and the query does not
+ include an ancestor.
+ """
+
+ if not self.__ancestor and _CurrentTransactionKey():
+ raise datastore_errors.BadRequestError(
+ 'Only ancestor queries are allowed inside transactions.')
+
+ pb = datastore_pb.Query()
+ _MaybeSetupTransaction(pb, [self.__ancestor])
+
+ if self.__kind is not None:
+ pb.set_kind(self.__kind.encode('utf-8'))
+ pb.set_keys_only(bool(self.__keys_only))
+ if self.__app:
+ pb.set_app(self.__app.encode('utf-8'))
+ if limit is not None:
+ pb.set_limit(limit)
+ if offset is not None:
+ pb.set_offset(offset)
+ if count is not None:
+ pb.set_count(count)
+ if self.__ancestor:
+ pb.mutable_ancestor().CopyFrom(self.__ancestor._Key__reference)
+
+ if ((self.__hint == self.ORDER_FIRST and self.__orderings) or
+ (self.__hint == self.ANCESTOR_FIRST and self.__ancestor) or
+ (self.__hint == self.FILTER_FIRST and len(self) > 0)):
+ pb.set_hint(self.__hint)
+
+ ordered_filters = [(i, f) for f, i in self.__filter_order.iteritems()]
+ ordered_filters.sort()
+
+ for i, filter_str in ordered_filters:
+ if filter_str not in self:
+ continue
+
+ values = self[filter_str]
+ match = self._CheckFilter(filter_str, values)
+ name = match.group(1)
+
+ props = datastore_types.ToPropertyPb(name, values)
+ if not isinstance(props, list):
+ props = [props]
+
+ op = match.group(3)
+ if op is None:
+ op = '='
+
+ for prop in props:
+ filter = pb.add_filter()
+ filter.set_op(self.OPERATORS[op])
+ filter.add_property().CopyFrom(prop)
+
+ for property, direction in self.__orderings:
+ order = pb.add_order()
+ order.set_property(property.encode('utf-8'))
+ order.set_direction(direction)
+
+ return pb
+
+
+def AllocateIds(model_key, size):
+ """Allocates a range of IDs of size for the key defined by model_key
+
+ Allocates a range of IDs in the datastore such that those IDs will not
+ be automatically assigned to new entities. You can only allocate IDs
+ for model keys from your app. If there is an error, raises a subclass of
+ datastore_errors.Error.
+
+ Args:
+ model_key: Key or string to serve as a model specifying the ID sequence
+ in which to allocate IDs
+
+ Returns:
+ (start, end) of the allocated range, inclusive.
+ """
+ keys, multiple = NormalizeAndTypeCheckKeys(model_key)
+
+ if len(keys) > 1:
+ raise datastore_errors.BadArgumentError(
+ 'Cannot allocate IDs for more than one model key at a time')
+
+ if size > _MAX_ID_BATCH_SIZE:
+ raise datastore_errors.BadArgumentError(
+ 'Cannot allocate more than %s ids at a time' % _MAX_ID_BATCH_SIZE)
+
+ req = datastore_pb.AllocateIdsRequest()
+ req.mutable_model_key().CopyFrom(keys[0]._Key__reference)
+ req.set_size(size)
+
+ resp = datastore_pb.AllocateIdsResponse()
+ try:
+ apiproxy_stub_map.MakeSyncCall('datastore_v3', 'AllocateIds', req, resp)
+ except apiproxy_errors.ApplicationError, err:
+ raise _ToDatastoreError(err)
+
+ return resp.start(), resp.end()
+
+
+class MultiQuery(Query):
+ """Class representing a query which requires multiple datastore queries.
+
+ This class is actually a subclass of datastore.Query as it is intended to act
+ like a normal Query object (supporting the same interface).
+
+ Does not support keys only queries, since it needs whole entities in order
+ to merge sort them. (That's not true if there are no sort orders, or if the
+ sort order is on __key__, but allowing keys only queries in those cases, but
+ not in others, would be confusing.)
+ """
+
+ def __init__(self, bound_queries, orderings):
+ if len(bound_queries) > MAX_ALLOWABLE_QUERIES:
+ raise datastore_errors.BadArgumentError(
+ 'Cannot satisfy query -- too many subqueries (max: %d, got %d).'
+ ' Probable cause: too many IN/!= filters in query.' %
+ (MAX_ALLOWABLE_QUERIES, len(bound_queries)))
+
+ for query in bound_queries:
+ if query.IsKeysOnly():
+ raise datastore_errors.BadQueryError(
+ 'MultiQuery does not support keys_only.')
+
+ self.__bound_queries = bound_queries
+ self.__orderings = orderings
+
+ def __str__(self):
+ res = 'MultiQuery: '
+ for query in self.__bound_queries:
+ res = '%s %s' % (res, str(query))
+ return res
+
+ def Get(self, limit, offset=0):
+ """Get results of the query with a limit on the number of results.
+
+ Args:
+ limit: maximum number of values to return.
+ offset: offset requested -- if nonzero, this will override the offset in
+ the original query
+
+ Returns:
+ A list of entities with at most "limit" entries (less if the query
+ completes before reading limit values).
+ """
+ count = 1
+ result = []
+
+ iterator = self.Run()
+
+ try:
+ for i in xrange(offset):
+ val = iterator.next()
+ except StopIteration:
+ pass
+
+ try:
+ while count <= limit:
+ val = iterator.next()
+ result.append(val)
+ count += 1
+ except StopIteration:
+ pass
+ return result
+
+ class SortOrderEntity(object):
+ """Allow entity comparisons using provided orderings.
+
+ The iterator passed to the constructor is eventually consumed via
+ calls to GetNext(), which generate new SortOrderEntity s with the
+ same orderings.
+ """
+
+ def __init__(self, entity_iterator, orderings):
+ """Ctor.
+
+ Args:
+ entity_iterator: an iterator of entities which will be wrapped.
+ orderings: an iterable of (identifier, order) pairs. order
+ should be either Query.ASCENDING or Query.DESCENDING.
+ """
+ self.__entity_iterator = entity_iterator
+ self.__entity = None
+ self.__min_max_value_cache = {}
+ try:
+ self.__entity = entity_iterator.next()
+ except StopIteration:
+ pass
+ else:
+ self.__orderings = orderings
+
+ def __str__(self):
+ return str(self.__entity)
+
+ def GetEntity(self):
+ """Gets the wrapped entity."""
+ return self.__entity
+
+ def GetNext(self):
+ """Wrap and return the next entity.
+
+ The entity is retrieved from the iterator given at construction time.
+ """
+ return MultiQuery.SortOrderEntity(self.__entity_iterator,
+ self.__orderings)
+
+ def CmpProperties(self, that):
+ """Compare two entities and return their relative order.
+
+ Compares self to that based on the current sort orderings and the
+ key orders between them. Returns negative, 0, or positive depending on
+ whether self is less, equal to, or greater than that. This
+ comparison returns as if all values were to be placed in ascending order
+ (highest value last). Only uses the sort orderings to compare (ignores
+ keys).
+
+ Args:
+ that: SortOrderEntity
+
+ Returns:
+ Negative if self < that
+ Zero if self == that
+ Positive if self > that
+ """
+ if not self.__entity:
+ return cmp(self.__entity, that.__entity)
+
+ for (identifier, order) in self.__orderings:
+ value1 = self.__GetValueForId(self, identifier, order)
+ value2 = self.__GetValueForId(that, identifier, order)
+
+ result = cmp(value1, value2)
+ if order == Query.DESCENDING:
+ result = -result
+ if result:
+ return result
+ return 0
+
+ def __GetValueForId(self, sort_order_entity, identifier, sort_order):
+ value = _GetPropertyValue(sort_order_entity.__entity, identifier)
+ entity_key = sort_order_entity.__entity.key()
+ if (entity_key, identifier) in self.__min_max_value_cache:
+ value = self.__min_max_value_cache[(entity_key, identifier)]
+ elif isinstance(value, list):
+ if sort_order == Query.DESCENDING:
+ value = min(value)
+ else:
+ value = max(value)
+ self.__min_max_value_cache[(entity_key, identifier)] = value
+
+ return value
+
+ def __cmp__(self, that):
+ """Compare self to that w.r.t. values defined in the sort order.
+
+ Compare an entity with another, using sort-order first, then the key
+ order to break ties. This can be used in a heap to have faster min-value
+ lookup.
+
+ Args:
+ that: other entity to compare to
+ Returns:
+ negative: if self is less than that in sort order
+ zero: if self is equal to that in sort order
+ positive: if self is greater than that in sort order
+ """
+ property_compare = self.CmpProperties(that)
+ if property_compare:
+ return property_compare
+ else:
+ return cmp(self.__entity.key(), that.__entity.key())
+
+ def Run(self):
+ """Return an iterable output with all results in order."""
+ results = []
+ count = 1
+ log_level = logging.DEBUG - 1
+ for bound_query in self.__bound_queries:
+ logging.log(log_level, 'Running query #%i' % count)
+ results.append(bound_query.Run())
+ count += 1
+
+ def IterateResults(results):
+ """Iterator function to return all results in sorted order.
+
+ Iterate over the array of results, yielding the next element, in
+ sorted order. This function is destructive (results will be empty
+ when the operation is complete).
+
+ Args:
+ results: list of result iterators to merge and iterate through
+
+ Yields:
+ The next result in sorted order.
+ """
+ result_heap = []
+ for result in results:
+ heap_value = MultiQuery.SortOrderEntity(result, self.__orderings)
+ if heap_value.GetEntity():
+ heapq.heappush(result_heap, heap_value)
+
+ used_keys = set()
+
+ while result_heap:
+ top_result = heapq.heappop(result_heap)
+
+ results_to_push = []
+ if top_result.GetEntity().key() not in used_keys:
+ yield top_result.GetEntity()
+ else:
+ pass
+
+ used_keys.add(top_result.GetEntity().key())
+
+ results_to_push = []
+ while result_heap:
+ next = heapq.heappop(result_heap)
+ if cmp(top_result, next):
+ results_to_push.append(next)
+ break
+ else:
+ results_to_push.append(next.GetNext())
+ results_to_push.append(top_result.GetNext())
+
+ for popped_result in results_to_push:
+ if popped_result.GetEntity():
+ heapq.heappush(result_heap, popped_result)
+
+ return IterateResults(results)
+
+ def Count(self, limit=None):
+ """Return the number of matched entities for this query.
+
+ Will return the de-duplicated count of results. Will call the more
+ efficient Get() function if a limit is given.
+
+ Args:
+ limit: maximum number of entries to count (for any result > limit, return
+ limit).
+ Returns:
+ count of the number of entries returned.
+ """
+ if limit is None:
+ count = 0
+ for i in self.Run():
+ count += 1
+ return count
+ else:
+ return len(self.Get(limit))
+
+ def __setitem__(self, query_filter, value):
+ """Add a new filter by setting it on all subqueries.
+
+ If any of the setting operations raise an exception, the ones
+ that succeeded are undone and the exception is propagated
+ upward.
+
+ Args:
+ query_filter: a string of the form "property operand".
+ value: the value that the given property is compared against.
+ """
+ saved_items = []
+ for index, query in enumerate(self.__bound_queries):
+ saved_items.append(query.get(query_filter, None))
+ try:
+ query[query_filter] = value
+ except:
+ for q, old_value in itertools.izip(self.__bound_queries[:index],
+ saved_items):
+ if old_value is not None:
+ q[query_filter] = old_value
+ else:
+ del q[query_filter]
+ raise
+
+ def __delitem__(self, query_filter):
+ """Delete a filter by deleting it from all subqueries.
+
+ If a KeyError is raised during the attempt, it is ignored, unless
+ every subquery raised a KeyError. If any other exception is
+ raised, any deletes will be rolled back.
+
+ Args:
+ query_filter: the filter to delete.
+
+ Raises:
+ KeyError: No subquery had an entry containing query_filter.
+ """
+ subquery_count = len(self.__bound_queries)
+ keyerror_count = 0
+ saved_items = []
+ for index, query in enumerate(self.__bound_queries):
+ try:
+ saved_items.append(query.get(query_filter, None))
+ del query[query_filter]
+ except KeyError:
+ keyerror_count += 1
+ except:
+ for q, old_value in itertools.izip(self.__bound_queries[:index],
+ saved_items):
+ if old_value is not None:
+ q[query_filter] = old_value
+ raise
+
+ if keyerror_count == subquery_count:
+ raise KeyError(query_filter)
+
+ def __iter__(self):
+ return iter(self.__bound_queries)
+
+
+class Iterator(object):
+ """An iterator over the results of a datastore query.
+
+ Iterators are used to access the results of a Query. An iterator is
+ obtained by building a Query, then calling Run() on it.
+
+ Iterator implements Python's iterator protocol, so results can be accessed
+ with the for and in statements:
+
+ > it = Query('Person').Run()
+ > for person in it:
+ > print 'Hi, %s!' % person['name']
+ """
+ def __init__(self, query_result_pb, batch_size=None):
+ self.__cursor = query_result_pb.cursor()
+ self.__keys_only = query_result_pb.keys_only()
+ self.__batch_size = batch_size
+ self.__buffer = self._ProcessQueryResult(query_result_pb)
+
+ def _Get(self, count):
+ """Gets the next count result(s) of the query.
+
+ Not intended to be used by application developers. Use the python
+ iterator protocol instead.
+
+ This method uses _Next to returns the next entities or keys from the list of
+ matching results. If the query specified a sort order, results are returned
+ in that order. Otherwise, the order is undefined.
+
+ The argument, count, specifies the number of results to return. However, the
+ length of the returned list may be smaller than count. This is the case only
+ if count is greater than the number of remaining results.
+
+ The results are always returned as a list. If there are no results left,
+ an empty list is returned.
+
+ Args:
+ # the number of results to return; must be >= 1
+ count: int or long
+
+ Returns:
+ # a list of entities or keys
+ [Entity or Key, ...]
+ """
+ entity_list = self._Next(count)
+ while len(entity_list) < count and self.__more_results:
+ next_results = self._Next(count - len(entity_list), self.__batch_size)
+ if not next_results:
+ break
+ entity_list += next_results
+ return entity_list;
+
+ def _Next(self, count=None):
+ """Returns the next batch of results.
+
+ Not intended to be used by application developers. Use the python
+ iterator protocol instead.
+
+ This method returns the next entities or keys from the list of matching
+ results. If the query specified a sort order, results are returned in that
+ order. Otherwise, the order is undefined.
+
+ The optional argument, count, specifies the number of results to return.
+ However, the length of the returned list may be smaller than count. This is
+ the case if count is greater than the number of remaining results or the
+ size of the remaining results exceeds the RPC buffer limit. Use _Get to
+ insure all possible entities are retrieved.
+
+ If the count is omitted, the datastore backend decides how many entities to
+ send.
+
+ There is an internal buffer for use with the next() method. If this buffer
+ is not empty, up to 'count' values are removed from this buffer and
+ returned. It's best not to mix _Next() and next().
+
+ The results are always returned as a list. If there are no results left,
+ an empty list is returned.
+
+ Args:
+ # the number of results to return; must be >= 1
+ count: int or long or None
+
+ Returns:
+ # a list of entities or keys
+ [Entity or Key, ...]
+ """
+ if count is not None and (not isinstance(count, (int, long)) or count <= 0):
+ raise datastore_errors.BadArgumentError(
+ 'Argument to _Next must be an int greater than 0; received %s (a %s)' %
+ (count, typename(count)))
+
+ if self.__buffer:
+ if count is None:
+ entity_list = self.__buffer
+ self.__buffer = []
+ return entity_list
+ elif count <= len(self.__buffer):
+ entity_list = self.__buffer[:count]
+ del self.__buffer[:count]
+ return entity_list
+ else:
+ entity_list = self.__buffer
+ self.__buffer = []
+ count -= len(entity_list)
+ else:
+ entity_list = []
+
+
+ if not self.__more_results:
+ return entity_list
+
+ req = datastore_pb.NextRequest()
+ if count is not None:
+ req.set_count(count)
+ req.mutable_cursor().CopyFrom(self.__cursor)
+ result = datastore_pb.QueryResult()
+ try:
+ apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Next', req, result)
+ except apiproxy_errors.ApplicationError, err:
+ raise _ToDatastoreError(err)
+
+ return entity_list + self._ProcessQueryResult(result)
+
+ def _ProcessQueryResult(self, result):
+ """Returns all results from datastore_pb.QueryResult and updates
+ self.__more_results
+
+ Not intended to be used by application developers. Use the python
+ iterator protocol instead.
+
+ The results are always returned as a list. If there are no results left,
+ an empty list is returned.
+
+ Args:
+ # the instance of datastore_pb.QueryResult to be stored
+ result: datastore_pb.QueryResult
+
+ Returns:
+ # a list of entities or keys
+ [Entity or Key, ...]
+ """
+ self.__more_results = result.more_results()
+
+ if self.__keys_only:
+ return [Key._FromPb(e.key()) for e in result.result_list()]
+ else:
+ return [Entity._FromPb(e) for e in result.result_list()]
+
+ def next(self):
+ if not self.__buffer:
+ self.__buffer = self._Next(self.__batch_size)
+ try:
+ return self.__buffer.pop(0)
+ except IndexError:
+ raise StopIteration
+
+ def __iter__(self): return self
+
+class _Transaction(object):
+ """Encapsulates a transaction currently in progress.
+
+ If we know the entity group for this transaction, it's stored in the
+ entity_group attribute, which is set by RunInTransaction().
+
+ modified_keys is a set containing the Keys of all entities modified (ie put
+ or deleted) in this transaction. If an entity is modified more than once, a
+ BadRequestError is raised.
+ """
+ def __init__(self, handle):
+ """Initializes the transaction.
+
+ Args:
+ handle: a datastore_pb.Transaction returned by a BeginTransaction call
+ """
+ assert isinstance(handle, datastore_pb.Transaction)
+ explanation = []
+ assert handle.IsInitialized(explanation), explanation
+
+ self.handle = handle
+ self.entity_group = None
+ self.modified_keys = None
+ self.modified_keys = set()
+
+
+def RunInTransaction(function, *args, **kwargs):
+ """Runs a function inside a datastore transaction.
+
+ Runs the user-provided function inside transaction, retries default
+ number of times.
+
+ Args:
+ # a function to be run inside the transaction
+ function: callable
+ # positional arguments to pass to the function
+ args: variable number of any type
+
+ Returns:
+ the function's return value, if any
+
+ Raises:
+ TransactionFailedError, if the transaction could not be committed.
+ """
+ return RunInTransactionCustomRetries(
+ DEFAULT_TRANSACTION_RETRIES, function, *args, **kwargs)
+
+
+def RunInTransactionCustomRetries(retries, function, *args, **kwargs):
+ """Runs a function inside a datastore transaction.
+
+ Runs the user-provided function inside a full-featured, ACID datastore
+ transaction. Every Put, Get, and Delete call in the function is made within
+ the transaction. All entities involved in these calls must belong to the
+ same entity group. Queries are not supported.
+
+ The trailing arguments are passed to the function as positional arguments.
+ If the function returns a value, that value will be returned by
+ RunInTransaction. Otherwise, it will return None.
+
+ The function may raise any exception to roll back the transaction instead of
+ committing it. If this happens, the transaction will be rolled back and the
+ exception will be re-raised up to RunInTransaction's caller.
+
+ If you want to roll back intentionally, but don't have an appropriate
+ exception to raise, you can raise an instance of datastore_errors.Rollback.
+ It will cause a rollback, but will *not* be re-raised up to the caller.
+
+ The function may be run more than once, so it should be idempotent. It
+ should avoid side effects, and it shouldn't have *any* side effects that
+ aren't safe to occur multiple times. This includes modifying the arguments,
+ since they persist across invocations of the function. However, this doesn't
+ include Put, Get, and Delete calls, of course.
+
+ Example usage:
+
+ > def decrement(key, amount=1):
+ > counter = datastore.Get(key)
+ > counter['count'] -= amount
+ > if counter['count'] < 0: # don't let the counter go negative
+ > raise datastore_errors.Rollback()
+ > datastore.Put(counter)
+ >
+ > counter = datastore.Query('Counter', {'name': 'foo'})
+ > datastore.RunInTransaction(decrement, counter.key(), amount=5)
+
+ Transactions satisfy the traditional ACID properties. They are:
+
+ - Atomic. All of a transaction's operations are executed or none of them are.
+
+ - Consistent. The datastore's state is consistent before and after a
+ transaction, whether it committed or rolled back. Invariants such as
+ "every entity has a primary key" are preserved.
+
+ - Isolated. Transactions operate on a snapshot of the datastore. Other
+ datastore operations do not see intermediated effects of the transaction;
+ they only see its effects after it has committed.
+
+ - Durable. On commit, all writes are persisted to the datastore.
+
+ Nested transactions are not supported.
+
+ Args:
+ # number of retries
+ retries: integer
+ # a function to be run inside the transaction
+ function: callable
+ # positional arguments to pass to the function
+ args: variable number of any type
+
+ Returns:
+ the function's return value, if any
+
+ Raises:
+ TransactionFailedError, if the transaction could not be committed.
+ """
+
+ if _CurrentTransactionKey():
+ raise datastore_errors.BadRequestError(
+ 'Nested transactions are not supported.')
+
+ if retries < 0:
+ raise datastore_errors.BadRequestError(
+ 'Number of retries should be non-negative number.')
+
+ tx_key = None
+
+ try:
+ tx_key = _NewTransactionKey()
+
+ for i in range(0, retries + 1):
+ handle = datastore_pb.Transaction()
+ try:
+ apiproxy_stub_map.MakeSyncCall('datastore_v3', 'BeginTransaction',
+ api_base_pb.VoidProto(), handle)
+ except apiproxy_errors.ApplicationError, err:
+ raise _ToDatastoreError(err)
+
+ tx = _Transaction(handle)
+ _txes[tx_key] = tx
+
+ try:
+ result = function(*args, **kwargs)
+ except:
+ original_exception = sys.exc_info()
+
+ try:
+ resp = api_base_pb.VoidProto()
+ apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Rollback',
+ tx.handle, resp)
+ except:
+ exc_info = sys.exc_info()
+ logging.info('Exception sending Rollback:\n' +
+ ''.join(traceback.format_exception(*exc_info)))
+
+ type, value, trace = original_exception
+ if type is datastore_errors.Rollback:
+ return
+ else:
+ raise type, value, trace
+
+ try:
+ resp = datastore_pb.CommitResponse()
+ apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Commit',
+ tx.handle, resp)
+ except apiproxy_errors.ApplicationError, err:
+ if (err.application_error ==
+ datastore_pb.Error.CONCURRENT_TRANSACTION):
+ logging.warning('Transaction collision for entity group with '
+ 'key %r. Retrying...', tx.entity_group)
+ tx.handle = None
+ tx.entity_group = None
+ continue
+ else:
+ raise _ToDatastoreError(err)
+
+ return result
+
+ raise datastore_errors.TransactionFailedError(
+ 'The transaction could not be committed. Please try again.')
+
+ finally:
+ if tx_key in _txes:
+ del _txes[tx_key]
+ del tx_key
+
+
+def _MaybeSetupTransaction(request, keys):
+ """If we're in a transaction, validates and populates it in the request.
+
+ If we're currently inside a transaction, this records the entity group,
+ checks that the keys are all in that entity group, and populates the
+ transaction handle in the request.
+
+ Raises BadRequestError if the entity has a different entity group than the
+ current transaction.
+
+ Args:
+ request: GetRequest, PutRequest, DeleteRequest, or Query
+ keys: sequence of Keys
+
+ Returns:
+ _Transaction if we're inside a transaction, otherwise None
+ """
+ assert isinstance(request, (datastore_pb.GetRequest, datastore_pb.PutRequest,
+ datastore_pb.DeleteRequest, datastore_pb.Query,
+ taskqueue_service_pb.TaskQueueAddRequest,
+ )), request.__class__
+ tx_key = None
+
+ try:
+ tx_key = _CurrentTransactionKey()
+ if tx_key:
+ tx = _txes[tx_key]
+
+ groups = [k.entity_group() for k in keys]
+ if tx.entity_group:
+ expected_group = tx.entity_group
+ elif groups:
+ expected_group = groups[0]
+ else:
+ expected_group = None
+
+ for group in groups:
+ if (group != expected_group or
+
+
+
+
+
+
+
+ (not group.has_id_or_name() and group is not expected_group)):
+ raise _DifferentEntityGroupError(expected_group, group)
+
+ if not tx.entity_group and group.has_id_or_name():
+ tx.entity_group = group
+
+ assert tx.handle.IsInitialized()
+ request.mutable_transaction().CopyFrom(tx.handle)
+
+ return tx
+
+ finally:
+ del tx_key
+
+
+def _DifferentEntityGroupError(a, b):
+ """Raises a BadRequestError that says the given entity groups are different.
+
+ Includes the two entity groups in the message, formatted more clearly and
+ concisely than repr(Key).
+
+ Args:
+ a, b are both Keys that represent entity groups.
+ """
+ def id_or_name(key):
+ if key.name():
+ return 'name=%r' % key.name()
+ else:
+ return 'id=%r' % key.id()
+
+ raise datastore_errors.BadRequestError(
+ 'Cannot operate on different entity groups in a transaction: '
+ '(kind=%r, %s) and (kind=%r, %s).' % (a.kind(), id_or_name(a),
+ b.kind(), id_or_name(b)))
+
+
+def _FindTransactionFrameInStack():
+ """Walks the stack to find a RunInTransaction() call.
+
+ Returns:
+ # this is the RunInTransactionCustomRetries() frame record, if found
+ frame record or None
+ """
+ frame = sys._getframe()
+ filename = frame.f_code.co_filename
+
+ frame = frame.f_back.f_back
+ while frame:
+ if (frame.f_code.co_filename == filename and
+ frame.f_code.co_name == 'RunInTransactionCustomRetries'):
+ return frame
+ frame = frame.f_back
+
+ return None
+
+_CurrentTransactionKey = _FindTransactionFrameInStack
+
+_NewTransactionKey = sys._getframe
+
+
+def _GetCompleteKeyOrError(arg):
+ """Expects an Entity or a Key, and returns the corresponding Key. Raises
+ BadArgumentError or BadKeyError if arg is a different type or is incomplete.
+
+ Args:
+ arg: Entity or Key
+
+ Returns:
+ Key
+ """
+ if isinstance(arg, Key):
+ key = arg
+ elif isinstance(arg, basestring):
+ key = Key(arg)
+ elif isinstance(arg, Entity):
+ key = arg.key()
+ elif not isinstance(arg, Key):
+ raise datastore_errors.BadArgumentError(
+ 'Expects argument to be an Entity or Key; received %s (a %s).' %
+ (arg, typename(arg)))
+ assert isinstance(key, Key)
+
+ if not key.has_id_or_name():
+ raise datastore_errors.BadKeyError('Key %r is not complete.' % key)
+
+ return key
+
+
+def _GetPropertyValue(entity, property):
+ """Returns an entity's value for a given property name.
+
+ Handles special properties like __key__ as well as normal properties.
+
+ Args:
+ entity: datastore.Entity
+ property: str; the property name
+
+ Returns:
+ property value. For __key__, a datastore_types.Key.
+
+ Raises:
+ KeyError, if the entity does not have the given property.
+ """
+ if property in datastore_types._SPECIAL_PROPERTIES:
+ assert property == datastore_types._KEY_SPECIAL_PROPERTY
+ return entity.key()
+ else:
+ return entity[property]
+
+
+def _AddOrAppend(dictionary, key, value):
+ """Adds the value to the existing values in the dictionary, if any.
+
+ If dictionary[key] doesn't exist, sets dictionary[key] to value.
+
+ If dictionary[key] is not a list, sets dictionary[key] to [old_value, value].
+
+ If dictionary[key] is a list, appends value to that list.
+
+ Args:
+ dictionary: a dict
+ key, value: anything
+ """
+ if key in dictionary:
+ existing_value = dictionary[key]
+ if isinstance(existing_value, list):
+ existing_value.append(value)
+ else:
+ dictionary[key] = [existing_value, value]
+ else:
+ dictionary[key] = value
+
+
+def _ToDatastoreError(err):
+ """Converts an apiproxy.ApplicationError to an error in datastore_errors.
+
+ Args:
+ err: apiproxy.ApplicationError
+
+ Returns:
+ a subclass of datastore_errors.Error
+ """
+ errors = {
+ datastore_pb.Error.BAD_REQUEST: datastore_errors.BadRequestError,
+ datastore_pb.Error.CONCURRENT_TRANSACTION:
+ datastore_errors.TransactionFailedError,
+ datastore_pb.Error.INTERNAL_ERROR: datastore_errors.InternalError,
+ datastore_pb.Error.NEED_INDEX: datastore_errors.NeedIndexError,
+ datastore_pb.Error.TIMEOUT: datastore_errors.Timeout,
+ }
+
+ if err.application_error in errors:
+ raise errors[err.application_error](err.error_detail)
+ else:
+ raise datastore_errors.Error(err.error_detail)