diff options
author | Benjamin Saller <bcsaller@objectrealms.net> | 2007-04-27 22:20:27 (GMT) |
---|---|---|
committer | Benjamin Saller <bcsaller@objectrealms.net> | 2007-04-27 22:20:27 (GMT) |
commit | fcf7ffeca964c31925c0ba275b2b09715070d541 (patch) | |
tree | de9a1a52ea31a539a4f05f6eefb2f029f8162fb2 /src/olpc/datastore/model.py |
Milestone 1 import
Diffstat (limited to 'src/olpc/datastore/model.py')
-rw-r--r-- | src/olpc/datastore/model.py | 292 |
1 files changed, 292 insertions, 0 deletions
diff --git a/src/olpc/datastore/model.py b/src/olpc/datastore/model.py new file mode 100644 index 0000000..cc4c458 --- /dev/null +++ b/src/olpc/datastore/model.py @@ -0,0 +1,292 @@ +""" +olpc.datastore.model +~~~~~~~~~~~~~~~~~~~~ +The datamodel for the metadata + +""" + +__author__ = 'Benjamin Saller <bcsaller@objectrealms.net>' +__docformat__ = 'restructuredtext' +__copyright__ = 'Copyright ObjectRealms, LLC, 2007' +__license__ = 'The GNU Public License V2+' + +from sqlalchemy import Table, Column, UniqueConstraint +from sqlalchemy import String, Integer, Unicode +from sqlalchemy import ForeignKey, Sequence, Index +from sqlalchemy import mapper, relation +from sqlalchemy import create_session +from sqlalchemy import MapperExtension, EXT_PASS +from sqlalchemy.ext.sessioncontext import SessionContext + +import datetime + +# XXX: Open issues +# list properties - Contributors (a, b, c) +# difficult to index now +# content state - searches don't include content deletion flag +# - not recording if content is on other storage yet + + +# we have a global thread local session factory +context = None + +def get_session(): return context.current + +class Content(object): + def __repr__(self): + return "<Content id:%s>" % (self.id, ) + + def __getattr__(self, key): + # mapped to property keys + session = get_session() + query = session.query(Property) + p = query.get_by(content_id=self.id, key=key) + if not p: + raise AttributeError(key) + + return p.value + + def get_properties(self, **kwargs): + session = get_session() + query = session.query(Property) + return query.select_by(content_id=self.id, **kwargs) + + + # Backingstore dependent bindings + def get_file(self): + if not hasattr(self, "_file") or self._file.closed is True: + self.backingstore.get(self.id) + return self._file + + def set_file(self, fileobj): + self._file = fileobj + file = property(get_file, set_file) + + @property + def filename(self): return self.file.name + + + def get_data(self): + f = self.file + t = f.tell() + data = f.read() + f.seek(t) + return data + + def set_data(self, filelike): + self.backingstore.set(self.id, filelike) + + data = property(get_data, set_data) + + +class BackingStoreContentMapping(MapperExtension): + """This mapper extension populates Content objects with the + binding to the backing store the files are kept on, this allow the + file-like methods to work as expected on content + """ + def __init__(self, backingstore): + MapperExtension.__init__(self) + self.backingstore = backingstore + + def populate_instance(self, mapper, selectcontext, row, instance, identitykey, isnew): + """called right before the mapper, after creating an instance + from a row, passes the row to its MapperProperty objects which + are responsible for populating the object's attributes. If + this method returns EXT_PASS, it is assumed that the mapper + should do the appending, else if this method returns any other + value or None, it is assumed that the append was handled by + this method. + + """ + instance.backingstore = self.backingstore + # allow normal population to happen + return EXT_PASS + + +class Property(object): + """A typed key value pair associated with a content object. + This is the objects metadata. The value side of the kv pair is + typically encoded as a UTF-8 String. There are however cases where + richer metadata is required by the application using the + datastore. + In these cases the type field is overridden to encode a reference + to another object that must be used to satisfy this value. An + example of this would be storing a PNG thumbnail as the a + value. In a case such as that the value should be set to a path or + key used to find the image on stable storage or in a database and + the type field will be used to demarshall it through this object. + """ + def __init__(self, key, value, type='string'): + self.key = key + self.value = value + self.type = type + + def __repr__(self): + return "<Property %s:%r of %s>" % (self.key, self.value, + self.content) + +class DateProperty(Property): + format = "YYYY-MM-DDTHH:MM:SS" + + def __init__(self, key, value, type="date"): + super(Property, self).__init__(key, value, type) + + + def get_value(self): + # parse the value back into a datetime + return datetime.datetime.strptime(self._value, self.format) + + def set_value(self, value): + self._value = value.isoformat() + + value = property(get_value, set_value) + + +class NumberProperty(Property): + def __init__(self, key, value, type="number"): + super(Property, self).__init__(key, value, type) + + def get_value(self): return float(self._value) + def set_value(self, value): self._value = value + value = property(get_value, set_value) + + + +class Model(object): + """ Manages the global state of the metadata model index. This is + intended to only be consumed by an olpc.datastore.query.QueryManager + instance for the management of its metadata. + + >>> m = Model() + >>> m.prepare(querymanager) + + >>> m.content + ... # Content Table + + >>> m['content'] + ... # content Mapper + + For details see the sqlalchemy documentation + + """ + + def __init__(self): + self.tables = {} + self.mappers = {} + + def __getattr__(self, key): return self.tables[key] + def __getitem__(self, key): return self.mappers[key] + + @property + def session(self): return get_session() + + def prepare(self, querymanager): + self.querymanager = querymanager + + # a single session manages the exclusive access we keep to the + # db. + global context + def make_session(): return create_session(bind_to=self.querymanager.db) + context = SessionContext(make_session) + + # content object + content = Table('content', + self.querymanager.metadata, + Column('id', Integer, Sequence('content_id_seq'), primary_key=True), + Column('activity_id', Integer), + Column('checksum', String,), + ) + Index('content_activity_id_idx', content.c.activity_id) + + # the properties of content objects + properties = Table('properties', + self.querymanager.metadata, + Column('id', Integer, Sequence('property_id_seq'), primary_key=True), + Column('content_id', Integer, ForeignKey('content.id')), + Column('key', Unicode, ), + Column('value', Unicode, ), + Column('type', Unicode, ), + # unique key to content mapping + UniqueConstraint('content_id', 'key', + name='property_content_key') + ) + + Index('property_key_idx', properties.c.key) + Index('property_type_idx', properties.c.type) + + # storage + storage = Table('storage', + self.querymanager.metadata, + Column('id', Integer, primary_key=True), + Column('description', String, ) + ) + + # storage -> * content + # XXX: this could be a purely runtime in-memory construct + # removing the storage table as well. Would depend in part on + # the frequency of the garbage collection runs and the + # frequency of connection to stable storage + storage_content = Table('storage_content', + self.querymanager.metadata, + Column('storage_id', Integer, ForeignKey('storage.id')), + Column('content_id', Integer, ForeignKey('content.id')), + ) + Index('idx_storage_content_content_id', storage_content.c.content_id) + + # Object Mapping + # the query manager provides a mapping extension for + # Content <-> BackingStore binding + content_mapper = mapper(Content, content, + extension=self.querymanager.content_ext, + properties = { + 'properties' : relation(Property, + cascade="all,delete-orphan", + backref='content'), + }, + + ) + + # retain reference to these tables to use for queries + self.tables['content'] = content + self.tables['properties'] = properties + self.tables['storage'] = storage + self.tables['storage_content'] = storage_content + + # and the mappers (though most likely not needed) + property_mapper = mapper(Property, properties, polymorphic_on=properties.c.type) + self.mappers['properties'] = property_mapper + self.mappers['content'] = content_mapper + + # default Property types are mapped to classes here + self.addPropertyType(DateProperty, 'date') + self.addPropertyType(NumberProperty, 'number') + + + + + + def addPropertyType(self, PropertyClass, typename, + map_value=True, **kwargs): + """Register a new type of Property. PropertyClass should be a + subclass of Property, typename is the textual + name of the new Property type. + + The flag map_value indicates if Property.value should + automatically be diverted to _value so that you can more + easily manage the interfaces 'value' as a Python property + (descriptor) + + Keyword args will be passed to the properties dictionary of + the sqlalchemy mapper call. See sqlalchemy docs for additional + details. + """ + properties = {} + properties.update(kwargs) + if map_value is True: + properties['_value'] = self.properties.c.value + + mapper(PropertyClass, + inherits=self['properties'], + polymorphic_identity=typename, + properties=properties + ) |