import datetime
import os
from tic.web.cdp import deps
TEMPLATES = {
'dojo': 'templates/command_dojo.jst',
'closure': 'templates/command_closure.jst'
}
_ALLOWED_PROPERTY_TYPES = set([
basestring,
str,
unicode,
bool,
int,
long,
float,
dict, # TODO: experimental
datetime.datetime,
datetime.date,
datetime.time
])
[docs]class Error(Exception):
"""
Base error
"""
[docs]class BadValueError(Error):
"""
Raised when property is set to an invalid value
"""
[docs]class DuplicatePropertyError(Error):
"""
Duplication error
"""
[docs]class Property(object):
"""
Base class for all command properties
"""
def __init__(self):
"""Documentation"""
self.value = None
def __get__(self, instance, owner):
"""Documentation"""
return self.value
def __set__(self, instance, value):
""" Docs """
self.value = self.validate(value)
def validate(self, value):
"""Validates if the value of the 'right' type"""
if value is not None and not isinstance(value, self.data_type):
raise BadValueError(
'Property %s must be %s instance, not a %s'
% ("self.name", self.data_type.__name__, type(value).__name__))
return value
def __property_config__(self, model_class, property_name):
"""Configure property, connecting it to its model.
Configure the property so that it knows its property name and what class
it belongs to.
Args:
model_class: Model class which Property will belong to.
property_name: Name of property within Model instance to store property
values in. By default this will be the property name preceded by
an underscore, but may change for different subclasses.
"""
self.model_class = model_class
self.name = property_name
def to_js(self):
"""Documentation"""
raise NotImplementedError
def from_js(self, value):
"""Documentation"""
raise NotImplementedError
data_type = str
[docs]class PropertiedClass(type):
"""Meta-class for initializing Model classes properties.
Used for initializing Properties defined in the context of a model.
By using a meta-class much of the configuration of a Property
descriptor becomes implicit. By using this meta-class, descriptors
that are of class Model are notified about which class they
belong to and what attribute they are associated with and can
do appropriate initialization via __property_config__.
Duplicate properties are not permitted.
"""
def __init__(cls, name, bases, dct):
"""Initializes a class that might have property definitions.
This method is called when a class is created with the PropertiedClass
meta-class.
Loads all properties for this model and its base classes in to a dictionary
for easy reflection via the 'properties' method.
Configures each property defined in the new class.
Duplicate properties, either defined in the new class or defined separately
in two base classes are not permitted.
Properties may not assigned to names which are in the list of
_RESERVED_WORDS. It is still possible to store a property using a reserved
word in the datastore by using the 'name' keyword argument to the Property
constructor.
Args:
cls: Class being initialized.
name: Name of new class.
bases: Base classes of new class.
dct: Dictionary of new definitions for class.
Raises:
DuplicatePropertyError when a property is duplicated either in the new
class or separately in two base classes.
ReservedWordError when a property is given a name that is in the list of
reserved words, attributes of Model and names of the form '__.*__'.
"""
super(PropertiedClass, cls).__init__(name, bases, dct)
_initialize_properties(cls, name, bases, dct)
def _initialize_properties(model_class, name, bases, dct):
"""Initialize Property attributes for Model-class.
Args:
model_class: Model class to initialize properties for.
"""
model_class._properties = {}
property_source = {}
def get_attr_source(name, cls):
for src_cls in cls.mro():
if name in src_cls.__dict__:
return src_cls
defined = set()
for base in bases:
if hasattr(base, '_properties'):
property_keys = set(base._properties.keys())
duplicate_property_keys = defined & property_keys
for dupe_prop_name in duplicate_property_keys:
old_source = property_source[dupe_prop_name] = get_attr_source(
dupe_prop_name, property_source[dupe_prop_name])
new_source = get_attr_source(dupe_prop_name, base)
if old_source != new_source:
raise DuplicatePropertyError(
'Duplicate property, %s, is inherited from both %s and %s.' %
(dupe_prop_name, old_source.__name__, new_source.__name__))
property_keys -= duplicate_property_keys
if property_keys:
defined |= property_keys
property_source.update(dict.fromkeys(property_keys, base))
model_class._properties.update(base._properties)
for attr_name in dct.keys():
attr = dct[attr_name]
if isinstance(attr, Property):
#check_reserved_word(attr_name)
if attr_name in defined:
raise DuplicatePropertyError('Duplicate property: %s' % attr_name)
defined.add(attr_name)
model_class._properties[attr_name] = attr
attr.__property_config__(model_class, attr_name)
[docs]class Command(object):
"""Command is the superclass of all commands.
The programming model is to declare Python subclasses of the Model class,
declaring datastore properties as class members of that class. So if you want
to publish a story with title, body, and created date, you would do it like
this:
class LoginCommand(Command):
title = StringProperty()
created = db.DateTimeProperty()
"""
__metaclass__ = PropertiedClass
def __init__(self, javascript_toolkit='dojo'):
"""init the command"""
self.javascript_toolkit = javascript_toolkit
@classmethod
[docs] def properties(cls):
"""Returns a dictionary of all the properties defined for this model."""
return dict(cls._properties)
[docs] def to_js(self):
"""Generates dojo class"""
properties = ""
length = len(self.properties().values())
for index, prop in enumerate(self.properties().values()):
properties += '%s:%s' % (prop.name, prop.to_js())
if index != length - 1:
properties += ","
vars = {
'properties': self.properties().values(),
'class_name': "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
'types': self.js_types()
}
path = os.path.join(os.path.dirname(__file__), TEMPLATES[self.javascript_toolkit])
return deps.template.render(path, vars)
[docs] def from_js(self, json):
"""Documentation"""
if isinstance(json, dict):
json_dict = json
else:
json_dict = deps.loads(json)
for key, prop in self.properties().items():
value = json_dict[key]
prop.from_js(value)
[docs] def to_json(self):
"""Documentation"""
properties = ""
length = len(self.properties().values())
for index, prop in enumerate(self.properties().values()):
properties += '%s:%s' % (prop.name, prop.to_js())
if index != length - 1:
properties += ","
return "{%s}" % properties
[docs] def js_types(self):
"""returns a set of closure_types if it is defined in the property
"""
types = set()
for key, prop in self.properties().items():
if hasattr(prop, 'closure_type'):
types.add(prop.closure_type)
return types
Result = Command
[docs]class StringProperty(Property):
data_type = basestring
def to_js(self):
"""Documentation"""
from tic.utils.simplejson.encoder import encode_basestring
if self.value is None:
self.value = ""
return encode_basestring(self.value)
def from_js(self, value):
"""Documentation"""
self.value = "" if value is None else value
[docs]class IntegerProperty(Property):
data_type = int
def to_js(self):
"""Documentation"""
from tic.utils.simplejson.encoder import encode_basestring
if self.value is None:
self.value = "undefined"
return self.value
def from_js(self, value):
"""Documentation"""
self.value = "undefined" if value is None else value
[docs]class LongProperty(IntegerProperty):
data_type = long
[docs]class DateTimeProperty(Property):
data_type = datetime.datetime
closure_type = 'goog.date.DateTime'
def to_js(self):
"""Documentation"""
return "null" if self.value is None else "new Date(%i)" % self._get_unix_epoch()
def from_js(self, value):
"""Documentation"""
v = None if value is None else datetime.datetime.fromtimestamp(value / 1000)
self.value = self.validate(v)
def _get_unix_epoch(self):
"""
returns the unix epoch in milliseconds
"""
from time import mktime
return int((mktime(self.value.timetuple()) + 1e-6 * self.value.microsecond) * 1000)
[docs]class ListProperty(Property):
data_type = list
def __init__(self, item_type):
"""Construct ListProperty.
Args:
item_type: Type for the list items; must be one of the allowed property
types.
verbose_name: Optional verbose name.
default: Optional default value; if omitted, an empty list is used.
**kwds: Optional additional keyword arguments, passed to base class.
Note that the only permissible value for 'required' is True.
"""
if item_type is str:
item_type = basestring
if not isinstance(item_type, type):
raise TypeError('Item type should be a type object')
if item_type not in _ALLOWED_PROPERTY_TYPES:
raise ValueError('Item type %s is not acceptable' % item_type.__name__)
self.item_type = item_type
super(ListProperty, self).__init__()
def to_js(self):
"""
generates js for now it only allows str
"""
return deps.dumps(self.value)
def from_js(self, value):
return self.validate(deps.loads(value))
def validate(self, value):
"""Validate list.
Returns:
A valid value.
Raises:
BadValueError if property is not a list whose items are instances of
the item_type given to the constructor.
"""
value = super(ListProperty, self).validate(value)
value = self.validate_list_contents(value)
return value
def validate_list_contents(self, value):
"""Validates that all items in the list are of the correct type.
Returns:
The validated list.
Raises:
BadValueError if the list has items are not instances of the
item_type given to the constructor.
"""
if self.item_type in (int, long):
item_type = (int, long)
else:
item_type = self.item_type
for item in value:
if not isinstance(item, item_type):
if item_type == (int, long):
raise BadValueError('Items in the %s list must all be integers.' %
self.name)
else:
raise BadValueError(
'Items in the %s list must all be %s instances' %
(self.name, self.item_type.__name__))
return value
[docs]class DictProperty(ListProperty):
data_type = dict
def __init__(self, item_type):
"""Construct DictProperty.
Args:
item_type: Type for the list items; must be one of the allowed property
types.
verbose_name: Optional verbose name.
default: Optional default value; if omitted, an empty list is used.
**kwds: Optional additional keyword arguments, passed to base class.
Note that the only permissible value for 'required' is True.
"""
super(DictProperty, self).__init__(item_type)
def validate(self, value):
"""Validate list.
Returns:
A valid value.
Raises:
BadValueError if property is not a list whose items are instances of
the item_type given to the constructor.
"""
value = super(DictProperty, self).validate(value)
value = self.validate_dict_contents(value)
return value
def validate_dict_contents(self, value):
"""Validates that all items in the list are of the correct type.
Returns:
The validated dict.
Raises:
BadValueError if the list has items are not instances of the
item_type given to the constructor.
"""
self.validate_list_contents(value.values())
return value