blob: c70779c227e582ff0948957a8f96336911bb2b33 [file] [log] [blame]
# Copyright 2012 Google Inc. All Rights Reserved.
"""Utility module for converting properties to ProtoRPC messages/fields.
The methods here are not specific to NDB or DB (the datastore APIs) and can
be used by utility methods in the datastore API specific code.
"""
__all__ = ['GeoPtMessage', 'MessageFieldsSchema', 'UserMessage',
'method', 'positional', 'query_method']
import datetime
from protorpc import messages
from protorpc import util as protorpc_util
from google.appengine.api import users
ALLOWED_DECORATOR_NAME = frozenset(['method', 'query_method'])
DATETIME_STRING_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'
DATE_STRING_FORMAT = '%Y-%m-%d'
TIME_STRING_FORMAT = '%H:%M:%S.%f'
positional = protorpc_util.positional
def IsSubclass(candidate, parent_class):
"""Calls issubclass without raising an exception.
Args:
candidate: A candidate to check if a subclass.
parent_class: A class or tuple of classes representing a potential parent.
Returns:
A boolean indicating whether or not candidate is a subclass of parent_class.
"""
try:
return issubclass(candidate, parent_class)
except TypeError:
return False
def IsSimpleField(property_type):
"""Checks if a property type is a "simple" ProtoRPC field.
We consider "simple" ProtoRPC fields to be ones which are not message/enum
fields, since those depend on extra data when defined.
Args:
property_type: A ProtoRPC field.
Returns:
A boolean indicating whether or not the passed in property type is a
simple field.
"""
if IsSubclass(property_type, messages.Field):
return property_type not in (messages.EnumField, messages.MessageField)
return False
def CheckValidPropertyType(property_type, raise_invalid=True):
"""Checks if a property type is a valid class.
Here "valid" means the property type is either a simple field, a ProtoRPC
enum class which can be used to define an EnumField or a ProtoRPC message
class that can be used to define a MessageField.
Args:
property_type: A ProtoRPC field, message class or enum class that
describes the output of the alias property.
raise_invalid: Boolean indicating whether or not an exception should be
raised if the given property is not valid. Defaults to True.
Returns:
A boolean indicating whether or not the passed in property type is valid.
NOTE: Only returns if raise_invalid is False.
Raises:
TypeError: If raise_invalid is True and the passed in property is not valid.
"""
is_valid = IsSimpleField(property_type)
if not is_valid:
is_valid = IsSubclass(property_type, (messages.Enum, messages.Message))
if not is_valid and raise_invalid:
error_msg = ('Property field must be either a subclass of a simple '
'ProtoRPC field, a ProtoRPC enum class or a ProtoRPC message '
'class. Received %r.' % (property_type,))
raise TypeError(error_msg)
return is_valid
def _DictToTuple(to_sort):
"""Converts a dictionary into a tuple of keys sorted by values.
Args:
to_sort: A dictionary like object that has a callable items method.
Returns:
A tuple containing the dictionary keys, sorted by value.
"""
items = to_sort.items()
items.sort(key=lambda pair: pair[1])
return tuple(pair[0] for pair in items)
class MessageFieldsSchema(object):
"""A custom dictionary which is hashable.
Intended to be used so either dictionaries or lists can be used to define
field index orderings of a ProtoRPC message classes. Since hashable, we can
cache these ProtoRPC message class definitions using the fields schema
as a key.
These objects can be used as if they were dictionaries in many contexts and
can be compared for equality by hash.
"""
def __init__(self, fields, name=None, collection_name=None, basename=''):
"""Save list/tuple or convert dictionary a list based on value ordering.
Attributes:
name: A name for the fields schema.
collection_name: A name for collections using the fields schema.
_data: The underlying dictionary holding the data for the instance.
Args:
fields: A dictionary or ordered iterable which defines an index ordering
for fields in a ProtoRPC message class
name: A name for the fields schema, defaults to None. If None, uses the
names in the fields in the order they appear. If the fields schema
passed in is an instance of MessageFieldsSchema, this is ignored.
collection_name: A name for collections containing the fields schema,
defaults to None. If None, uses the name and appends the string
'Collection'.
basename: A basename for the default fields schema name, defaults to the
empty string. If the fields passed in is an instance of
MessageFieldsSchema, this is ignored.
Raises:
TypeError: if the fields passed in are not a dictionary, tuple, list or
existing MessageFieldsSchema instance.
"""
if isinstance(fields, MessageFieldsSchema):
self._data = fields._data
name = fields.name
collection_name = fields.collection_name
elif isinstance(fields, dict):
self._data = _DictToTuple(fields)
elif isinstance(fields, (list, tuple)):
self._data = tuple(fields)
else:
error_msg = ('Can\'t create MessageFieldsSchema from object of type %s. '
'Must be a dictionary or iterable.' % (fields.__class__,))
raise TypeError(error_msg)
self.name = name or self._DefaultName(basename=basename)
self.collection_name = collection_name or (self.name + 'Collection')
def _DefaultName(self, basename=''):
"""The default name of the fields schema.
Can potentially use a basename at the front, but otherwise uses the instance
fields and joins all the values together using an underscore.
Args:
basename: An optional string, defaults to the empty string. If not empty,
is used at the front of the default name.
Returns:
A string containing the default name of the fields schema.
"""
name_parts = []
if basename:
name_parts.append(basename)
name_parts.extend(self._data)
return '_'.join(name_parts)
def __ne__(self, other):
"""Not equals comparison that uses the definition of equality."""
return not self.__eq__(other)
def __eq__(self, other):
"""Comparison for equality that uses the hash of the object."""
if not isinstance(other, self.__class__):
return False
return self.__hash__() == other.__hash__()
def __hash__(self):
"""Unique and idempotent hash.
Uses a the property list (_data) which is uniquely defined by its elements
and their sort order, the name of the fields schema and the collection name
of the fields schema.
Returns:
Integer hash value.
"""
return hash((self._data, self.name, self.collection_name))
def __iter__(self):
"""Iterator for loop expressions."""
return iter(self._data)
class GeoPtMessage(messages.Message):
"""ProtoRPC container for GeoPt instances.
Attributes:
lat: Float; The latitude of the point.
lon: Float; The longitude of the point.
"""
# TODO(dhermes): This behavior should be regulated more directly.
# This is to make sure the schema name in the discovery
# document is GeoPtMessage rather than
# EndpointsProtoDatastoreGeoPtMessage.
__module__ = ''
lat = messages.FloatField(1, required=True)
lon = messages.FloatField(2, required=True)
class UserMessage(messages.Message):
"""ProtoRPC container for users.User objects.
Attributes:
email: String; The email of the user.
auth_domain: String; The auth domain of the user.
user_id: String; The user ID.
federated_identity: String; The federated identity of the user.
"""
# TODO(dhermes): This behavior should be regulated more directly.
# This is to make sure the schema name in the discovery
# document is UserMessage rather than
# EndpointsProtoDatastoreUserMessage.
__module__ = ''
email = messages.StringField(1, required=True)
auth_domain = messages.StringField(2, required=True)
user_id = messages.StringField(3)
federated_identity = messages.StringField(4)
def UserMessageFromUser(user):
"""Converts a native users.User object to a UserMessage.
Args:
user: An instance of users.User.
Returns:
A UserMessage with attributes set from the user.
"""
return UserMessage(email=user.email(),
auth_domain=user.auth_domain(),
user_id=user.user_id(),
federated_identity=user.federated_identity())
def UserMessageToUser(message):
"""Converts a UserMessage to a native users.User object.
Args:
message: The message to be converted.
Returns:
An instance of users.User with attributes set from the message.
"""
return users.User(email=message.email,
_auth_domain=message.auth_domain,
_user_id=message.user_id,
federated_identity=message.federated_identity)
def DatetimeValueToString(value):
"""Converts a datetime value to a string.
Args:
value: The value to be converted to a string.
Returns:
A string containing the serialized value of the datetime stamp.
Raises:
TypeError: if the value is not an instance of one of the three
datetime types.
"""
if isinstance(value, datetime.time):
return value.strftime(TIME_STRING_FORMAT)
# Order is important, datetime.datetime is a subclass of datetime.date
elif isinstance(value, datetime.datetime):
return value.strftime(DATETIME_STRING_FORMAT)
elif isinstance(value, datetime.date):
return value.strftime(DATE_STRING_FORMAT)
else:
raise TypeError('Could not serialize timestamp: %s.' % (value,))
def DatetimeValueFromString(value):
"""Converts a serialized datetime string to the native type.
Args:
value: The string value to be deserialized.
Returns:
A datetime.datetime/date/time object that was deserialized from the string.
Raises:
TypeError: if the value can not be deserialized to one of the three
datetime types.
"""
try:
return datetime.datetime.strptime(value, TIME_STRING_FORMAT).time()
except ValueError:
pass
try:
return datetime.datetime.strptime(value, DATE_STRING_FORMAT).date()
except ValueError:
pass
try:
return datetime.datetime.strptime(value, DATETIME_STRING_FORMAT)
except ValueError:
pass
raise TypeError('Could not deserialize timestamp: %s.' % (value,))
def RaiseNotImplementedMethod(property_class, explanation=None):
"""Wrapper method that returns a method which always fails.
Args:
property_class: A property class
explanation: An optional argument explaining why the given property
has not been implemented
Returns:
A method which will always raise NotImplementedError. If explanation is
included, it will be raised as part of the exception, otherwise, a
simple explanation will be provided that uses the name of the property
class.
"""
if explanation is None:
explanation = ('The property %s can\'t be used to define an '
'EndpointsModel.' % (property_class.__name__,))
def RaiseNotImplemented(unused_prop, unused_index):
"""Dummy method that will always raise NotImplementedError.
Raises:
NotImplementedError: always
"""
raise NotImplementedError(explanation)
return RaiseNotImplemented
def _GetEndpointsMethodDecorator(decorator_name, modelclass, **kwargs):
"""Decorate a ProtoRPC method for use by the endpoints model passed in.
Requires exactly two positional arguments and passes the rest of the keyword
arguments to the classmethod method at the decorator name on the given class.
Args:
decorator_name: The name of the attribute on the model containing the
function which will produce the decorator.
modelclass: An Endpoints model class.
Returns:
A decorator that will use the endpoint metadata to decorate an endpoints
method.
"""
if decorator_name not in ALLOWED_DECORATOR_NAME:
raise TypeError('Decorator %s not allowed.' % (decorator_name,))
# Import here to avoid circular imports
from .ndb import model
if IsSubclass(modelclass, model.EndpointsModel):
return getattr(modelclass, decorator_name)(**kwargs)
raise TypeError('Model class %s not a valid Endpoints model.' % (modelclass,))
@positional(1)
def method(modelclass, **kwargs):
"""Decorate a ProtoRPC method for use by the endpoints model passed in.
Requires exactly one positional argument and passes the rest of the keyword
arguments to the classmethod "method" on the given class.
Args:
modelclass: An Endpoints model class that can create a method.
Returns:
A decorator that will use the endpoint metadata to decorate an endpoints
method.
"""
return _GetEndpointsMethodDecorator('method', modelclass, **kwargs)
@positional(1)
def query_method(modelclass, **kwargs):
"""Decorate a ProtoRPC method intended for queries
For use by the endpoints model passed in. Requires exactly one positional
argument and passes the rest of the keyword arguments to the classmethod
"query_method" on the given class.
Args:
modelclass: An Endpoints model class that can create a query method.
Returns:
A decorator that will use the endpoint metadata to decorate an endpoints
query method.
"""
return _GetEndpointsMethodDecorator('query_method', modelclass, **kwargs)