diff -r 27971a13089f -r 2e0b0af889be thirdparty/google_appengine/google/appengine/api/datastore_types.py --- a/thirdparty/google_appengine/google/appengine/api/datastore_types.py Sat Sep 05 14:04:24 2009 +0200 +++ b/thirdparty/google_appengine/google/appengine/api/datastore_types.py Sun Sep 06 23:31:53 2009 +0200 @@ -47,6 +47,7 @@ from google.appengine.datastore import datastore_pb from google.appengine.api import datastore_errors from google.appengine.api import users +from google.appengine.api import namespace_manager from google.net.proto import ProtocolBuffer from google.appengine.datastore import entity_pb @@ -59,6 +60,8 @@ _KEY_SPECIAL_PROPERTY = '__key__' _SPECIAL_PROPERTIES = frozenset([_KEY_SPECIAL_PROPERTY]) +_NAMESPACE_SEPARATOR='!' + class UtcTzinfo(datetime.tzinfo): def utcoffset(self, dt): return datetime.timedelta(0) def dst(self, dt): return datetime.timedelta(0) @@ -80,7 +83,8 @@ def ValidateString(value, name='unused', exception=datastore_errors.BadValueError, - max_len=_MAX_STRING_LENGTH): + max_len=_MAX_STRING_LENGTH, + empty_ok=False): """Raises an exception if value is not a valid string or a subclass thereof. A string is valid if it's not empty, no more than _MAX_STRING_LENGTH bytes, @@ -91,17 +95,49 @@ value: the value to validate. name: the name of this value; used in the exception message. exception: the type of exception to raise. - max_len: the maximum allowed length, in bytes + max_len: the maximum allowed length, in bytes. + empty_ok: allow empty value. """ + if value is None and empty_ok: + return if not isinstance(value, basestring) or isinstance(value, Blob): raise exception('%s should be a string; received %s (a %s):' % (name, value, typename(value))) - if not value: + if not value and not empty_ok: raise exception('%s must not be empty.' % name) if len(value.encode('utf-8')) > max_len: raise exception('%s must be under %d bytes.' % (name, max_len)) +def ValidateInteger(value, + name='unused', + exception=datastore_errors.BadValueError, + empty_ok=False, + zero_ok=False, + negative_ok=False): + """Raises an exception if value is not a valid integer. + + An integer is valid if it's not negative or empty and is an integer. + The exception type can be specified with the exception argument; + it defaults to BadValueError. + + Args: + value: the value to validate. + name: the name of this value; used in the exception message. + exception: the type of exception to raise. + empty_ok: allow None value. + zero_ok: allow zero value. + negative_ok: allow negative value. + """ + if value is None and empty_ok: + return + if not isinstance(value, int): + raise exception('%s should be an integer; received %s (a %s).' % + (name, value, typename(value))) + if not value and not zero_ok: + raise exception('%s must not be 0 (zero)' % name) + if value < 0 and not negative_ok: + raise exception('%s must not be negative.' % name) def ResolveAppId(app, name='_app'): """Validate app id, providing a default. @@ -124,6 +160,152 @@ return app +class AppIdNamespace(object): + """Combined AppId and Namespace + + An identifier that combines the application identifier and the + namespace. + """ + __app_id = None + __namespace = None + + def __init__(self, app_id, namespace): + """Constructor. Creates a AppIdNamespace from two strings. + + Args: + app_id: application identifier string + namespace: namespace identifier string + Raises: + BadArgumentError if the values contain + the _NAMESPACE_SEPARATOR character (!) or + the app_id is empty. + """ + self.__app_id = app_id + if namespace: + self.__namespace = namespace + else: + self.__namespace = None + ValidateString(self.__app_id, 'app_id', datastore_errors.BadArgumentError) + ValidateString(self.__namespace, + 'namespace', datastore_errors.BadArgumentError, + empty_ok=True) + if _NAMESPACE_SEPARATOR in self.__app_id: + raise datastore_errors.BadArgumentError( + 'app_id must not contain a "%s"' % _NAMESPACE_SEPARATOR) + if self.__namespace and _NAMESPACE_SEPARATOR in self.__namespace: + raise datastore_errors.BadArgumentError( + 'namespace must not contain a "%s"' % _NAMESPACE_SEPARATOR) + + def __cmp__(self, other): + """Returns negative, zero, or positive when comparing two AppIdNamespace. + + Args: + other: AppIdNamespace to compare to. + + Returns: + Negative if self is less than "other" + Zero if "other" is equal to self + Positive if self is greater than "other" + """ + if not isinstance(other, AppIdNamespace): + return cmp(id(self), id(other)) + return cmp((self.__app_id, self.__namespace), + (other.__app_id, other.__namespace)) + + def to_encoded(self): + """Returns this AppIdNamespace's string equivalent + + i.e. "app!namespace" + """ + if not self.__namespace: + return self.__app_id + else: + return self.__app_id + _NAMESPACE_SEPARATOR + self.__namespace + + def app_id(self): + """Returns this AppId portion of this AppIdNamespace. + """ + return self.__app_id; + + def namespace(self): + """Returns this namespace portion of this AppIdNamespace. + """ + return self.__namespace; + + +def PartitionString(value, separator): + """Equivalent to python2.5 str.partition() + TODO(gmariani) use str.partition() when python 2.5 is adopted. + + Args: + value: String to be partitioned + separator: Separator string + """ + index = value.find(separator); + if index == -1: + return (value, '', value[0:0]); + else: + return (value[0:index], separator, value[index+len(separator):len(value)]) + + +def parse_app_id_namespace(app_id_namespace): + """ + An app_id_namespace string is valid if it's not empty, and contains + at most one namespace separator ('!'). Also, an app_id_namespace + with an empty namespace must not contain a namespace separator. + + Args: + app_id_namespace: an encoded app_id_namespace. + Raises exception if format of app_id_namespace is invalid. + """ + if not app_id_namespace: + raise datastore_errors.BadArgumentError( + 'app_id_namespace must be non empty') + parts = PartitionString(app_id_namespace, _NAMESPACE_SEPARATOR) + if parts[1] == _NAMESPACE_SEPARATOR: + if not parts[2]: + raise datastore_errors.BadArgumentError( + 'app_id_namespace must not contain a "%s" if the namespace is empty' % + _NAMESPACE_SEPARATOR) + if parts[2]: + return AppIdNamespace(parts[0], parts[2]) + return AppIdNamespace(parts[0], None) + +def ResolveAppIdNamespace( + app_id=None, namespace=None, app_id_namespace=None): + """Validate an app id/namespace and substitute default values. + + If the argument is None, $APPLICATION_ID!$NAMESPACE is substituted. + + Args: + app_id: The app id argument value to be validated. + namespace: The namespace argument value to be validated. + app_id_namespace: An AppId/Namespace pair + + Returns: + An AppIdNamespace object initialized with AppId and Namespace. + + Raises: + BadArgumentError if the value is empty or not a string. + """ + if app_id_namespace is None: + if app_id is None: + app_id = os.environ.get('APPLICATION_ID', '') + if namespace is None: + namespace = namespace_manager.get_request_namespace(); + else: + if not app_id is None: + raise datastore_errors.BadArgumentError( + 'app_id is overspecified. Cannot define app_id_namespace and app_id') + if not namespace is None: + raise datastore_errors.BadArgumentError( + 'namespace is overspecified. ' + + 'Cannot define app_id_namespace and namespace') + return parse_app_id_namespace(app_id_namespace) + + return AppIdNamespace(app_id, namespace) + + class Key(object): """The primary key for a datastore entity. @@ -172,6 +354,26 @@ else: self.__reference = entity_pb.Reference() + def to_path(self): + """Construct the "path" of this key as a list. + + Returns: + A list [kind_1, id_or_name_1, ..., kind_n, id_or_name_n] of the key path. + + Raises: + datastore_errors.BadKeyError if this key does not have a valid path. + """ + path = [] + for path_element in self.__reference.path().element_list(): + path.append(path_element.type().decode('utf-8')) + if path_element.has_name(): + path.append(path_element.name().decode('utf-8')) + elif path_element.has_id(): + path.append(path_element.id()) + else: + raise datastore_errors.BadKeyError('Incomplete key found in to_path') + return path + @staticmethod def from_path(*args, **kwds): """Static method to construct a Key out of a "path" (kind, id or name, ...). @@ -202,7 +404,10 @@ BadKeyError if the parent key is incomplete. """ parent = kwds.pop('parent', None) - _app = ResolveAppId(kwds.pop('_app', None)) + _app_id_namespace_obj = ResolveAppIdNamespace( + kwds.pop('_app', None), + kwds.pop('_namespace', None), + kwds.pop('_app_id_namespace', None)) if kwds: raise datastore_errors.BadArgumentError( @@ -221,17 +426,18 @@ if not parent.has_id_or_name(): raise datastore_errors.BadKeyError( 'The parent Key is incomplete.') - if _app != parent.app(): + if _app_id_namespace_obj != parent.app_id_namespace(): raise datastore_errors.BadArgumentError( - 'The _app argument (%r) should match parent.app() (%s)' % - (_app, parent.app())) + 'The app_id/namespace arguments (%r) should match ' + + 'parent.app_id_namespace().to_encoded() (%s)' % + (_app_id_namespace_obj, parent.app_id_namespace())) key = Key() ref = key.__reference if parent is not None: ref.CopyFrom(parent.__reference) else: - ref.set_app(_app) + ref.set_app(_app_id_namespace_obj.to_encoded()) path = ref.mutable_path() for i in xrange(0, len(args), 2): @@ -248,9 +454,6 @@ elem.set_id(id_or_name) elif isinstance(id_or_name, basestring): ValidateString(id_or_name, 'name') - if id_or_name and id_or_name[0] in string.digits: - raise datastore_errors.BadArgumentError( - 'Names may not begin with a digit; received %s.' % id_or_name) elem.set_name(id_or_name.encode('utf-8')) else: raise datastore_errors.BadArgumentError( @@ -263,7 +466,21 @@ def app(self): """Returns this entity's app id, a string.""" if self.__reference.app(): - return self.__reference.app().decode('utf-8') + return self.app_id_namespace().app_id().decode('utf-8') + else: + return None + + def namespace(self): + """Returns this entity's app id, a string.""" + if self.__reference.app(): + return self.app_id_namespace().namespace().decode('utf-8') + else: + return None + + def app_id_namespace(self): + """Returns this entity's app id/namespace, an appIdNamespace object.""" + if self.__reference.app(): + return parse_app_id_namespace(self.__reference.app()) else: return None @@ -339,11 +556,13 @@ raise datastore_errors.BadKeyError( 'ToTagUri() called for an entity with an incomplete key.') - return u'tag:%s.%s,%s:%s[%s]' % (saxutils.escape(self.app()), - os.environ['AUTH_DOMAIN'], - datetime.date.today().isoformat(), - saxutils.escape(self.kind()), - saxutils.escape(str(self))) + return u'tag:%s.%s,%s:%s[%s]' % ( + saxutils.escape(self.app_id_namespace().to_encoded()), + os.environ['AUTH_DOMAIN'], + datetime.date.today().isoformat(), + saxutils.escape(self.kind()), + saxutils.escape(str(self))) + ToXml = ToTagUri def entity_group(self): @@ -436,7 +655,7 @@ else: args.append(repr(elem.id())) - args.append('_app=%r' % self.__reference.app().decode('utf-8')) + args.append('_app_id_namespace=%r' % self.__reference.app().decode('utf-8')) return u'datastore_types.Key.from_path(%s)' % ', '.join(args) def __cmp__(self, other): @@ -459,25 +678,29 @@ self_args = [] other_args = [] - self_args.append(self.__reference.app().decode('utf-8')) - other_args.append(other.__reference.app().decode('utf-8')) + self_args.append(self.__reference.app()) + other_args.append(other.__reference.app()) for elem in self.__reference.path().element_list(): - self_args.append(repr(elem.type())) + self_args.append(elem.type()) if elem.has_name(): - self_args.append(repr(elem.name().decode('utf-8'))) + self_args.append(elem.name()) else: self_args.append(elem.id()) for elem in other.__reference.path().element_list(): - other_args.append(repr(elem.type())) + other_args.append(elem.type()) if elem.has_name(): - other_args.append(repr(elem.name().decode('utf-8'))) + other_args.append(elem.name()) else: other_args.append(elem.id()) - result = cmp(self_args, other_args) - return result + for self_component, other_component in zip(self_args, other_args): + comparison = cmp(self_component, other_component) + if comparison != 0: + return comparison + + return cmp(len(self_args), len(other_args)) def __hash__(self): """Returns a 32-bit integer hash of this key. @@ -698,6 +921,7 @@ except datastore_errors.BadValueError: return NotImplemented + return cmp((self.address, self.protocol), (other.address, other.protocol)) @@ -900,6 +1124,63 @@ return saxutils.escape(encoded) +class BlobKey(object): + """Key used to identify a blob in Blobstore. + + This object wraps a string that gets used internally by the Blobstore API + to identify application blobs. The BlobKey corresponds to the entity name + of the underlying BlobReference entity. The structure of the key is: + + _ + + This class is exposed in the API in both google.appengine.ext.db and + google.appengine.ext.blobstore. + """ + + def __init__(self, blob_key): + """Constructor. + + Used to convert a string to a BlobKey. Normally used internally by + Blobstore API. + + Args: + blob_key: Key name of BlobReference that this key belongs to. + """ + self.__blob_key = blob_key + + def __str__(self): + """Convert to string.""" + return self.__blob_key + + def __repr__(self): + """Returns an eval()able string representation of this key. + + Returns a Python string of the form 'datastore_types.BlobKey(...)' + that can be used to recreate this key. + + Returns: + string + """ + s = type(self).__module__ + return '%s.%s(%r)' % (type(self).__module__, + type(self).__name__, + self.__blob_key) + + def __cmp__(self, other): + if type(other) is type(self): + return cmp(str(self), str(other)) + elif isinstance(other, basestring): + return cmp(self.__blob_key, other) + else: + return NotImplemented + + def __hash__(self): + return hash(self.__blob_key) + + def ToXml(self): + return str(self) + + _PROPERTY_MEANINGS = { @@ -916,6 +1197,7 @@ PhoneNumber: entity_pb.Property.GD_PHONENUMBER, PostalAddress: entity_pb.Property.GD_POSTALADDRESS, Rating: entity_pb.Property.GD_RATING, + BlobKey: entity_pb.Property.BLOBKEY, } _PROPERTY_TYPES = frozenset([ @@ -940,6 +1222,7 @@ type(None), unicode, users.User, + BlobKey, ]) _RAW_PROPERTY_TYPES = (Blob, Text) @@ -1043,6 +1326,7 @@ type(None): ValidatePropertyNothing, unicode: ValidatePropertyString, users.User: ValidatePropertyNothing, + BlobKey: ValidatePropertyString, } assert set(_VALIDATE_PROPERTY_VALUES.iterkeys()) == _PROPERTY_TYPES @@ -1222,6 +1506,7 @@ """ pbvalue.set_doublevalue(value) + _PACK_PROPERTY_VALUES = { Blob: PackBlob, ByteString: PackBlob, @@ -1244,6 +1529,7 @@ type(None): lambda name, value, pbvalue: None, unicode: PackString, users.User: PackUser, + BlobKey: PackString, } assert set(_PACK_PROPERTY_VALUES.iterkeys()) == _PROPERTY_TYPES @@ -1331,7 +1617,6 @@ entity_pb.Property.ATOM_CATEGORY: Category, entity_pb.Property.ATOM_LINK: Link, entity_pb.Property.GD_EMAIL: Email, - entity_pb.Property.GEORSS_POINT: lambda coords: GeoPt(*coords), entity_pb.Property.GD_IM: IM, entity_pb.Property.GD_PHONENUMBER: PhoneNumber, entity_pb.Property.GD_POSTALADDRESS: PostalAddress, @@ -1339,6 +1624,7 @@ entity_pb.Property.BLOB: Blob, entity_pb.Property.BYTESTRING: ByteString, entity_pb.Property.TEXT: Text, + entity_pb.Property.BLOBKEY: BlobKey, } @@ -1368,7 +1654,7 @@ elif pbval.has_referencevalue(): value = FromReferenceProperty(pbval) elif pbval.has_pointvalue(): - value = (pbval.pointvalue().x(), pbval.pointvalue().y()) + value = GeoPt(pbval.pointvalue().x(), pbval.pointvalue().y()) elif pbval.has_uservalue(): email = unicode(pbval.uservalue().email().decode('utf-8')) auth_domain = unicode(pbval.uservalue().auth_domain().decode('utf-8')) @@ -1381,7 +1667,7 @@ value = None try: - if pb.has_meaning(): + if pb.has_meaning() and pb.meaning() in _PROPERTY_CONVERSIONS: conversion = _PROPERTY_CONVERSIONS[meaning] value = conversion(value) except (KeyError, ValueError, IndexError, TypeError, AttributeError), msg: @@ -1437,6 +1723,7 @@ 'gd:phonenumber': PhoneNumber, 'gd:postaladdress': PostalAddress, 'gd:rating': Rating, + 'blobkey': BlobKey, }