def run(neo): import neo4j class BaseObject(object): @property def neo_service(self): return neo transactional = neo4j.transactional(BaseObject.neo_service) class MyService(BaseObject): @transactional def make_entity(self, name): return Entity( self.neo_service.node(name=name) ) class Entity(BaseObject): def __init__(self, node): self._node = node def __get__name(self): return self._node['name'] def __set__name(self, name): self._node['name'] = name name = transactional( property(__get__name, __set__name) ) perform( neo, simple_test(test, MyService(), name=__name__) )
def __bootstrap__(pyneo): global NodeModel, Relationship, Property # XXX: move these inwards global NeoServiceProperty, Incoming, Outgoing try: from django.db import models as django from django.db.models.fields.related import add_lazy_relation except: import traceback; traceback.print_exc() raise ImportError( "The Django models for Neo4j can only be used from within Django.") from neo4j import Incoming, Outgoing, transactional import itertools class NeoServiceProperty(object): def __get__(self, obj, cls=None): return DjangoNeo.neo class not_implemented(object): def __init__(self, method): self.__method = method if method.__doc__: self.__doc = "\n" + method.__doc__ else: self.__doc = '' self.__repr = method.__name__ def contribute_to_class(self, cls, name): self.set_repr(cls, name) setattr(cls, name, self) def set_repr(self, cls, name): self.__repr = '<%s>.%s()' % (cls.__name__, name) def __get__(self, obj, cls): if obj is None: return self if not '.' in self.__repr: if cls is None: cls = type(obj) self.set_repr(cls, self.__repr) def closure(*args, **kwargs): self(obj, *args, **kwargs) return closure def __call__(self, *args, **kwargs): self.__method(*args, **kwargs) # this is to verify signature raise NotImplementedError(self.__repr + self.__doc) transactional = transactional(NeoServiceProperty()) class LazyModel(object): def __init__(self, cls, field, name, setup_reversed): add_lazy_relation(cls, field, name, self.__setup) self.__setup_reversed = setup_reversed def __setup(self, field, target, source): if not issubclass(target, NodeModel): raise TypeError("Relationships may only extend from Nodes.") self.__target = target self.__setup_reversed(target) __target = None @property def __model(self): model = self.__target if model is None: raise ValueError("Lazy model not initialized!") else: return model def __getattr__(self, attr): return getattr(self.__model, attr) def __call__(self, *args, **kwargs): return self.__model(*args, **kwargs) class DjangoNeo(object): def __init__(self): self.__field_counter = 0 @property def field_counter(self): res = self.__field_counter self.__field_counter += 1 return res @property def neo(self): try: return self.__neo except: return self.__setup_neo() def __setup_neo(self): from django.conf import settings from neo4j import NeoService import os try: resource_uri = settings.NEO4J_RESOURCE_URI assert resource_uri, "the resource_uri must be defined" except: raise ValueError("NEO4J_RESOURCE_URI is not defined in " "the settings module.") options = getattr(settings, 'NEO4J_OPTIONS', {}) self.__neo = NeoService(resource_uri, **options) return self.__neo def index(self, propname): # TODO: add the ability to choose index type return self.neo.index(propname, create=True) @transactional def type_node(self, app_label, model_name): for relationship in self.neo.reference_node.type_node: if (relationship['app_label'] == app_label and relationship['model_name'] == model_name): type_node = relationship.end break else: type_node = self.neo.node() self.neo.reference_node.type_node(type_node, app_label=app_label, model_name=model_name,) return type_node @transactional def apply_to_buffer(self, constructor, items, size=1): result = [constructor(item) for item in itertools.takewhile(countdown(size), items)] if not result: raise StopIteration return result @property def log(self): if False: pass else: return self.__log class __log(object): def __getattr__(self, attr): def logger(message, *args, **kwargs): pass logger.__name__ = attr return logger __log = __log() DjangoNeo = DjangoNeo() # singleton def write_through(obj): return getattr(getattr(obj,'_meta',None),'write_through', False) def all_your_base(cls, base): if issubclass(cls, base): yield cls for parent in cls.__bases__: for cls in all_your_base(parent, base): yield cls def countdown(number): counter = itertools.count() def done(*junk): for count in counter: return count < number return done def buffer_iterator(constructor, items, size=1): items = iter(items) # make sure we have an iterator while 1: for item in DjangoNeo.apply_to_buffer(constructor, items, size): yield item @pyneo.make def NodeModel(): class NodeModelManager(django.Manager): def get_query_set(self): return NodeQuerySet(self.model) @not_implemented def _insert(self, values, **kwargs): pass @not_implemented def _update(self, values, **kwargs): pass class IdProperty(object): def __init__(self, getter, setter): self.getter = getter self.setter = setter def __get__(self, inst, cls): if inst is None: return IdLookup(cls) else: return self.getter(inst) def __set__(self, inst, value): return self.setter(inst, value) class IdLookup(object): indexed = True unique = True def __init__(self, model): self.__model = model index = property(lambda self: self) def to_neo(self, value): return int(value) def nodes(self, nodeid): try: node = DjangoNeo.neo.node[nodeid] except: node = None else: type_node = DjangoNeo.type_node( self.__model._meta.app_label, self.__model.__name__) for rel in node.relationships('<<INSTANCE>>').incoming: # verify that the found node is an instance of the # requested type if rel.start == type_node: break # ok, it is! else: # no, it isn't! node = None if node is not None: yield node class NodeModel(django.Model): """Extend to make models""" objects = NodeModelManager() class Meta: abstract = True @classmethod def _neo4j_instance(cls, node): instance = cls.__new__(cls) instance.__node = node return instance #def __init__(self, *args, **kwargs): # self.__node = Neo4jDjangoModel.neo.node() # super(NodeModel, self).__init__(*args, **kwargs) def _get_pk_val(self, meta=None): return self.__node.id def _set_pk_val(self, value): if self.__node is None and value is None: return raise TypeError("Cannot change the id of nodes.") pk = id = IdProperty(_get_pk_val, _set_pk_val) def __eq__(self, other): try: return self.__node == other.__node except: return False __node = None @property def node(self): node = self.__node if node is None: # XXX: come up with a better exception type raise ValueError("Unsaved objects have no nodes.") else: return node _neo4j_underlying = node @transactional def save_base(self, raw=False, cls=None, origin=None, force_insert=False, force_update=False): assert not (force_insert and force_update) if cls: DjangoNeo.log.debug("save_base: cls=%s", cls) if origin: DjangoNeo.log.debug("save_base: origin=%s", origin) self._save_neo4j_node() self._save_neo4j_Properties(self, self.__node) self._save_neo4j_Relationships(self, self.__node) save_base.alters_data = True @transactional def _save_neo4j_node(self): if self.__node is None: self.__node = node = DjangoNeo.neo.node() for type_node in self.__all_type_nodes(): type_node.relationships('<<INSTANCE>>')(node) return self.__node _save_neo4j_node.alters_data = True @classmethod def _neo4j_type_node(cls): assert not cls == NodeModel, "only defined models have a type" try: node = cls.__type_node except: node=DjangoNeo.type_node(cls._meta.app_label,cls.__name__) cls.__type_node = node return node @classmethod def __all_type_nodes(cls): for cls in all_your_base(cls, NodeModel): if cls != NodeModel: yield cls._neo4j_type_node() def _collect_sub_objects(self,seen_objs,parent=None,nullable=False): raise TypeError( "NodeModel does not support _collect_sub_objects") @transactional @not_implemented def delete(self): pass delete.alters_data = True @transactional def _get_next_or_previous_by_FIELD(self, field, is_next, **kwargs): raise NotImplementedError("<NodeModel>.next/previous by %s" % field.attname) @transactional def _get_next_or_previous_in_order(self, is_next): raise NotImplementedError("<NodeModel>.next/previous") class NodeQuerySet(object): def __init__(self, model): self.model = model def __nodes(self): type_node = self.model._neo4j_type_node() for relationship in type_node.relationships('<<INSTANCE>>'): yield relationship.end def __iter__(self): return buffer_iterator(self.model._neo4j_instance, self.__nodes(), size=10) # count # dates # distinct # extra def create(self, **kwargs): obj = self.model(**kwargs) obj.save(force_insert=True) return obj @transactional def get(self, **lookup): resultset = self indexes = [] index = None for key, value in lookup.items(): if value is None: continue # None cannot be used in indexes attribute = getattr(self.model, key) if attribute.indexed: if attribute.unique: index = attribute.index break else: indexes.append((key, attribute.index)) else: if indexes: # XXX: select most appropriate index based on # number of entries for the key key, index = indexes.pop() if index: value = lookup.pop(key) value = attribute.to_neo(value) resultset = index.nodes(value) if lookup: # TODO: filter for the remaining ones raise NotImplementedError("filtering of query set") result = None for item in resultset: if result is not None: if index: lookup[key] = value raise self.model.MultipleObjectsReturned( "get() returned more than one %s. " "Lookup parameters were %s" % (self.model._meta.object_name, lookup)) else: result = item if result is None: if index: lookup[key] = value raise self.model.DoesNotExist( "%s matching query does not exist." "Lookup parameters were %s" % (self.model._meta.object_name, lookup)) return self.model._neo4j_instance(result) # get_or_create # filter # aggregate # annotate # complex_filter # exclude # in_bulk # iterator # latest # latest # order_by # select_related # values # values_list # update # reverse # defer # only return NodeModel @pyneo.make def Relationship(): class Meta(type): def __new__(meta, name, bases, body): new = super(Meta, meta).__new__ parents = [cls for cls in bases if isinstance(cls, Meta)] if not parents: # this is the base class return new(meta, name, bases, body) module = body.pop('__module__') modelbases = [cls.Model for cls in parents if hasattr(cls, 'Model')] Model = RelationshipModel.new(module, name, modelbases) for key, value in body.items(): if hasattr(value, 'contribute_to_class'): value.contribute_to_class(Model, key) else: setattr(Model, key, value) return new(meta, name, bases, { '__module__': module, 'Model': Model, }) def __getattr__(cls, key): if hasattr(cls, 'Model'): return getattr(cls.Model, key) else: raise AttributeError(key) def __setattr__(cls, key, value): if hasattr(cls, 'Model'): setattr(cls.Model, key, value) else: raise TypeError( "Cannot assign attributes to base Relationship") class RelationshipModel(object): __relationship = None def __init__(self): pass @property def relationship(self): rel = self.__relationship if rel is None: # XXX: better exception raise ValueError("Unsaved objects have no relationship.") else: return rel _neo4j_underlying = relationship @classmethod def new(RelationshipModel, module, name, bases): return type(name, bases + [RelationshipModel], { '__module__': module,}) @classmethod def add_field(self, prop): raise NotImplementedError("<RelationshipModel>.add_field()") class Relationship(object): """Extend to add properties to relationships.""" __metaclass__ = Meta def __init__(self, target, type=None, direction=None, optional=True, single=False, related_single=False, related_name=None): if related_name is None: if related_single: related_name = '%(name)s' else: related_name = '%(name)s_set' if not pyneo.python.is_string(type): if direction is not None: if type.direction is not direction: raise TypeError("Incompatiable direction!") else: direction = type.direction type = type.type self.__target = target self.__name = type self.__single = single self._direction = direction self.creation_counter = DjangoNeo.field_counter self.__related_single = related_single self.reversed_name = related_name target = property(lambda self: self.__target) __is_reversed = False def reverse(self, target, name): if self._direction is Incoming: direction = Outgoing elif self._direction is Outgoing: direction = Incoming else: direction = None relationship = Relationship( target, type=self.__name, direction=direction, single=self.__related_single, related_name=name) relationship.__is_reversed = True return relationship def contribute_to_class(self, source, name): if not issubclass(source, NodeModel): raise TypeError("Relationships may only extend from Nodes.") if pyneo.python.is_string(self.__target): target = LazyModel(source, self, self.__target, lambda target: bound._setup_reversed(target)) else: target = self.__target if hasattr(self, 'Model'): if self.__single: Bound = SingleRelationship else: Bound = MultipleRelationships bound = Bound(self, source, self.__name or name, name, self.Model) else: if self.__single: Bound = SingleNode else: Bound = MultipleNodes bound = Bound(self, source, self.__name or name, name) source._meta.add_field(bound) setattr(source, name, bound) if not self.__is_reversed: bound._setup_reversed(target) class BoundRelationship(object): indexed = False rel = None primary_key = False choices = None db_index = None def __init__(self, rel, source, relname, attname): self.__rel = rel self.__source = source self._type = relname self.__attname = attname relationships = self.__relationships_for(source) relationships[self.name] = self # XXX weekref def _setup_reversed(self, target): self.__target = target if not isinstance(target, LazyModel): self.__rel.reverse(self.__source, self.__attname).contribute_to_class( target, self.__reversed_name) attname = name = property(lambda self: self.__attname) _direction = property(lambda self: self.__rel._direction) _target_model = property(lambda self: self.__rel.target) __reversed_name = property(lambda self: self.__rel.reversed_name) def get_default(self): return None @staticmethod def __state_for(instance, create=True): try: state = instance.__state except: state = {} if create: instance.__state = state return state @staticmethod def __relationships_for(obj_or_cls): meta = obj_or_cls._meta try: relationships = meta.__relationships except: meta.__relationships = relationships = {} return relationships def _save_(instance, node): state = BoundRelationship.__state_for(instance, create=False) if state: rels = BoundRelationship.__relationships_for(instance) for key, value in state.items(): rels[key]._save_relationship(instance, node, value) NodeModel._save_neo4j_Relationships = staticmethod(_save_) del _save_ @not_implemented def _save_relationship(self, instance, node, state): pass def _get_all_relationships(self, node): return self.__load_relationships(node) def __load_relationships(self, this): relationships = this.relationships(self._type) if self._direction is Incoming: relationships = relationships.incoming elif self._direction is Outgoing: relationships = relationships.outgoing return relationships creation_counter = property(lambda self:self.__rel.creation_counter) def __cmp__(self, other): return cmp(self.creation_counter, other.creation_counter) def __get__(self, obj, cls=None): if obj is None: return self return self._get_relationship(obj, self.__state_for(obj)) def __set__(self, obj, value): self._set_relationship(obj, self.__state_for(obj), value) def __delete__(self, obj): self._del_relationship(obj, self.__state_for(obj)) def _set_relationship(self, obj, state, value): if value is None: # assume initialization - ignore return # TODO: verify that obj is unsaved! raise TypeError("<%s>.%s is not assignable" % (obj.__class__.__name__, self.name)) def _del_relationship(self, obj, state): raise TypeError("Cannot delete <%s>.%s" % (obj.__class__.__name__, self.name)) class SingleNode(BoundRelationship): def _get_relationship(self, obj, state): if self.name in state: changed, result = state[self.name] else: try: this = obj.node except: result = None else: result = self.__load_related(this) state[self.name] = False, result return result @transactional def __load_related(self, this): relationship = self.__load_relationships(this).single if relationship is None: return None return self._neo4j_instance(this, relationship) def _neo4j_instance(self, this, relationship): that = relationship.getOtherNode(this) return self._target_model._neo4j_instance(that) def __load_relationships(self, this): relationships = this.relationships(self._type) if self._direction is Incoming: relationships = relationships.incoming elif self._direction is Outgoing: relationships = relationships.outgoing return relationships def _del_relationship(self, obj, state): self._set_relationship(obj, state, None) def _set_relationship(self, obj, state, other): state[self.name] = True, other def _save_relationship(self, instance, node, state): changed, other = state if not changed: return if other is None: del self.__load_relationships(node).single else: relationships = self.__load_relationships(node) relationships.single = other._save_neo4j_node() class BoundRelationshipModel(BoundRelationship): def __init__(self, rel, cls, relname, attname, Model): super(BoundRelationship, self).__init__( rel, cls, relname, attname) self.Model = Model raise NotImplementedError("Support for extended relationship " "models is not implemented yet.") class SingleRelationship(BoundRelationshipModel): # WAIT! @not_implemented def _get_relationship(self, obj, state): pass @not_implemented def _set_relationship(self, obj, state, other): pass class MultipleNodes(BoundRelationship): def _get_relationship(self, obj, states): state = states.get(self.name) if state is None: states[self.name] = state = RelationshipInstance(self, obj) return state #return RelationshipInstance(self, obj, state) #this = obj.node #for rel in this.relationships(self._type): # that = rel.getOtherNode(this) # yield self._NodeModel(that) def _neo4j_instance(self, this, relationship): that = relationship.getOtherNode(this) return self._target_model._neo4j_instance(that) def accept(self, obj): pass # TODO: implement verification def _save_relationship(self, instance, node, state): state.__save__(node) def _create_relationship(self, node, obj): other = obj._save_neo4j_node() # TODO: verify that it's ok in the reverse direction self.__load_relationships(node)( other ) def __load_relationships(self, this): relationships = this.relationships(self._type) if self._direction is Incoming: relationships = relationships.incoming elif self._direction is Outgoing: relationships = relationships.outgoing return relationships @not_implemented def _extract_relationship(self, obj): pass class MultipleRelationships(BoundRelationshipModel): # WAIT! @not_implemented def _get_relationship(self, obj, state): pass @not_implemented def add(self, obj, other): pass class RelationshipInstance(django.Manager): def __init__(self, rel, obj): self.__rel = rel self.__obj = obj self.__added = [] # contains domain objects self.__removed = pyneo.python.Set() # contains relationships def __save__(self, node): for relationship in self.__removed: relationship.delete() for obj in self.__added: self.__rel._create_relationship(node, obj) self.__removed.clear() self.__added[:] = [] def _neo4j_relationships(self, node): for rel in self.__rel._get_all_relationships(node): if rel not in self.__removed: yield rel @property def _new(self): for item in self.__added: yield item def add(self, *objs): for obj in objs: self.__rel.accept(obj) self.__added.extend(objs) def remove(self, *objs): for obj in objs: self.__removed.add( self.__rel._extract_relationship(obj) ) @not_implemented def clear(self): pass @not_implemented def create(self, *args, **kwargs): pass @not_implemented def get_or_create(self, *args, **kwargs): pass def get_query_set(self): return RelationshipQuerySet(self, self.__rel, self.__obj) class RelationshipQuerySet(object): def __init__(self, inst, rel, obj): self.__inst = inst self.__rel = rel self.__obj = obj def __relationships(self, node): for rel in self.__inst._neo4j_relationships(node): if self.__keep_relationship(rel): yield rel def __iter__(self): try: node = self.__obj.node except: pass else: buffered = buffer_iterator( lambda rel: self.__rel._neo4j_instance(node, rel), self.__relationships(node), size=10) for item in buffered: yield item for item in self.__inst._new: if self.__keep_instance(item): yield item def __keep_instance(self, obj): return True # TODO: filtering def __keep_relationship(self, rel): return True # TODO: filterning @not_implemented def get(self, **lookup): pass return Relationship @pyneo.make def Property(): class Property(object): """Extend to create properties of specific types.""" def __init__(self,indexed=False,unique=False,type=None,name=None): self.indexed = indexed self.unique = unique self.__name = name self.creation_counter = DjangoNeo.field_counter @property def default(self): return None # TODO: perhaps add better code here def to_neo(self, value): return value def from_neo(self, value): return value def contribute_to_class(self, cls, name): if issubclass(cls, NodeModel): prop = BoundProperty(self, cls, self.__name or name, name) cls._meta.add_field(prop) elif issubclass(cls, Relationship): if self.indexed: raise TypeError( "Relationship properties may not be indexed.") prop = BoundProperty(self, cls, self.__name or name) cls.add_field(prop) else: raise TypeError("Properties may only be added to Nodes" " or Relationships") setattr(cls, name, prop) class BoundProperty(object): rel = None primary_key = False choices = None # TODO: add support for this def __init__(self, property, cls, propname, attname): self.__property = property self.__class = cls self.__propname = propname self.__attname = attname properties = self.__properties_for(cls) properties[self.name] = self # XXX: weakref creation_counter = property(lambda self: self.__property.creation_counter) attname = name = property(lambda self: self.__attname) convert = property(lambda self: self.__property.convert) indexed = db_index = property(lambda self: self.__property.indexed) unique = property(lambda self: self.__property.unique) to_neo = property(lambda self: self.__property.to_neo) from_neo = property(lambda self: self.__property.from_neo) @property def index(self): if not self.indexed: raise TypeError("'%s' is not indexed" % (self.__propname,)) try: index = self.__index except: index_name = "%s %s %s" % ( self.__class._meta.app_label, self.__class.__name__, self.__propname,) self.__index = index = DjangoNeo.index(index_name) return index def get_default(self): return self.__property.default def __cmp__(self, other): return cmp(self.creation_counter, other.creation_counter) @staticmethod def __values_of(instance, create=True): try: values = instance.__values except: values = {} if create: instance.__values = values return values @staticmethod def __properties_for(obj_or_cls): meta = obj_or_cls._meta try: properties = meta.__properties except: meta.__properties = properties = {} return properties def _save_(instance, node): values = BoundProperty.__values_of(instance) if values: properties = BoundProperty.__properties_for(instance) for key, value in values.items(): self = properties[key] old, value = self.__set_value(instance, value) if self.indexed: if self.unique and value is not None: old_node = self.index[value] if old_node and old_node != node: raise ValueError( "Duplicate index entries for <%s>.%s" % (instance.__class__.__name__, self.name)) if old is not None: self.index.remove(old, node) if value is not None: self.index.add(value, node) values.clear() NodeModel._save_neo4j_Properties = staticmethod(_save_) del _save_ def __get__(self, instance, cls=None): if instance is None: return self values = self.__values_of(instance, create=False) if self.__propname in values: return values[self.__propname] else: return self.__get_value(instance) def __set__(self, instance, value): if write_through(instance): self.___set_value(instance, value) else: values = self.__values_of(instance) values[self.__propname] = value @transactional def __get_value(self, instance): try: node = instance._neo4j_underlying except: # no node existed pass else: try: return self.__property.from_neo(node[self.__propname]) except: # no value set on node pass return self.get_default() # fall through: default value @transactional def __set_value(self, instance, value): value = self.__property.to_neo(value) underlying = instance._neo4j_underlying try: old = underlying[self.__propname] except: old = None underlying[self.__propname] = value return old, value return Property
def __bootstrap__(pyneo): global NodeModel, Relationship, Property # XXX: move these inwards global NeoServiceProperty, Incoming, Outgoing try: from django.db import models as django from django.db.models.fields.related import add_lazy_relation except: import traceback traceback.print_exc() raise ImportError( "The Django models for Neo4j can only be used from within Django.") from neo4j import Incoming, Outgoing, transactional import itertools class NeoServiceProperty(object): def __get__(self, obj, cls=None): return DjangoNeo.neo class not_implemented(object): def __init__(self, method): self.__method = method if method.__doc__: self.__doc = "\n" + method.__doc__ else: self.__doc = '' self.__repr = method.__name__ def contribute_to_class(self, cls, name): self.set_repr(cls, name) setattr(cls, name, self) def set_repr(self, cls, name): self.__repr = '<%s>.%s()' % (cls.__name__, name) def __get__(self, obj, cls): if obj is None: return self if not '.' in self.__repr: if cls is None: cls = type(obj) self.set_repr(cls, self.__repr) def closure(*args, **kwargs): self(obj, *args, **kwargs) return closure def __call__(self, *args, **kwargs): self.__method(*args, **kwargs) # this is to verify signature raise NotImplementedError(self.__repr + self.__doc) transactional = transactional(NeoServiceProperty()) class LazyModel(object): def __init__(self, cls, field, name, setup_reversed): add_lazy_relation(cls, field, name, self.__setup) self.__setup_reversed = setup_reversed def __setup(self, field, target, source): if not issubclass(target, NodeModel): raise TypeError("Relationships may only extend from Nodes.") self.__target = target self.__setup_reversed(target) __target = None @property def __model(self): model = self.__target if model is None: raise ValueError("Lazy model not initialized!") else: return model def __getattr__(self, attr): return getattr(self.__model, attr) def __call__(self, *args, **kwargs): return self.__model(*args, **kwargs) class DjangoNeo(object): def __init__(self): self.__field_counter = 0 @property def field_counter(self): res = self.__field_counter self.__field_counter += 1 return res @property def neo(self): try: return self.__neo except: return self.__setup_neo() def __setup_neo(self): from django.conf import settings from neo4j import NeoService import os try: resource_uri = settings.NEO4J_RESOURCE_URI assert resource_uri, "the resource_uri must be defined" except: raise ValueError("NEO4J_RESOURCE_URI is not defined in " "the settings module.") options = getattr(settings, 'NEO4J_OPTIONS', {}) self.__neo = NeoService(resource_uri, **options) return self.__neo def index(self, propname): # TODO: add the ability to choose index type return self.neo.index(propname, create=True) @transactional def type_node(self, app_label, model_name): for relationship in self.neo.reference_node.type_node: if (relationship['app_label'] == app_label and relationship['model_name'] == model_name): type_node = relationship.end break else: type_node = self.neo.node() self.neo.reference_node.type_node( type_node, app_label=app_label, model_name=model_name, ) return type_node @transactional def apply_to_buffer(self, constructor, items, size=1): result = [ constructor(item) for item in itertools.takewhile(countdown(size), items) ] if not result: raise StopIteration return result @property def log(self): if False: pass else: return self.__log class __log(object): def __getattr__(self, attr): def logger(message, *args, **kwargs): pass logger.__name__ = attr return logger __log = __log() DjangoNeo = DjangoNeo() # singleton def write_through(obj): return getattr(getattr(obj, '_meta', None), 'write_through', False) def all_your_base(cls, base): if issubclass(cls, base): yield cls for parent in cls.__bases__: for cls in all_your_base(parent, base): yield cls def countdown(number): counter = itertools.count() def done(*junk): for count in counter: return count < number return done def buffer_iterator(constructor, items, size=1): items = iter(items) # make sure we have an iterator while 1: for item in DjangoNeo.apply_to_buffer(constructor, items, size): yield item @pyneo.make def NodeModel(): class NodeModelManager(django.Manager): def get_query_set(self): return NodeQuerySet(self.model) @not_implemented def _insert(self, values, **kwargs): pass @not_implemented def _update(self, values, **kwargs): pass class IdProperty(object): def __init__(self, getter, setter): self.getter = getter self.setter = setter def __get__(self, inst, cls): if inst is None: return IdLookup(cls) else: return self.getter(inst) def __set__(self, inst, value): return self.setter(inst, value) class IdLookup(object): indexed = True unique = True def __init__(self, model): self.__model = model index = property(lambda self: self) def to_neo(self, value): return int(value) def nodes(self, nodeid): try: node = DjangoNeo.neo.node[nodeid] except: node = None else: type_node = DjangoNeo.type_node( self.__model._meta.app_label, self.__model.__name__) for rel in node.relationships('<<INSTANCE>>').incoming: # verify that the found node is an instance of the # requested type if rel.start == type_node: break # ok, it is! else: # no, it isn't! node = None if node is not None: yield node class NodeModel(django.Model): """Extend to make models""" objects = NodeModelManager() class Meta: abstract = True @classmethod def _neo4j_instance(cls, node): instance = cls.__new__(cls) instance.__node = node return instance #def __init__(self, *args, **kwargs): # self.__node = Neo4jDjangoModel.neo.node() # super(NodeModel, self).__init__(*args, **kwargs) def _get_pk_val(self, meta=None): return self.__node.id def _set_pk_val(self, value): if self.__node is None and value is None: return raise TypeError("Cannot change the id of nodes.") pk = id = IdProperty(_get_pk_val, _set_pk_val) def __eq__(self, other): try: return self.__node == other.__node except: return False __node = None @property def node(self): node = self.__node if node is None: # XXX: come up with a better exception type raise ValueError("Unsaved objects have no nodes.") else: return node _neo4j_underlying = node @transactional def save_base(self, raw=False, cls=None, origin=None, force_insert=False, force_update=False): assert not (force_insert and force_update) if cls: DjangoNeo.log.debug("save_base: cls=%s", cls) if origin: DjangoNeo.log.debug("save_base: origin=%s", origin) self._save_neo4j_node() self._save_neo4j_Properties(self, self.__node) self._save_neo4j_Relationships(self, self.__node) save_base.alters_data = True @transactional def _save_neo4j_node(self): if self.__node is None: self.__node = node = DjangoNeo.neo.node() for type_node in self.__all_type_nodes(): type_node.relationships('<<INSTANCE>>')(node) return self.__node _save_neo4j_node.alters_data = True @classmethod def _neo4j_type_node(cls): assert not cls == NodeModel, "only defined models have a type" try: node = cls.__type_node except: node = DjangoNeo.type_node(cls._meta.app_label, cls.__name__) cls.__type_node = node return node @classmethod def __all_type_nodes(cls): for cls in all_your_base(cls, NodeModel): if cls != NodeModel: yield cls._neo4j_type_node() def _collect_sub_objects(self, seen_objs, parent=None, nullable=False): raise TypeError( "NodeModel does not support _collect_sub_objects") @transactional @not_implemented def delete(self): pass delete.alters_data = True @transactional def _get_next_or_previous_by_FIELD(self, field, is_next, **kwargs): raise NotImplementedError("<NodeModel>.next/previous by %s" % field.attname) @transactional def _get_next_or_previous_in_order(self, is_next): raise NotImplementedError("<NodeModel>.next/previous") class NodeQuerySet(object): def __init__(self, model): self.model = model def __nodes(self): type_node = self.model._neo4j_type_node() for relationship in type_node.relationships('<<INSTANCE>>'): yield relationship.end def __iter__(self): return buffer_iterator(self.model._neo4j_instance, self.__nodes(), size=10) # count # dates # distinct # extra def create(self, **kwargs): obj = self.model(**kwargs) obj.save(force_insert=True) return obj @transactional def get(self, **lookup): resultset = self indexes = [] index = None for key, value in lookup.items(): if value is None: continue # None cannot be used in indexes attribute = getattr(self.model, key) if attribute.indexed: if attribute.unique: index = attribute.index break else: indexes.append((key, attribute.index)) else: if indexes: # XXX: select most appropriate index based on # number of entries for the key key, index = indexes.pop() if index: value = lookup.pop(key) value = attribute.to_neo(value) resultset = index.nodes(value) if lookup: # TODO: filter for the remaining ones raise NotImplementedError("filtering of query set") result = None for item in resultset: if result is not None: if index: lookup[key] = value raise self.model.MultipleObjectsReturned( "get() returned more than one %s. " "Lookup parameters were %s" % (self.model._meta.object_name, lookup)) else: result = item if result is None: if index: lookup[key] = value raise self.model.DoesNotExist( "%s matching query does not exist." "Lookup parameters were %s" % (self.model._meta.object_name, lookup)) return self.model._neo4j_instance(result) # get_or_create # filter # aggregate # annotate # complex_filter # exclude # in_bulk # iterator # latest # latest # order_by # select_related # values # values_list # update # reverse # defer # only return NodeModel @pyneo.make def Relationship(): class Meta(type): def __new__(meta, name, bases, body): new = super(Meta, meta).__new__ parents = [cls for cls in bases if isinstance(cls, Meta)] if not parents: # this is the base class return new(meta, name, bases, body) module = body.pop('__module__') modelbases = [ cls.Model for cls in parents if hasattr(cls, 'Model') ] Model = RelationshipModel.new(module, name, modelbases) for key, value in body.items(): if hasattr(value, 'contribute_to_class'): value.contribute_to_class(Model, key) else: setattr(Model, key, value) return new(meta, name, bases, { '__module__': module, 'Model': Model, }) def __getattr__(cls, key): if hasattr(cls, 'Model'): return getattr(cls.Model, key) else: raise AttributeError(key) def __setattr__(cls, key, value): if hasattr(cls, 'Model'): setattr(cls.Model, key, value) else: raise TypeError( "Cannot assign attributes to base Relationship") class RelationshipModel(object): __relationship = None def __init__(self): pass @property def relationship(self): rel = self.__relationship if rel is None: # XXX: better exception raise ValueError("Unsaved objects have no relationship.") else: return rel _neo4j_underlying = relationship @classmethod def new(RelationshipModel, module, name, bases): return type(name, bases + [RelationshipModel], { '__module__': module, }) @classmethod def add_field(self, prop): raise NotImplementedError("<RelationshipModel>.add_field()") class Relationship(object): """Extend to add properties to relationships.""" __metaclass__ = Meta def __init__(self, target, type=None, direction=None, optional=True, single=False, related_single=False, related_name=None): if related_name is None: if related_single: related_name = '%(name)s' else: related_name = '%(name)s_set' if not pyneo.python.is_string(type): if direction is not None: if type.direction is not direction: raise TypeError("Incompatiable direction!") else: direction = type.direction type = type.type self.__target = target self.__name = type self.__single = single self._direction = direction self.creation_counter = DjangoNeo.field_counter self.__related_single = related_single self.reversed_name = related_name target = property(lambda self: self.__target) __is_reversed = False def reverse(self, target, name): if self._direction is Incoming: direction = Outgoing elif self._direction is Outgoing: direction = Incoming else: direction = None relationship = Relationship(target, type=self.__name, direction=direction, single=self.__related_single, related_name=name) relationship.__is_reversed = True return relationship def contribute_to_class(self, source, name): if not issubclass(source, NodeModel): raise TypeError( "Relationships may only extend from Nodes.") if pyneo.python.is_string(self.__target): target = LazyModel( source, self, self.__target, lambda target: bound._setup_reversed(target)) else: target = self.__target if hasattr(self, 'Model'): if self.__single: Bound = SingleRelationship else: Bound = MultipleRelationships bound = Bound(self, source, self.__name or name, name, self.Model) else: if self.__single: Bound = SingleNode else: Bound = MultipleNodes bound = Bound(self, source, self.__name or name, name) source._meta.add_field(bound) setattr(source, name, bound) if not self.__is_reversed: bound._setup_reversed(target) class BoundRelationship(object): indexed = False rel = None primary_key = False choices = None db_index = None def __init__(self, rel, source, relname, attname): self.__rel = rel self.__source = source self._type = relname self.__attname = attname relationships = self.__relationships_for(source) relationships[self.name] = self # XXX weekref def _setup_reversed(self, target): self.__target = target if not isinstance(target, LazyModel): self.__rel.reverse(self.__source, self.__attname).contribute_to_class( target, self.__reversed_name) attname = name = property(lambda self: self.__attname) _direction = property(lambda self: self.__rel._direction) _target_model = property(lambda self: self.__rel.target) __reversed_name = property(lambda self: self.__rel.reversed_name) def get_default(self): return None @staticmethod def __state_for(instance, create=True): try: state = instance.__state except: state = {} if create: instance.__state = state return state @staticmethod def __relationships_for(obj_or_cls): meta = obj_or_cls._meta try: relationships = meta.__relationships except: meta.__relationships = relationships = {} return relationships def _save_(instance, node): state = BoundRelationship.__state_for(instance, create=False) if state: rels = BoundRelationship.__relationships_for(instance) for key, value in state.items(): rels[key]._save_relationship(instance, node, value) NodeModel._save_neo4j_Relationships = staticmethod(_save_) del _save_ @not_implemented def _save_relationship(self, instance, node, state): pass def _get_all_relationships(self, node): return self.__load_relationships(node) def __load_relationships(self, this): relationships = this.relationships(self._type) if self._direction is Incoming: relationships = relationships.incoming elif self._direction is Outgoing: relationships = relationships.outgoing return relationships creation_counter = property( lambda self: self.__rel.creation_counter) def __cmp__(self, other): return cmp(self.creation_counter, other.creation_counter) def __get__(self, obj, cls=None): if obj is None: return self return self._get_relationship(obj, self.__state_for(obj)) def __set__(self, obj, value): self._set_relationship(obj, self.__state_for(obj), value) def __delete__(self, obj): self._del_relationship(obj, self.__state_for(obj)) def _set_relationship(self, obj, state, value): if value is None: # assume initialization - ignore return # TODO: verify that obj is unsaved! raise TypeError("<%s>.%s is not assignable" % (obj.__class__.__name__, self.name)) def _del_relationship(self, obj, state): raise TypeError("Cannot delete <%s>.%s" % (obj.__class__.__name__, self.name)) class SingleNode(BoundRelationship): def _get_relationship(self, obj, state): if self.name in state: changed, result = state[self.name] else: try: this = obj.node except: result = None else: result = self.__load_related(this) state[self.name] = False, result return result @transactional def __load_related(self, this): relationship = self.__load_relationships(this).single if relationship is None: return None return self._neo4j_instance(this, relationship) def _neo4j_instance(self, this, relationship): that = relationship.getOtherNode(this) return self._target_model._neo4j_instance(that) def __load_relationships(self, this): relationships = this.relationships(self._type) if self._direction is Incoming: relationships = relationships.incoming elif self._direction is Outgoing: relationships = relationships.outgoing return relationships def _del_relationship(self, obj, state): self._set_relationship(obj, state, None) def _set_relationship(self, obj, state, other): state[self.name] = True, other def _save_relationship(self, instance, node, state): changed, other = state if not changed: return if other is None: del self.__load_relationships(node).single else: relationships = self.__load_relationships(node) relationships.single = other._save_neo4j_node() class BoundRelationshipModel(BoundRelationship): def __init__(self, rel, cls, relname, attname, Model): super(BoundRelationship, self).__init__(rel, cls, relname, attname) self.Model = Model raise NotImplementedError("Support for extended relationship " "models is not implemented yet.") class SingleRelationship(BoundRelationshipModel): # WAIT! @not_implemented def _get_relationship(self, obj, state): pass @not_implemented def _set_relationship(self, obj, state, other): pass class MultipleNodes(BoundRelationship): def _get_relationship(self, obj, states): state = states.get(self.name) if state is None: states[self.name] = state = RelationshipInstance(self, obj) return state #return RelationshipInstance(self, obj, state) #this = obj.node #for rel in this.relationships(self._type): # that = rel.getOtherNode(this) # yield self._NodeModel(that) def _neo4j_instance(self, this, relationship): that = relationship.getOtherNode(this) return self._target_model._neo4j_instance(that) def accept(self, obj): pass # TODO: implement verification def _save_relationship(self, instance, node, state): state.__save__(node) def _create_relationship(self, node, obj): other = obj._save_neo4j_node() # TODO: verify that it's ok in the reverse direction self.__load_relationships(node)(other) def __load_relationships(self, this): relationships = this.relationships(self._type) if self._direction is Incoming: relationships = relationships.incoming elif self._direction is Outgoing: relationships = relationships.outgoing return relationships @not_implemented def _extract_relationship(self, obj): pass class MultipleRelationships(BoundRelationshipModel): # WAIT! @not_implemented def _get_relationship(self, obj, state): pass @not_implemented def add(self, obj, other): pass class RelationshipInstance(django.Manager): def __init__(self, rel, obj): self.__rel = rel self.__obj = obj self.__added = [] # contains domain objects self.__removed = pyneo.python.Set() # contains relationships def __save__(self, node): for relationship in self.__removed: relationship.delete() for obj in self.__added: self.__rel._create_relationship(node, obj) self.__removed.clear() self.__added[:] = [] def _neo4j_relationships(self, node): for rel in self.__rel._get_all_relationships(node): if rel not in self.__removed: yield rel @property def _new(self): for item in self.__added: yield item def add(self, *objs): for obj in objs: self.__rel.accept(obj) self.__added.extend(objs) def remove(self, *objs): for obj in objs: self.__removed.add(self.__rel._extract_relationship(obj)) @not_implemented def clear(self): pass @not_implemented def create(self, *args, **kwargs): pass @not_implemented def get_or_create(self, *args, **kwargs): pass def get_query_set(self): return RelationshipQuerySet(self, self.__rel, self.__obj) class RelationshipQuerySet(object): def __init__(self, inst, rel, obj): self.__inst = inst self.__rel = rel self.__obj = obj def __relationships(self, node): for rel in self.__inst._neo4j_relationships(node): if self.__keep_relationship(rel): yield rel def __iter__(self): try: node = self.__obj.node except: pass else: buffered = buffer_iterator( lambda rel: self.__rel._neo4j_instance(node, rel), self.__relationships(node), size=10) for item in buffered: yield item for item in self.__inst._new: if self.__keep_instance(item): yield item def __keep_instance(self, obj): return True # TODO: filtering def __keep_relationship(self, rel): return True # TODO: filterning @not_implemented def get(self, **lookup): pass return Relationship @pyneo.make def Property(): class Property(object): """Extend to create properties of specific types.""" def __init__(self, indexed=False, unique=False, type=None, name=None): self.indexed = indexed self.unique = unique self.__name = name self.creation_counter = DjangoNeo.field_counter @property def default(self): return None # TODO: perhaps add better code here def to_neo(self, value): return value def from_neo(self, value): return value def contribute_to_class(self, cls, name): if issubclass(cls, NodeModel): prop = BoundProperty(self, cls, self.__name or name, name) cls._meta.add_field(prop) elif issubclass(cls, Relationship): if self.indexed: raise TypeError( "Relationship properties may not be indexed.") prop = BoundProperty(self, cls, self.__name or name) cls.add_field(prop) else: raise TypeError("Properties may only be added to Nodes" " or Relationships") setattr(cls, name, prop) class BoundProperty(object): rel = None primary_key = False choices = None # TODO: add support for this def __init__(self, property, cls, propname, attname): self.__property = property self.__class = cls self.__propname = propname self.__attname = attname properties = self.__properties_for(cls) properties[self.name] = self # XXX: weakref creation_counter = property( lambda self: self.__property.creation_counter) attname = name = property(lambda self: self.__attname) convert = property(lambda self: self.__property.convert) indexed = db_index = property(lambda self: self.__property.indexed) unique = property(lambda self: self.__property.unique) to_neo = property(lambda self: self.__property.to_neo) from_neo = property(lambda self: self.__property.from_neo) @property def index(self): if not self.indexed: raise TypeError("'%s' is not indexed" % (self.__propname, )) try: index = self.__index except: index_name = "%s %s %s" % ( self.__class._meta.app_label, self.__class.__name__, self.__propname, ) self.__index = index = DjangoNeo.index(index_name) return index def get_default(self): return self.__property.default def __cmp__(self, other): return cmp(self.creation_counter, other.creation_counter) @staticmethod def __values_of(instance, create=True): try: values = instance.__values except: values = {} if create: instance.__values = values return values @staticmethod def __properties_for(obj_or_cls): meta = obj_or_cls._meta try: properties = meta.__properties except: meta.__properties = properties = {} return properties def _save_(instance, node): values = BoundProperty.__values_of(instance) if values: properties = BoundProperty.__properties_for(instance) for key, value in values.items(): self = properties[key] old, value = self.__set_value(instance, value) if self.indexed: if self.unique and value is not None: old_node = self.index[value] if old_node and old_node != node: raise ValueError( "Duplicate index entries for <%s>.%s" % (instance.__class__.__name__, self.name)) if old is not None: self.index.remove(old, node) if value is not None: self.index.add(value, node) values.clear() NodeModel._save_neo4j_Properties = staticmethod(_save_) del _save_ def __get__(self, instance, cls=None): if instance is None: return self values = self.__values_of(instance, create=False) if self.__propname in values: return values[self.__propname] else: return self.__get_value(instance) def __set__(self, instance, value): if write_through(instance): self.___set_value(instance, value) else: values = self.__values_of(instance) values[self.__propname] = value @transactional def __get_value(self, instance): try: node = instance._neo4j_underlying except: # no node existed pass else: try: return self.__property.from_neo(node[self.__propname]) except: # no value set on node pass return self.get_default() # fall through: default value @transactional def __set_value(self, instance, value): value = self.__property.to_neo(value) underlying = instance._neo4j_underlying try: old = underlying[self.__propname] except: old = None underlying[self.__propname] = value return old, value return Property