Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/sugar_network/document/metadata.py
diff options
context:
space:
mode:
Diffstat (limited to 'sugar_network/document/metadata.py')
-rw-r--r--sugar_network/document/metadata.py394
1 files changed, 394 insertions, 0 deletions
diff --git a/sugar_network/document/metadata.py b/sugar_network/document/metadata.py
new file mode 100644
index 0000000..c79f810
--- /dev/null
+++ b/sugar_network/document/metadata.py
@@ -0,0 +1,394 @@
+# Copyright (C) 2011-2012, Aleksey Lim
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import types
+import cPickle as pickle
+from os.path import exists
+
+from active_document import env
+from active_toolkit import enforce
+
+
+_LIST_TYPES = (list, tuple, frozenset)
+
+
+def active_property(property_class=None, *args, **kwargs):
+
+ def getter(func, self):
+ value = self[func.__name__]
+ return func(self, value)
+
+ def decorate_setter(func, attr):
+ attr.prop.setter = lambda self, value: \
+ self.set(attr.name, func(self, value))
+ attr.prop.on_set = func
+ return attr
+
+ def decorate_getter(func):
+ enforce(func.__name__ != 'guid',
+ "Active property should not have 'guid' name")
+ attr = lambda self: getter(func, self)
+ attr.setter = lambda func: decorate_setter(func, attr)
+ attr._is_active_property = True
+ attr.name = func.__name__
+ attr.prop = (property_class or ActiveProperty)(
+ attr.name, *args, **kwargs)
+ attr.prop.on_get = func
+ return attr
+
+ return decorate_getter
+
+
+class Metadata(dict):
+ """Structure to describe the document.
+
+ Dictionary derived class that contains `Property` objects.
+
+ """
+
+ def __init__(self, cls):
+ """
+ :param cls:
+ class inherited from `active_document.Document`
+
+ """
+ self._name = cls.__name__.lower()
+
+ slots = {}
+ prefixes = {}
+
+ for attr in [getattr(cls, i) for i in dir(cls)]:
+ if not hasattr(attr, '_is_active_property'):
+ continue
+
+ prop = attr.prop
+
+ if hasattr(prop, 'slot'):
+ enforce(prop.slot is None or prop.slot not in slots,
+ 'Property %r has a slot already defined for %r in %r',
+ prop.name, slots.get(prop.slot), self.name)
+ slots[prop.slot] = prop.name
+
+ if hasattr(prop, 'prefix'):
+ enforce(not prop.prefix or prop.prefix not in prefixes,
+ 'Property %r has a prefix already defined for %r',
+ prop.name, prefixes.get(prop.prefix))
+ prefixes[prop.prefix] = prop.name
+
+ if prop.setter is not None:
+ setattr(cls, attr.name, property(attr, prop.setter))
+ else:
+ setattr(cls, attr.name, property(attr))
+
+ self[prop.name] = prop
+
+ @property
+ def name(self):
+ """Document type name."""
+ return self._name
+
+ def __getitem__(self, prop_name):
+ enforce(prop_name in self, 'There is no %r property in %r',
+ prop_name, self.name)
+ return dict.__getitem__(self, prop_name)
+
+
+class PropertyMeta(dict):
+
+ BLOB_SUFFIX = '.blob'
+
+ def __init__(self, path_=None, **meta):
+ if path_:
+ with file(path_) as f:
+ meta.update(pickle.load(f))
+ if exists(path_ + PropertyMeta.BLOB_SUFFIX):
+ meta['path'] = path_ + PropertyMeta.BLOB_SUFFIX
+ meta['mtime'] = os.stat(path_).st_mtime
+ dict.__init__(self, meta)
+
+ @classmethod
+ def is_blob(cls, blob):
+ return isinstance(blob, (type(None), basestring, cls)) or \
+ hasattr(blob, 'read')
+
+
+class Property(object):
+ """Bacis class to collect information about document property."""
+
+ def __init__(self, name, permissions=env.ACCESS_PUBLIC, typecast=None,
+ reprcast=None, default=None):
+ self.setter = None
+ self.on_get = lambda self, x: x
+ self.on_set = None
+ self._name = name
+ self._permissions = permissions
+ self._typecast = typecast
+ self._reprcast = reprcast
+ self._default = default
+
+ @property
+ def name(self):
+ """Property name."""
+ return self._name
+
+ @property
+ def permissions(self):
+ """Specify access to the property.
+
+ Value might be ORed composition of `active_document.ACCESS_*`
+ constants.
+
+ """
+ return self._permissions
+
+ @property
+ def typecast(self):
+ """Cast property value before storing in the system.
+
+ Supported values are:
+ * `None`, string values
+ * `int`, interger values
+ * `float`, float values
+ * `bool`, boolean values repesented by symbols `0` and `1`
+ * sequence of strings, property value should confirm one of values
+ from the sequence
+
+ """
+ return self._typecast
+
+ @property
+ def composite(self):
+ """Is property value a list of values."""
+ is_composite, __ = _is_composite(self.typecast)
+ return is_composite
+
+ @property
+ def default(self):
+ """Default property value or None."""
+ return self._default
+
+ def decode(self, value):
+ """Convert property value according to its `typecast`."""
+ if self.typecast is None:
+ return value
+ return _decode(self.typecast, value)
+
+ def to_string(self, value):
+ """Convert value to list of strings ready to index."""
+ result = []
+
+ if self._reprcast is not None:
+ value = self._reprcast(value)
+
+ for value in (value if type(value) in _LIST_TYPES else [value]):
+ if type(value) is bool:
+ value = int(value)
+ if type(value) is unicode:
+ value = unicode(value).encode('utf8')
+ else:
+ value = str(value)
+ result.append(value)
+
+ return result
+
+ def assert_access(self, mode):
+ """Is access to the property permitted.
+
+ If there are no permissions, function should raise
+ `active_document.Forbidden` exception.
+
+ :param mode:
+ one of `active_document.ACCESS_*` constants
+ to specify the access mode
+
+ """
+ enforce(mode & self.permissions, env.Forbidden,
+ '%s access is disabled for %r property',
+ env.ACCESS_NAMES[mode], self.name)
+
+
+class BrowsableProperty(object):
+ """Property that can be listed while browsing documents."""
+ pass
+
+
+class StoredProperty(Property, BrowsableProperty):
+ """Property that can be saved in persistent storare."""
+
+ def __init__(self, name, localized=False, typecast=None, reprcast=None,
+ **kwargs):
+ self._localized = localized
+
+ if localized:
+ enforce(typecast is None,
+ 'typecast should be None for localized properties')
+ enforce(reprcast is None,
+ 'reprcast should be None for localized properties')
+ typecast = _localized_typecast
+ reprcast = _localized_reprcast
+
+ Property.__init__(self, name, typecast=typecast, reprcast=reprcast,
+ **kwargs)
+
+ @property
+ def localized(self):
+ """Property value will be stored per locale."""
+ return self._localized
+
+
+class ActiveProperty(StoredProperty):
+ """Property that need to be indexed."""
+
+ def __init__(self, name, slot=None, prefix=None, full_text=False,
+ boolean=False, **kwargs):
+ enforce(name == 'guid' or slot != 0,
+ "For %r property, slot '0' is reserved for internal needs",
+ name)
+ enforce(name == 'guid' or prefix != env.GUID_PREFIX,
+ 'For %r property, prefix %r is reserved for internal needs',
+ name, env.GUID_PREFIX)
+ enforce(slot is not None or prefix or full_text,
+ 'For %r property, either slot, prefix or full_text '
+ 'need to be set',
+ name)
+ enforce(slot is None or _is_sloted_prop(kwargs.get('typecast')),
+ 'Slot can be set only for properties for str, int, float, '
+ 'bool types, or, for list of these types')
+
+ StoredProperty.__init__(self, name, **kwargs)
+ self._slot = slot
+ self._prefix = prefix
+ self._full_text = full_text
+ self._boolean = boolean
+
+ @property
+ def slot(self):
+ """Xapian document's slot number to add property value to."""
+ return self._slot
+
+ @property
+ def prefix(self):
+ """Xapian serach term prefix, if `None`, property is not a term."""
+ return self._prefix
+
+ @property
+ def full_text(self):
+ """Property takes part in full-text search."""
+ return self._full_text
+
+ @property
+ def boolean(self):
+ """Xapian will use boolean search for this property."""
+ return self._boolean
+
+
+class BlobProperty(Property):
+ """Binary large objects that need to be fetched alone.
+
+ To get access to these properties, use `Document.send()` and
+ `Document.receive()` functions.
+
+ """
+
+ def __init__(self, name, permissions=env.ACCESS_PUBLIC,
+ mime_type='application/octet-stream', composite=False):
+ Property.__init__(self, name, permissions=permissions)
+ self._mime_type = mime_type
+ self._composite = composite
+
+ @property
+ def mime_type(self):
+ """MIME type for BLOB content.
+
+ By default, MIME type is application/octet-stream.
+
+ """
+ return self._mime_type
+
+ @property
+ def composite(self):
+ """Property is a list of BLOBs."""
+ return self._composite
+
+
+def _is_composite(typecast):
+ if type(typecast) in _LIST_TYPES:
+ if typecast:
+ first = iter(typecast).next()
+ if type(first) is not type and \
+ type(first) not in _LIST_TYPES:
+ return False, True
+ return True, False
+ return False, False
+
+
+def _decode(typecast, value):
+ enforce(value is not None, ValueError, 'Property value cannot be None')
+
+ is_composite, is_enum = _is_composite(typecast)
+
+ if is_composite:
+ enforce(len(typecast) <= 1, ValueError,
+ 'List values should contain values of the same type')
+ if type(value) not in _LIST_TYPES:
+ value = (value,)
+ typecast, = typecast or [str]
+ value = tuple([_decode(typecast, i) for i in value])
+ elif is_enum:
+ enforce(value in typecast, ValueError,
+ "Value %r is not in '%s' list",
+ value, ', '.join([str(i) for i in typecast]))
+ elif isinstance(typecast, types.FunctionType):
+ value = typecast(value)
+ elif typecast is str:
+ if isinstance(value, unicode):
+ value = value.encode('utf-8')
+ else:
+ value = str(value)
+ elif typecast is int:
+ value = int(value)
+ elif typecast is float:
+ value = float(value)
+ elif typecast is bool:
+ value = bool(value)
+ elif typecast is dict:
+ value = dict(value)
+ else:
+ raise ValueError('Unknown typecast')
+ return value
+
+
+def _is_sloted_prop(typecast):
+ if typecast in [None, int, float, bool, str]:
+ return True
+ if type(typecast) in _LIST_TYPES:
+ if typecast and [i for i in typecast
+ if type(i) in [None, int, float, bool, str]]:
+ return True
+
+
+def _localized_typecast(value):
+ if isinstance(value, dict):
+ return value
+ else:
+ return {env.DEFAULT_LANG: value}
+
+
+def _localized_reprcast(value):
+ if isinstance(value, dict):
+ return value.values()
+ else:
+ return [value]