blob: 3d66fc7ac49faafe0656b9c15fb3a3960d6cfdb0 [file] [log] [blame]
# Copyright 2012 Google Inc. All Rights Reserved.
"""Custom properties for hybrid NDB/ProtoRPC models.
Custom properties are defined to allow custom interactions with complex
types and custom serialization of these values into ProtoRPC fields.
Defined here:
EndpointsAliasProperty:
A local only property used for including custom properties in messages
without having to persist these properties in the datastore and for creating
custom setters based on values parsed from requests.
EndpointsUserProperty:
For getting the user the same way an endpoints method does.
EndpointsDateTimeProperty,EndpointsDateProperty,EndpointsTimeProperty:
For custom serialization of date and/or time stamps.
EndpointsVariantIntegerProperty,EndpointsVariantFloatProperty:
For allowing ProtoRPC type variants for fields which allow it, e.g. a 32-bit
integer instead of the default 64-bit.
EndpointsComputedProperty:
a subclass of ndb.ComputedProperty; this property class is needed since one
cannot readily determine the type desired of the output.
"""
import datetime
import warnings
warnings.simplefilter('default') # To allow DeprecationWarning
from . import utils as ndb_utils
from .. import utils
import endpoints
from protorpc import messages
from google.appengine.ext import ndb
__all__ = [
'EndpointsAliasProperty', 'EndpointsUserProperty',
'EndpointsDateTimeProperty', 'EndpointsDateProperty',
'EndpointsTimeProperty', 'EndpointsVariantIntegerProperty',
'EndpointsVariantFloatProperty', 'EndpointsComputedProperty',
]
DEFAULT_PROPERTY_TYPE = messages.StringField
DATETIME_STRING_FORMAT = utils.DATETIME_STRING_FORMAT
DATE_STRING_FORMAT = utils.DATE_STRING_FORMAT
TIME_STRING_FORMAT = utils.TIME_STRING_FORMAT
def ComputedPropertyToProto(prop, index):
"""Converts a computed property to the corresponding message field.
Args:
prop: The NDB property to be converted.
index: The index of the property within the message.
Returns:
A ProtoRPC field. If the property_type of prop is a field, then a field of
that type will be returned. If the property_type of prop is an enum
class, then an enum field using that enum class is returned. If the
property_type of prop is a message class, then a message field using
that message class is returned.
Raises:
TypeError: if the property_type manages to pass CheckValidPropertyType
without an exception but does not match any of the parent types
messages.Field, messages.Enum or messages.Message. NOTE: This should
not occur, given the behavior of CheckValidPropertyType.
"""
kwargs = ndb_utils.GetKeywordArgs(prop)
property_type = prop.property_type
utils.CheckValidPropertyType(property_type)
if utils.IsSubclass(property_type, messages.Field):
return property_type(index, **kwargs)
elif utils.IsSubclass(property_type, messages.Enum):
return messages.EnumField(property_type, index, **kwargs)
elif utils.IsSubclass(property_type, messages.Message):
# No default for {MessageField}s
kwargs.pop('default', None)
return messages.MessageField(property_type, index, **kwargs)
else:
# Should never occur due to utils.CheckValidPropertyType.
raise TypeError('Unexpected property type: %s.' % (property_type,))
class EndpointsAliasProperty(property):
"""A custom property that also considers the type of the response.
Allows Python properties to be used in an EndpointsModel by also
specifying a property type. These properties can be derived from the rest
of the model and included in a ProtoRPC message definition, but will not need
to be persisted in the datastore.
This class can be used directly to define properties or as a decorator.
Attributes:
message_field: a value used to register the property in the property class
to proto dictionary for any model class with this property. The method
ComputedPropertyToProto is used here.
"""
message_field = ComputedPropertyToProto
@utils.positional(2)
def __init__(self, func=None, setter=None, fdel=None, doc=None,
repeated=False, required=False, default=None, name=None,
variant=None, property_type=DEFAULT_PROPERTY_TYPE):
"""Constructor for property.
Attributes:
__saved_property_args: A dictionary that can be stored on the instance if
used as a decorator rather than directly as a property.
__initialized: A boolean corresponding to whether or not the instance has
completed initialization or needs to continue when called as a
decorator.
_required: A boolean attribute for ProtoRPC conversion, denoting whether
this property is required in a message class.
_repeated: A boolean attribute for ProtoRPC conversion, denoting whether
this property is repeated in a message class.
_name: The true name of the property.
_code_name: The attribute name of the property on the model that
instantiated it.
_variant: An optional variant that can be used for ProtoRPC conversion,
since some ProtoRPC fields allow variants. Will not always be set on
alias properties.
property_type: A ProtoRPC field, message class or enum class that
describes the output of the alias property.
Args:
func: The method that outputs the value of the property. If None,
we use this as a signal the instance is being used as a decorator.
setter: The (optional) method that will allow the property to be set.
Passed to the property constructor as fset. Defaults to None.
fdel: The (optional) method that will be called when the property is
deleted. Passed to the property constructor as fdel. Defaults to None.
doc: The (optional) docstring for the property. Defaults to None.
repeated: Optional boolean, defaults to False. Indicates whether or not
the ProtoRPC field is repeated.
required: Optional boolean, defaults to False. Indicates whether or not
the ProtoRPC field should be required.
default: Optional default value for the property. Only set on the property
instance if not None. Will be validated when a corresponding message
field is created.
name: A custom name that can be used to describe the property.
variant: A variant of that can be used to augment the ProtoRPC field. Will
be validated when a corresponding message field is created.
property_type: A ProtoRPC field, message class or enum class that
describes the output of the alias property.
"""
self._required = required
self._repeated = repeated
self._name = name
self._code_name = None
if default is not None:
self._default = default
if variant is not None:
self._variant = variant
utils.CheckValidPropertyType(property_type)
self.property_type = property_type
property_args = {'fset': setter, 'fdel': fdel, 'doc': doc}
if func is None:
self.__initialized = False
self.__saved_property_args = property_args
else:
self.__initialized = True
super(EndpointsAliasProperty, self).__init__(func, **property_args)
def __call__(self, func):
"""Callable method to be used when instance is used as a decorator.
If called as a decorator, passes the saved keyword arguments and the func
to the constructor to complete initialization.
Args:
func: The method that outputs the value of the property.
Returns:
The property instance.
Raises:
TypeError: if the instance has already been initialized, either directly
as a property or as a decorator elsewhere.
"""
if self.__initialized:
raise TypeError('EndpointsAliasProperty is not callable.')
super(EndpointsAliasProperty, self).__init__(func,
**self.__saved_property_args)
del self.__saved_property_args
# Return the property created
return self
def _FixUp(self, code_name):
"""Internal helper called to tell the property its name.
Intended to allow a similar name interface as provided by NDB properties.
Used during class creation in EndpointsMetaModel.
Args:
code_name: The attribute name of the property as set on a class.
"""
self._code_name = code_name
if self._name is None:
self._name = self._code_name
class EndpointsUserProperty(ndb.UserProperty):
"""A custom user property for interacting with user ID tokens.
Uses the tools provided in the endpoints module to detect the current user.
In addition, has an optional parameter raise_unauthorized which will return
a 401 to the endpoints API request if a user can't be detected.
"""
def __init__(self, *args, **kwargs):
"""Constructor for User property.
NOTE: Have to pop custom arguments from the keyword argument dictionary
to avoid corrupting argument order when sent to the superclass.
Attributes:
_raise_unauthorized: An optional boolean, defaulting to False. If True,
the property will return a 401 to the API request if a user can't
be deteced.
"""
self._raise_unauthorized = kwargs.pop('raise_unauthorized', False)
super(EndpointsUserProperty, self).__init__(*args, **kwargs)
def _set_value(self, entity, value):
"""Internal helper to set value on model entity.
If the value to be set is null, will try to retrieve the current user and
will return a 401 if a user can't be found and raise_unauthorized is True.
Args:
entity: An instance of some NDB model.
value: The value of this property to be set on the instance.
"""
if value is None:
value = endpoints.get_current_user()
if self._raise_unauthorized and value is None:
raise endpoints.UnauthorizedException('Invalid token.')
super(EndpointsUserProperty, self)._set_value(entity, value)
def _fix_up(self, cls, code_name):
"""Internal helper called to register the property with the model class.
Overrides the _set_attributes method on the model class to interject this
attribute in to the keywords passed to it. Since the method _set_attributes
is called by the model class constructor to set values, this -- in congress
with the custom defined _set_value -- will make sure this property always
gets set when an instance is created, even if not passed in.
Args:
cls: The model class that owns the property.
code_name: The name of the attribute on the model class corresponding
to the property.
"""
original_set_attributes = cls._set_attributes
def CustomSetAttributes(setattr_self, kwds):
"""Custom _set_attributes which makes sure this property is always set."""
if self._code_name not in kwds:
kwds[self._code_name] = None
original_set_attributes(setattr_self, kwds)
cls._set_attributes = CustomSetAttributes
super(EndpointsUserProperty, self)._fix_up(cls, code_name)
class EndpointsDateTimeProperty(ndb.DateTimeProperty):
"""A custom datetime property.
Allows custom serialization of a datetime.datetime stamp when used to create
a message field.
"""
def __init__(self, *args, **kwargs):
"""Constructor for datetime property.
NOTE: Have to pop custom arguments from the keyword argument dictionary
to avoid corrupting argument order when sent to the superclass.
Attributes:
_string_format: An optional string, defaulting to DATETIME_STRING_FORMAT.
This is used to serialize using strftime and deserialize using strptime
when the datetime stamp is turned into a message.
"""
self._string_format = kwargs.pop('string_format', DATETIME_STRING_FORMAT)
super(EndpointsDateTimeProperty, self).__init__(*args, **kwargs)
def ToValue(self, value):
"""A custom method to override the typical ProtoRPC message serialization.
Uses the string_format set on the property to serialize the datetime stamp.
Args:
value: A datetime stamp, the value of the property.
Returns:
The serialized string value of the datetime stamp.
"""
return value.strftime(self._string_format)
def FromValue(self, value):
"""A custom method to override the typical ProtoRPC message deserialization.
Uses the string_format set on the property to deserialize the datetime
stamp.
Args:
value: A serialized datetime stamp as a string.
Returns:
The deserialized datetime.datetime stamp.
"""
return datetime.datetime.strptime(value, self._string_format)
class EndpointsDateProperty(ndb.DateProperty):
"""A custom date property.
Allows custom serialization of a datetime.date stamp when used to create a
message field.
"""
def __init__(self, *args, **kwargs):
"""Constructor for date property.
NOTE: Have to pop custom arguments from the keyword argument dictionary
to avoid corrupting argument order when sent to the superclass.
Attributes:
_string_format: An optional string, defaulting to DATE_STRING_FORMAT. This
is used to serialize using strftime and deserialize using strptime when
the date stamp is turned into a message.
"""
self._string_format = kwargs.pop('string_format', DATE_STRING_FORMAT)
super(EndpointsDateProperty, self).__init__(*args, **kwargs)
def ToValue(self, value):
"""A custom method to override the typical ProtoRPC message serialization.
Uses the string_format set on the property to serialize the date stamp.
Args:
value: A date stamp, the value of the property.
Returns:
The serialized string value of the date stamp.
"""
return value.strftime(self._string_format)
def FromValue(self, value):
"""A custom method to override the typical ProtoRPC message deserialization.
Uses the string_format set on the property to deserialize the date stamp.
Args:
value: A serialized date stamp as a string.
Returns:
The deserialized datetime.date stamp.
"""
return datetime.datetime.strptime(value, self._string_format).date()
class EndpointsTimeProperty(ndb.TimeProperty):
"""A custom time property.
Allows custom serialization of a datetime.time stamp when used to create a
message field.
"""
def __init__(self, *args, **kwargs):
"""Constructor for time property.
NOTE: Have to pop custom arguments from the keyword argument dictionary
to avoid corrupting argument order when sent to the superclass.
Attributes:
string_format: An optional string, defaulting to TIME_STRING_FORMAT. This
is used to serialize using strftime and deserialize using strptime when
the time stamp is turned into a message.
"""
self._string_format = kwargs.pop('string_format', TIME_STRING_FORMAT)
super(EndpointsTimeProperty, self).__init__(*args, **kwargs)
def ToValue(self, value):
"""A custom method to override the typical ProtoRPC message serialization.
Uses the string_format set on the property to serialize the date stamp.
Args:
value: A date stamp, the value of the property.
Returns:
The serialized string value of the time stamp.
"""
return value.strftime(self._string_format)
def FromValue(self, value):
"""A custom method to override the typical ProtoRPC message deserialization.
Uses the string_format set on the property to deserialize the time stamp.
Args:
value: A serialized time stamp as a string.
Returns:
The deserialized datetime.time stamp.
"""
return datetime.datetime.strptime(value, self._string_format).time()
class EndpointsVariantIntegerProperty(ndb.IntegerProperty):
"""A custom integer property.
Allows custom serialization of a integers by allowing variant types when used
to create a message field.
"""
def __init__(self, *args, **kwargs):
"""Constructor for integer property.
NOTE: Have to pop custom arguments from the keyword argument dictionary
to avoid corrupting argument order when sent to the superclass.
Attributes:
variant: A variant of integer types, defaulting to the default variant for
a ProtoRPC IntegerField.
"""
# The value of variant will be verified when the message field is created
self._variant = kwargs.pop('variant', messages.IntegerField.DEFAULT_VARIANT)
super(EndpointsVariantIntegerProperty, self).__init__(*args, **kwargs)
class EndpointsVariantFloatProperty(ndb.FloatProperty):
"""A custom float property.
Allows custom serialization of a float by allowing variant types when used
to create a message field.
"""
def __init__(self, *args, **kwargs):
"""Constructor for float property.
NOTE: Have to pop custom arguments from the keyword argument dictionary
to avoid corrupting argument order when sent to the superclass.
Attributes:
variant: A variant of float types, defaulting to the default variant for
a ProtoRPC FloatField.
"""
# The value of variant be verified when the message field is created
self._variant = kwargs.pop('variant', messages.FloatField.DEFAULT_VARIANT)
super(EndpointsVariantFloatProperty, self).__init__(*args, **kwargs)
class EndpointsComputedProperty(ndb.ComputedProperty):
"""A custom computed property that also considers the type of the response.
Allows NDB computed properties to be used in an EndpointsModel by also
specifying a property type.
This class can be used directly to define properties or as a decorator.
Attributes:
message_field: a value used to register the property in the property class
to proto dictionary for any model class with this property. The method
ComputedPropertyToProto is used here.
"""
message_field = ComputedPropertyToProto
@utils.positional(2)
def __init__(self, func=None, **kwargs):
"""Constructor for computed property.
NOTE: Have to pop custom arguments from the keyword argument dictionary
to avoid corrupting argument order when sent to the superclass.
Attributes:
_variant: A variant of that can be used to augment the ProtoRPC field.
property_type: A ProtoRPC field, message class or enum class that
describes the output of the alias property.
__saved_kwargs: A dictionary that can be stored on the instance if used
as a decorator rather than directly as a property.
__initialized: A boolean corresponding to whether or not the instance has
completed initialization or needs to continue when called as a
decorator.
Args:
func: The method that outputs the value of the computed property. If None,
we use this as a signal the instance is being used as a decorator.
"""
variant = kwargs.pop('variant', None)
# The value of variant will be verified when the message field is created
if variant is not None:
self._variant = variant
property_type = kwargs.pop('property_type', DEFAULT_PROPERTY_TYPE)
utils.CheckValidPropertyType(property_type)
self.property_type = property_type
if func is None:
self.__initialized = False
self.__saved_kwargs = kwargs
else:
self.__initialized = True
super(EndpointsComputedProperty, self).__init__(func, **kwargs)
def __call__(self, func):
"""Callable method to be used when instance is used as a decorator.
If called as a decorator, passes the saved keyword arguments and the func
to the constructor to complete initialization.
Args:
func: The method that outputs the value of the computed property.
Returns:
The property instance.
Raises:
TypeError: if the instance has already been initialized, either directly
as a property or as a decorator elsewhere.
"""
if self.__initialized:
raise TypeError('EndpointsComputedProperty is not callable.')
super(EndpointsComputedProperty, self).__init__(func, **self.__saved_kwargs)
del self.__saved_kwargs
# Return the property created
return self
def _set_value(self, unused_entity, unused_value):
"""Internal helper to set a value in an entity for a ComputedProperty.
Typically, on a computed property, an ndb.model.ComputedPropertyError
exception is raised when we try to set the property.
In endpoints, since we will be deserializing messages to entities, we want
to be able to call entity.some_computed_property_name = some_value without
halting code, hence this will simply do nothing.
"""
warnings.warn('Cannot assign to a ComputedProperty.', DeprecationWarning)