Commits

Lynn Rees  committed 325735a

- refactor

  • Participants
  • Parent commits dfe1da5

Comments (0)

Files changed (43)

File graphalchemy/apps.py

 
     class include(Branch):
         backends = 'graphalchemy.backends.apps'
-        generics = 'graphalchemy.generics.apps'
-        models = 'graphalchemy.models.apps'
+        manager = 'graphalchemy.managers.apps'
+        object = 'graphalchemy.objects.apps'
 
-    class manager(Namespace):
-        link = 'graphalchemy.managers.Links'
-        node = 'graphalchemy.managers.Nodes'
-
-    class writer(Namespace):
-        link = 'graphalchemy.writers.Links'
-        node = 'graphalchemy.writers.Nodes'
-
-    class reader(Namespace):
-        link = 'graphalchemy.readers.Links'
-        node = 'graphalchemy.readers.Nodes'
-
-    class model(Namespace):
-
-        class finder(Namespace):
-            link = 'graphalchemy.finders.links'
-            node = 'graphalchemy.finders.nodes'
+    class session(Namespace):
+        objects = 'graphalchemy.sessions.Object'
+        manager = 'graphalchemy.sessions.Manager'

File graphalchemy/core.py

 octopus = Spine(graphalchemy, Required, Defaults)
 
 # decorators
-defer = octopus.context.defer
-direct = octopus.context.direct
-factory = octopus.context.factory
-class_defer = octopus.context.class_defer
-extend = octopus.context.extend
+app = octopus.decorator.app
+defer = octopus.decorator.defer
+extend = octopus.decorator.extend
+factory = octopus.decorator.factory

File graphalchemy/finders.py

-# -*- coding: utf-8 -*-
-'''graph model finder'''
-
-from appspace import NoAppError
-from appspace.six import string_types
-
-from graphalchemy.core import octopus, direct
-
-__all__ = ('links', 'nodes')
-
-# settings
-conf = octopus.S
-
-
-class Finder(octopus.context.Thing):
-
-    '''model finder'''
-
-    def __call__(self, model=None, element=None):
-        '''
-        get default model
-
-        @param model: model class or model identifier (default: None)
-        @param element: graph database element
-        '''
-        if element is not None:
-            try:
-                return self.Q.app(element['_model'], self.S.userspace)
-            except (NoAppError, KeyError):
-                pass
-        elif isinstance(model, string_types):
-            return self.Q.app(model, self.S.userspace)
-        return model if model else self.generic
-
-
-class Links(Finder):
-
-    '''link model finder'''
-
-    # generic link model
-    generic = direct(conf.generic.element.link, conf.generics)
-
-
-links = Links()
-
-
-class Nodes(Finder):
-
-    '''node model finder'''
-
-    # generic node model
-    generic = direct(conf.generic.element.node, conf.generics)
-
-
-nodes = Nodes()

File graphalchemy/generics/__init__.py

-# -*- coding: utf-8 -*-
-'''graphalchemy generics'''

File graphalchemy/generics/apps.py

-# -*- coding: utf-8 -*-
-'''graph generics appconf'''
-
-from spine import Pathways
-from appspace import Namespace
-
-__all__ = ['appconf']
-
-
-class Appconf(Pathways):
-
-    class generic(Namespace):
-
-        class element(Namespace):
-            link = 'graphalchemy.generics.elements.Link'
-            node = 'graphalchemy.generics.elements.Node'
-
-        class collection(Namespace):
-            link = 'graphalchemy.generics.collections.Links'
-            node = 'graphalchemy.generics.collections.Nodes'
-
-        class collector(Namespace):
-            link = 'graphalchemy.generics.collectors.Links'
-            node = 'graphalchemy.generics.collectors.Nodes'
-
-
-appconf = Appconf.build()

File graphalchemy/generics/collections.py

-# -*- coding: utf-8 -*-
-'''generic collections'''
-
-from graphalchemy.mixins.collections import LinksMixin, NodesMixin
-
-__all__ = ('Links', 'Nodes')
-
-
-class Links(LinksMixin):
-
-    '''links collection'''
-
-
-class Nodes(NodesMixin):
-
-    '''nodes collection'''

File graphalchemy/generics/collectors.py

-# -*- coding: utf-8 -*-
-'''generic collectors'''
-
-from graphalchemy.core import octopus, direct
-from graphalchemy.mixins.collectors import LinksMixin, NodesMixin
-
-__all__ = ('Links', 'Nodes')
-
-# settings
-conf = octopus.S
-
-
-class Links(LinksMixin):
-
-    '''generic link collections collector'''
-
-    _collection = direct(conf.generic.collection.link, conf.generics)
-
-
-class Nodes(NodesMixin):
-
-    '''generic nodes collections collector'''
-
-    _collection = direct(conf.generic.collection.node, conf.generics)

File graphalchemy/generics/elements.py

-# -*- coding: utf-8 -*-
-# pylint: disable-msg=w0201
-'''generic element models'''
-
-from graphalchemy.core import octopus, defer, direct
-from graphalchemy.mixins.elements import LinkMixin, NodeMixin, ElementMixin
-
-__all__ = ('Link', 'Node')
-
-# settings
-conf = octopus.S
-
-
-class Generic(ElementMixin, octopus.process.Thing):
-
-    '''generic graph model'''
-
-    # generic links collection
-    _links = direct(conf.generic.collector.link, conf.generics)
-
-    @staticmethod
-    def _link():
-        '''link class'''
-        return Link
-
-
-class Link(LinkMixin, Generic):
-
-    '''generic link model'''
-
-    @staticmethod
-    def _node():
-        '''node class'''
-        return Node
-
-    @defer
-    def create(self, kw):
-        '''make link'''
-        self.source = self.l.make(self.link, self.start, self.end, kw=kw)
-        self._refresh()
-
-    @defer
-    def modify(self, **kw):
-        '''update link'''
-        self.l.modify(self, kw)
-        self._refresh()
-
-
-class Node(NodeMixin, Generic):
-
-    '''generic node model'''
-
-    # generic node collection
-    _nodes = direct(conf.generic.collector.node, conf.generics)
-
-    @defer
-    def create(self, kw):
-        '''make node'''
-        self.source = self.n.make(kw)
-        self._refresh()
-
-    @defer
-    def modify(self, kw):
-        '''update node'''
-        self.n.modify(self, kw)
-        self._refresh()

File graphalchemy/graphs.py

 
 from stuf.utils import clsname
 
-from graphalchemy.core import octopus
-from graphalchemy.session import Session
+from graphalchemy.core import octopus, extend
 
 __all__ = ['Graph']
 
 # settings
-conf = octopus.S
+conf = octopus.G
 
 
 class Graph(octopus.workflow.Manager):
 
     '''graph interface'''
 
-    _session_class = Session
+    manager = extend(conf.session.manager, conf.appspace)
+    objects = extend(conf.session.object, conf.appspace)
 
     def __init__(self, url, **kw):
         '''
         # add data source to appspace
         self.Q.add(self._db, self.S.key.backend, userspace)
         # pipe for abort
-        self.M.pipe('abort', self.close)
+        self.pipe('abort', self.close)
 
     def __repr__(self):
         return '{name}@{url}'.format(

File graphalchemy/managers.py

-# -*- coding: utf-8 -*-
-'''graph element managers'''
-
-from graphalchemy.core import octopus, direct
-
-__all__ = ('Links', 'Nodes')
-
-# settings
-conf = octopus.S
-appspace = conf.appspace
-reader = conf.reader
-wrapper = conf.wrapper
-writer = conf.writer
-
-
-class Manager(octopus.workflow.Thing):
-
-    '''graph element manager base'''
-
-    # graph source
-    _db = direct(conf.key.backend, conf.userspace)
-
-    def query(self, model):
-        '''
-        wrap graph element model together with database
-
-        @param model: graph element model
-        '''
-        return self._reader(model)
-
-
-class Links(Manager):
-
-    '''graph link manager'''
-
-    # link reader
-    _reader = direct(reader.link, appspace)
-    # link writer
-    _writer = direct(writer.link, appspace)
-
-
-class Nodes(Manager):
-
-    '''graph node manager'''
-
-    # node reader
-    _reader = direct(reader.node, appspace)
-    # node writers
-    _writer = direct(writer.node, appspace)

File graphalchemy/managers/__init__.py

+# -*- coding: utf-8 -*-
+'''graphalchemy generics'''

File graphalchemy/managers/apps.py

+# -*- coding: utf-8 -*-
+'''graph managers appconf'''
+
+from spine import Pathways
+from appspace import Namespace
+
+__all__ = ['appconf']
+
+
+class Appconf(Pathways):
+
+    class generic(Namespace):
+
+        class element(Namespace):
+            link = 'graphalchemy.managers.elements.Link'
+            node = 'graphalchemy.managers.elements.Node'
+
+        class collection(Namespace):
+            link = 'graphalchemy.managers.collections.Links'
+            node = 'graphalchemy.managers.collections.Nodes'
+
+        class collector(Namespace):
+            link = 'graphalchemy.managers.collectors.Links'
+            node = 'graphalchemy.managers.collectors.Nodes'
+
+        class writer(Namespace):
+            link = 'graphalchemy.managers.writers.Links'
+            node = 'graphalchemy.managers.writers.Nodes'
+
+        class reader(Namespace):
+            link = 'graphalchemy.managers.readers.Links'
+            node = 'graphalchemy.managers.readers.Nodes'
+
+
+appconf = Appconf.build()

File graphalchemy/managers/collections.py

+# -*- coding: utf-8 -*-
+'''generic collections'''
+
+from graphalchemy.mixins.collections import LinksMixin, NodesMixin
+
+__all__ = ('Links', 'Nodes')
+
+
+class Links(LinksMixin):
+
+    '''links collection'''
+
+
+class Nodes(NodesMixin):
+
+    '''nodes collection'''

File graphalchemy/managers/collectors.py

+# -*- coding: utf-8 -*-
+'''generic collectors'''
+
+from graphalchemy.core import octopus, app
+from graphalchemy.mixins.collectors import LinksMixin, NodesMixin
+
+__all__ = ('Links', 'Nodes')
+
+# settings
+conf = octopus.G
+
+
+class Links(LinksMixin):
+
+    '''generic link collections collector'''
+
+    _collection = app(conf.generic.collection.link, conf.generics)
+
+
+class Nodes(NodesMixin):
+
+    '''generic nodes collections collector'''
+
+    _collection = app(conf.generic.collection.node, conf.generics)

File graphalchemy/managers/elements.py

+# -*- coding: utf-8 -*-
+# pylint: disable-msg=w0201
+'''generic element models'''
+
+from graphalchemy.core import octopus, defer, app
+from graphalchemy.mixins.elements import LinkMixin, NodeMixin, ElementMixin
+
+__all__ = ('Link', 'Node')
+
+# settings
+conf = octopus.G
+
+
+class Generic(ElementMixin, octopus.process.Thing):
+
+    '''generic graph model'''
+
+    # generic links collection
+    _links = app(conf.generic.collector.link, conf.generics)
+
+    @staticmethod
+    def _link():
+        '''link class'''
+        return Link
+
+
+class Link(LinkMixin, Generic):
+
+    '''generic link model'''
+
+    @staticmethod
+    def _node():
+        '''node class'''
+        return Node
+
+    @defer
+    def create(self, kw):
+        '''make link'''
+        self.source = self.l.make(self.link, self.start, self.end, kw=kw)
+        self._refresh()
+
+    @defer
+    def modify(self, **kw):
+        '''update link'''
+        self.l.modify(self, kw)
+        self._refresh()
+
+
+class Node(NodeMixin, Generic):
+
+    '''generic node model'''
+
+    # generic node collection
+    _nodes = app(conf.generic.collector.node, conf.generics)
+
+    @defer
+    def create(self, kw):
+        '''make node'''
+        self.source = self.n.make(kw)
+        self._refresh()
+
+    @defer
+    def modify(self, kw):
+        '''update node'''
+        self.n.modify(self, kw)
+        self._refresh()

File graphalchemy/managers/readers.py

+# -*- coding: utf-8 -*-
+'''graph readers'''
+
+from appspace import AppLookupError
+
+from graphalchemy.core import octopus, app
+from graphalchemy.mixins.readers import ReaderMixin, LinksMixin, NodesMixin
+
+__all__ = ('Links', 'Nodes')
+
+# settings
+conf = octopus.G
+
+
+class Reader(ReaderMixin):
+
+    '''graph reader base'''
+
+    def filter(self, index, queries):
+        '''
+        full text search using index
+
+        @param index: name of index
+        @param queries: queries built with query builder
+        @param model: element model (default: None)
+        '''
+        return self.r.filter(index, queries, self._model)
+
+    def filter_by(self, index, key, value):
+        '''
+        filter by keywords
+
+        @param index: index name
+        @param key: keyword in index
+        @param value: value in index (or second key)
+        '''
+        return self.r.filter_by(index, key, value, self._model)
+
+    def get(self, element):
+        '''
+        get database element by id
+
+        @param element: element id
+        '''
+        return self._model(self.r.get(element))
+
+    def traverse(self, this, tester=None, links=None, unique=None):
+        '''
+        traverse graph, yielding graph elements
+
+        @param this: graph model instance
+        @param tester: filter for traversed elements (default: None)
+        @param links: links to traverse (default: None)
+        @param unique: how often to traverse the same element (default: None)
+        '''
+        model = self._model
+        for element in self.r.traverser(this, tester, links, unique):
+            yield model(element)
+
+
+class Links(Reader, LinksMixin):
+
+    '''graph link reader'''
+
+    _model = app(conf.generic.element.link, conf.generics)
+
+    def walk(self, this, direction=None, label=None, keys=None):
+        '''
+        iterate links and yield graph elements
+
+        @param this: graph model instance
+        @param direction: direction of links (default: None)
+        @param label: label for links (default: None)
+        @param keys: keys to find on elements (default: None)
+        '''
+        model = self._model
+        for element in self.r.walker(this, direction, label, keys):
+            yield model(element)
+
+
+class Nodes(Reader, NodesMixin):
+
+    '''graph manager node reader'''
+
+    _model = app(conf.generic.element.node, conf.generics)
+
+    def root(self):
+        '''
+        reference root for graph
+
+        @param model: element model (default: None)
+        '''
+        try:
+            S = self.S
+            return self.Q.app(S.root, S.userspace)
+        except AppLookupError:
+            root = self.r.root
+            root = self._model(root)
+            self.Q.register(root, S.root, S.userspace)
+            return root
+
+    def walk(self, this, direction=None, label=None, keys=None, end='end'):
+        '''
+        iterate links and yield node from link
+
+        @param this: graph node instance
+        @param direction: direction of links (default: None)
+        @param label: label for links (default: None)
+        @param keys: keys to find on elements (default: None)
+        @param end: which end of link to return (default: 'end')
+        '''
+        model = self._model
+        for element in self.r.walker(this, direction, label, keys, end):
+            yield model(element)

File graphalchemy/managers/writers.py

+# -*- coding: utf-8 -*-
+'''graph generic writers'''
+
+from graphalchemy.mixins.writers import LinksMixin, NodesMixin
+
+__all__ = ('Links', 'Nodes')
+
+
+class Links(LinksMixin):
+
+    '''generic graph link writer'''
+
+    def build(self, link, start, end, kw=None):
+        '''
+        build a new graph link
+
+        @param data: data
+        @param model: graph model (default: None)
+        '''
+        if kw is None:
+            kw = {}
+        kw.update(_created=self.now(), _uuid=self.Q.uuid())
+        return self.w.build(link, start, end, **kw)
+
+
+class Nodes(NodesMixin):
+
+    '''generic graph node writer'''
+
+    def build(self, data):
+        '''
+        build a new graph node
+
+        @param data: data
+        @param model: graph model (default: None)
+        '''
+        data.update(_created=self.now(), _uuid=self.Q.uuid())
+        return self.w.build(data)

File graphalchemy/mixins/collectors.py

 __all__ = ('LinksMixin', 'NodesMixin')
 
 # settings
-conf = octopus.S
+conf = octopus.G
 appspace = conf.appspace
 finder = conf.model.finder
 

File graphalchemy/mixins/elements.py

 
 from stuf.utils import both, getcls, lazy, clsname, either
 
-from graphalchemy.core import octopus, defer, direct
+from graphalchemy.core import octopus, defer, app
 
 __all__ = ('LinksMixin', 'NodesMixin')
 
     '''graph element'''
 
     # graph connector
-    _g = direct(conf.manager.graph, conf.appspace)
+    _g = app(conf.manager.graph, conf.appspace)
 
     def __init__(self, element=None, **kw):
         '''

File graphalchemy/mixins/readers.py

+# -*- coding: utf-8 -*-
+'''graph reader mixins'''
+
+from graphalchemy.core import octopus, app, factory
+
+__all__ = ('Reader Mixin', 'LinksMixin', 'Nodes')
+
+# settings
+conf = octopus.G
+
+
+class ReaderMixin(object):
+
+    '''graph reader base'''
+
+    # graph source
+    _db = app(conf.key.backend, conf.userspace)
+
+    def id(self, element):
+        '''
+        graph element identifier
+
+        @param element: a graph element
+        '''
+        return self.r.id(element)
+
+    def properties(self, element):
+        '''
+        fetch a graph model's properties
+
+        @param element: element with properties
+        '''
+        return self.r.properties(element)
+
+    def close(self):
+        '''close database'''
+        self.r.close()
+
+
+class LinksMixin(object):
+
+    '''graph link reader mixin'''
+
+    # graph reader
+    r = factory(conf.read.link, conf.backends, conf.key.db)
+
+    def kind(self, element):
+        '''
+        kind of link
+
+        @param element: graph element
+        '''
+        return self.r.kind(element)
+
+
+class NodesMixin(object):
+
+    '''graph node reader mixin'''
+
+    # graph reader
+    r = factory(conf.read.node, conf.backends, conf.key.db)

File graphalchemy/mixins/writers.py

+# -*- coding: utf-8 -*-
+'''graph writers'''
+
+import nanotime
+
+from graphalchemy.core import octopus, defer, app, factory
+
+__all__ = ('Links', 'Nodes')
+
+# settings
+conf = octopus.G
+backends = conf.backends
+db = conf.key.db
+
+
+class Writer(octopus.event.Worker):
+
+    '''graph writer base'''
+
+    # graph source
+    _db = app(conf.key.backend, conf.userspace)
+
+    @property
+    def transaction(self):
+        '''get transaction'''
+        return self.w.transaction
+
+    @defer
+    def remove(self, this, indices=None):
+        '''
+        delete graph element
+
+        @param this: graph model instance
+        @param indicies: graph indices (default: None)
+        '''
+        self.w.delete_element(this, indices)
+
+    def index(self, index, fts=False):
+        '''
+        create link index
+
+        @param index: node index name
+        @param fts: create a full text index (default: False)
+        '''
+        self.w.create_index(index, fts)
+
+    def index_many(self, index, this, indexed):
+        '''
+        index properties on a graph element
+
+        @param index: graph index label
+        @param this: graph model instance
+        @param indexed: properties to index
+        '''
+        self.w.index_many(self.r.index(index), this, indexed)
+
+    def index_one(self, index, key, value, this):
+        '''
+        index one property of a graph element
+
+        @param index: graph index label
+        @param key: graph element property key
+        @param value: graph element property value
+        @param this: graph model instance
+        '''
+        self.w.index_one(self.r.index(index), key, value, this)
+
+    def modify(self, this, data):
+        '''
+        update a graph element's properties
+
+        @param this: graph model instance
+        @param data: data
+        '''
+        self.w.update(this, data)
+
+    @staticmethod
+    def now():
+        '''current time in microseconds'''
+        return nanotime.now().microseconds()
+
+
+class LinksMixin(Writer):
+
+    '''graph link writer mixin'''
+
+    # link model finder
+    _finder = app(conf.model.finder.link, conf.appspace)
+    # direct link reader
+    r = factory(conf.read.link, backends, db)
+    # direct link writer
+    w = factory(conf.write.link, backends, db)
+
+
+class NodesMixin(Writer):
+
+    '''graph node writer mixin'''
+
+    # node model finder
+    _finder = app(conf.model.finder.node, conf.appspace)
+    # direct node reader
+    r = factory(conf.read.node, backends, db)
+    # direct node writer
+    w = factory(conf.write.node, backends, db)
+
+    def create(self, model=None, **kw):
+        '''
+        create a new graph element
+
+        @param model: graph model (default: None)
+        '''
+        if model is None:
+            model = self._finder(model)
+        instance = model(**kw)
+        self.local.add(instance)
+        return instance

File graphalchemy/models/__init__.py

-# -*- coding: utf-8 -*-
-'''graphalchemy models'''

File graphalchemy/models/apps.py

-# -*- coding: utf-8 -*-
-'''graph backends appconf'''
-
-from spine import Pathways
-from appspace import Namespace
-
-__all__ = ['appconf']
-
-
-class Appconf(Pathways):
-
-    class model(Namespace):
-
-        class element(Namespace):
-            node = 'graphalchemy.models.elements.Node'
-
-        class collection(Namespace):
-            node = 'graphalchemy.models.collections.Nodes'
-
-        class collector(Namespace):
-            node = 'graphalchemy.models.collectors.Nodes'
-
-
-appconf = Appconf.build()

File graphalchemy/models/collections.py

-# -*- coding: utf-8 -*-
-'''model collections'''
-
-from stuf.utils import lazy
-
-from graphalchemy.errors import UnrelatedLinkError
-from graphalchemy.mixins.collections import NodesMixin
-
-__all__ = ['Nodes']
-
-
-class Nodes(NodesMixin):
-
-    '''collection of nodes from a specific instance'''
-
-    def __init__(self, this=None, model=None, **kw):
-        '''
-        init
-
-        @param this: graph element (default: None)
-        @param model: graph model (default: None)
-        '''
-        self.link = kw.pop('link', None)
-        self.name = kw.pop('name', None)
-        super(Nodes, self).__init__(this, model, **kw)
-
-    @lazy
-    def _inner(self):
-        '''inner list inside collection'''
-        return self._this.n.walked(
-            self._this, self.direction, self.link, end=self.link_end,
-        )
-
-    def add(self, node, **kw):
-        '''
-        link a node attached to a specific node by a link
-
-        @param node: node to link to other node
-        '''
-        if isinstance(node, self._model):
-            self._this.link(self.link, node, **kw)
-        else:
-            raise UnrelatedLinkError(
-                '{inst} instance is unrelated to {model}'.format(
-                    inst=node.C.name, model=self._model.C.name,
-                )
-            )
-
-    def create(self, **kw):
-        '''create a node attached to a specific node by a link'''
-        links = kw.pop('link_props', {})
-        new_node = super(Nodes, self).create(**kw)
-        self.add(new_node, **links)
-        return new_node

File graphalchemy/models/collectors.py

-# -*- coding: utf-8 -*-
-'''model collectors'''
-
-from stuf.utils import lazy
-
-from graphalchemy.core import octopus, direct
-from graphalchemy.mixins.collectors import NodesMixin
-
-__all__ = ('In', 'Nodes', 'Out')
-
-# settings
-conf = octopus.S
-
-
-class Nodes(NodesMixin):
-
-    '''collection of model nodes'''
-
-    # node collection
-    _collection = direct(conf.model.collection.node, conf.models)
-
-    @lazy
-    def reference(self):
-        return self._this.n.reference()
-
-    @lazy
-    def forks(self):
-        '''list of nodes forked from this node'''
-        fork_link = self._this.C.get('fork_link')
-        if fork_link is not None:
-            return self._collection(
-                self._this, self._model, link_end='start', link=fork_link,
-            )
-
-    @lazy
-    def versions(self):
-        '''list of nodes versioned from this node'''
-        version_link = self._this.C.get('version_link')
-        if version_link is not None:
-            return self._collection(
-                self._this, self._model, link_end='start', link=version_link,
-            )
-
-
-class Direction(octopus.context.Thing):
-
-    '''direction definition'''
-
-    # model finder
-    _finder = direct(conf.model.finder.node, conf.appspace)
-    # node collection
-    _nodes = direct(conf.model.collection.node, conf.models)
-
-    def __init__(self, model=None, link=''):
-        '''
-        @param model: model with direction (default: None)
-        @param link: link name
-        '''
-        super(Direction, self).__init__()
-        self.link = link
-        self.model = model
-
-    def __get__(self, this, that):
-        if this is None:
-            return self._finder(self.model)
-        return self._nodes(this, **self.config)
-
-    @lazy
-    def config(self):
-        '''configuration for collection'''
-        self.model = self._finder(self.model)
-        return dict(
-            direction=self.direction,
-            link=self.link,
-            link_end=self.link_end,
-            model=self.model,
-            name=self.model.C.name,
-        )
-
-
-class In(Direction):
-
-    '''incoming direction definition'''
-
-    direction = 'outgoing'
-    link_end = 'start'
-
-
-class Out(Direction):
-
-    '''outgoing direction definition'''
-
-    direction = 'incoming'
-    link_end = 'end'

File graphalchemy/models/elements.py

-# -*- coding: utf-8 -*-
-#pylint: disable-msg=e0203,w0201
-'''graph element models'''
-
-from stuf.utils import lazy_class, selfname
-
-from graphalchemy.errors import ReversionError
-from graphalchemy.core import octopus, direct, defer
-from graphalchemy.mixins.elements import NodeMixin, ElementMixin
-
-__all__ = ['Node']
-
-# settings
-conf = octopus.S
-
-
-class Node(NodeMixin, ElementMixin, octopus.workflow.Thing):
-
-    '''node model'''
-
-    # links collection
-    _links = direct(conf.generic.collector.link, conf.generics)
-    # node collection
-    _nodes = direct(conf.model.collector.node, conf.models)
-
-    @lazy_class
-    def C(self):
-        '''local settings'''
-        existing = self.traits.localize()
-        metas = dict()
-        # model name
-        metas['name'] = selfname(self)
-        class_names = self.traits.names
-        # slug field
-        try:
-            metas['slug_from'] = class_names(slug_from=True)[0]
-        except IndexError:
-            metas['slug_from'] = ''
-        # properties to index
-        metas['indexed'] = set(k for k in class_names(indexed=True))
-        # properties indexed for full text search
-        metas['fts_indexed'] = set(k for k in class_names(full_text=True))
-        # properties to HTML escape
-        metas['escaped'] = set(k for k in class_names(escaped=True))
-        existing.update(metas)
-        return existing
-
-    @classmethod
-    def _link(cls):
-        return cls.Q.app(cls.S.generic.element.link, cls.S.generics)
-
-    @defer
-    def _versioning(self, diff):
-        '''snapshot node'''
-        diff.update(_hash=self.traits.hash_diff(), _version=self._version())
-        # create new snapshot
-        version = self.n.snapshot(self, diff)
-        self._refresh()
-        return version
-
-    def _version(self):
-        '''increment current snapshot number of node'''
-        self.update({'_versions': self._current['_versions'] + 1})
-        return self._current['_versions']
-
-    def commit(self):
-        '''save this node'''
-        super(Node, self).commit()
-        # snapshot if flag set
-        if self.C.versioned:
-            self.snapshot()
-
-    @defer
-    def create(self, kw):
-        '''build node'''
-        self.source = self.n.build(kw).source
-        self._refresh()
-
-    @defer
-    def modify(self, kw):
-        '''update node'''
-        self.n.update(self, kw)
-        self._refresh()
-
-    @defer
-    def revert(self, version):
-        '''
-        change to another snapshot of this node
-
-        @param snapshot: id of previous snapshot of this node
-        '''
-        if self.C.versioned:
-            self.snapshot()
-            self.n.revert(self, version)
-            self.snapshot()
-        raise ReversionError('could not revert snapshot {0}'.format(version))
-
-    def snapshot(self):
-        '''track changes for this node'''
-        diff = self.traits.diff()
-        # if there is a change to nodes properties...
-        return self._versioning(diff) if diff else {}
-
-    @defer
-    def anchor(self, anchor, **kw):
-        '''link node to anchor'''
-        self.l.make(self.C.anchor_link, self, anchor, kw)
-
-    class Meta:
-        # anchor link label
-        anchor_link = ''
-        # fork link label
-        fork_link = ''
-        # full text index name
-        fts_index = ''
-        # index name
-        index = ''
-        # reference link label
-        reference_link = ''
-        # root index name
-        root_index = 'roots'
-        # slug field name
-        slug_field = ''
-        # snapshot link label
-        version_link = ''
-        # whether to track changes
-        versioned = False

File graphalchemy/models/properties.py

-# -*- coding: utf-8 -*-
-'''graph model properties'''
-
-import sys
-
-from spine.traits import Trait
-from spine.bases.keys import NoDefault
-from spine.traits import CheckedUnicode, Bool, Float as Ft, Integer, Unicode
-
-__all__ = (
-    'BooleanField', 'CharField', 'FloatField', 'IntegerField', 'StringField',
-    'TextField',
-)
-
-
-class PropertyMixin(Trait):
-
-    metadata = dict(property=True)
-
-    def __init__(self, initial=NoDefault, **kw):
-        kw.update(dict(property=True, indexed=kw.get('indexed', False)))
-        super(PropertyMixin, self).__init__(initial, **kw)
-
-
-class StringMixin(PropertyMixin):
-
-    def __init__(self, initial=NoDefault, **kw):
-        kw.update(dict(
-            escaped=kw.get('escaped', False),
-            slug_from=kw.get('slug_from', False)
-        ))
-        super(StringMixin, self).__init__(initial, **kw)
-
-
-class BooleanField(PropertyMixin, Bool):
-
-    '''boolean field'''
-
-
-class CharField(StringMixin, CheckedUnicode):
-
-    '''checked string field'''
-
-    def __init__(self, value='', minlen=0, maxlen=sys.maxsize, regex='', **kw):
-        super(CharField, self).__init__(value, minlen, maxlen, regex, **kw)
-
-
-class FloatField(PropertyMixin, Ft):
-
-    '''float field'''
-
-
-class IntegerField(PropertyMixin, Integer):
-
-    '''integer field'''
-
-
-class StringField(StringMixin, Unicode):
-
-    '''string field'''
-
-
-class TextField(StringField):
-
-    '''string that can be full text searched'''
-
-    def __init__(self, initial=NoDefault, **kw):
-        super(TextField, self).__init__(initial, **kw)
-        kw.update(dict(full_text=kw.get('full_text', False)))

File graphalchemy/objects/__init__.py

+# -*- coding: utf-8 -*-
+'''graphalchemy models'''

File graphalchemy/objects/apps.py

+# -*- coding: utf-8 -*-
+'''graph backends appconf'''
+
+from spine import Pathways
+from appspace import Namespace
+
+__all__ = ['appconf']
+
+
+class Appconf(Pathways):
+
+    class model(Namespace):
+
+        class element(Namespace):
+            node = 'graphalchemy.objects.elements.Node'
+
+        class collection(Namespace):
+            node = 'graphalchemy.objects.collections.Nodes'
+
+        class collector(Namespace):
+            node = 'graphalchemy.objects.collectors.Nodes'
+
+        class finder(Namespace):
+            link = 'graphalchemy.objects.finders.links'
+            node = 'graphalchemy.objects.finders.nodes'
+
+        class writer(Namespace):
+            node = 'graphalchemy.objects.writers.Nodes'
+
+        class reader(Namespace):
+            node = 'graphalchemy.objects.readers.Nodes'
+
+
+appconf = Appconf.build()

File graphalchemy/objects/collections.py

+# -*- coding: utf-8 -*-
+'''model collections'''
+
+from stuf.utils import lazy
+
+from graphalchemy.errors import UnrelatedLinkError
+from graphalchemy.mixins.collections import NodesMixin
+
+__all__ = ['Nodes']
+
+
+class Nodes(NodesMixin):
+
+    '''collection of nodes from a specific instance'''
+
+    def __init__(self, this=None, model=None, **kw):
+        '''
+        init
+
+        @param this: graph element (default: None)
+        @param model: graph model (default: None)
+        '''
+        self.link = kw.pop('link', None)
+        self.name = kw.pop('name', None)
+        super(Nodes, self).__init__(this, model, **kw)
+
+    @lazy
+    def _inner(self):
+        '''inner list inside collection'''
+        return self._this.n.walked(
+            self._this, self.direction, self.link, end=self.link_end,
+        )
+
+    def add(self, node, **kw):
+        '''
+        link a node attached to a specific node by a link
+
+        @param node: node to link to other node
+        '''
+        if isinstance(node, self._model):
+            self._this.link(self.link, node, **kw)
+        else:
+            raise UnrelatedLinkError(
+                '{inst} instance is unrelated to {model}'.format(
+                    inst=node.C.name, model=self._model.C.name,
+                )
+            )
+
+    def create(self, **kw):
+        '''create a node attached to a specific node by a link'''
+        links = kw.pop('link_props', {})
+        new_node = super(Nodes, self).create(**kw)
+        self.add(new_node, **links)
+        return new_node

File graphalchemy/objects/collectors.py

+# -*- coding: utf-8 -*-
+'''model collectors'''
+
+from stuf.utils import lazy
+
+from graphalchemy.core import octopus, app
+from graphalchemy.mixins.collectors import NodesMixin
+
+__all__ = ('In', 'Nodes', 'Out')
+
+# settings
+conf = octopus.G
+
+
+class Nodes(NodesMixin):
+
+    '''collection of model nodes'''
+
+    # node collection
+    _collection = app(conf.model.collection.node, conf.models)
+
+    @lazy
+    def reference(self):
+        return self._this.n.reference()
+
+    @lazy
+    def forks(self):
+        '''list of nodes forked from this node'''
+        fork_link = self._this.C.get('fork_link')
+        if fork_link is not None:
+            return self._collection(
+                self._this, self._model, link_end='start', link=fork_link,
+            )
+
+    @lazy
+    def versions(self):
+        '''list of nodes versioned from this node'''
+        version_link = self._this.C.get('version_link')
+        if version_link is not None:
+            return self._collection(
+                self._this, self._model, link_end='start', link=version_link,
+            )
+
+
+class Direction(octopus.context.Thing):
+
+    '''direction definition'''
+
+    # model finder
+    _finder = app(conf.model.finder.node, conf.appspace)
+    # node collection
+    _nodes = app(conf.model.collection.node, conf.models)
+
+    def __init__(self, model=None, link=''):
+        '''
+        @param model: model with direction (default: None)
+        @param link: link name
+        '''
+        super(Direction, self).__init__()
+        self.link = link
+        self.model = model
+
+    def __get__(self, this, that):
+        if this is None:
+            return self._finder(self.model)
+        return self._nodes(this, **self.config)
+
+    @lazy
+    def config(self):
+        '''configuration for collection'''
+        self.model = self._finder(self.model)
+        return dict(
+            direction=self.direction,
+            link=self.link,
+            link_end=self.link_end,
+            model=self.model,
+            name=self.model.C.name,
+        )
+
+
+class In(Direction):
+
+    '''incoming direction definition'''
+
+    direction = 'outgoing'
+    link_end = 'start'
+
+
+class Out(Direction):
+
+    '''outgoing direction definition'''
+
+    direction = 'incoming'
+    link_end = 'end'

File graphalchemy/objects/elements.py

+# -*- coding: utf-8 -*-
+#pylint: disable-msg=e0203,w0201
+'''graph element models'''
+
+from stuf.utils import lazy_class, selfname
+
+from graphalchemy.errors import ReversionError
+from graphalchemy.core import octopus, app, defer
+from graphalchemy.mixins.elements import NodeMixin, ElementMixin
+
+__all__ = ['Node']
+
+# settings
+conf = octopus.G
+
+
+class Node(NodeMixin, ElementMixin, octopus.workflow.Thing):
+
+    '''node model'''
+
+    # links collection
+    _links = app(conf.generic.collector.link, conf.generics)
+    # node collection
+    _nodes = app(conf.model.collector.node, conf.models)
+
+    @lazy_class
+    def C(self):
+        '''local settings'''
+        existing = self.traits.localize()
+        metas = dict()
+        # model name
+        metas['name'] = selfname(self)
+        class_names = self.traits.names
+        # slug field
+        try:
+            metas['slug_from'] = class_names(slug_from=True)[0]
+        except IndexError:
+            metas['slug_from'] = ''
+        # properties to index
+        metas['indexed'] = set(k for k in class_names(indexed=True))
+        # properties indexed for full text search
+        metas['fts_indexed'] = set(k for k in class_names(full_text=True))
+        # properties to HTML escape
+        metas['escaped'] = set(k for k in class_names(escaped=True))
+        existing.update(metas)
+        return existing
+
+    @classmethod
+    def _link(cls):
+        return cls.Q.app(cls.S.generic.element.link, cls.S.generics)
+
+    @defer
+    def _versioning(self, diff):
+        '''snapshot node'''
+        diff.update(_hash=self.traits.hash_diff(), _version=self._version())
+        # create new snapshot
+        version = self.n.snapshot(self, diff)
+        self._refresh()
+        return version
+
+    def _version(self):
+        '''increment current snapshot number of node'''
+        self.update({'_versions': self._current['_versions'] + 1})
+        return self._current['_versions']
+
+    def commit(self):
+        '''save this node'''
+        super(Node, self).commit()
+        # snapshot if flag set
+        if self.C.versioned:
+            self.snapshot()
+
+    @defer
+    def create(self, kw):
+        '''build node'''
+        self.source = self.n.build(kw).source
+        self._refresh()
+
+    @defer
+    def modify(self, kw):
+        '''update node'''
+        self.n.update(self, kw)
+        self._refresh()
+
+    @defer
+    def revert(self, version):
+        '''
+        change to another snapshot of this node
+
+        @param snapshot: id of previous snapshot of this node
+        '''
+        if self.C.versioned:
+            self.snapshot()
+            self.n.revert(self, version)
+            self.snapshot()
+        raise ReversionError('could not revert snapshot {0}'.format(version))
+
+    def snapshot(self):
+        '''track changes for this node'''
+        diff = self.traits.diff()
+        # if there is a change to nodes properties...
+        return self._versioning(diff) if diff else {}
+
+    @defer
+    def anchor(self, anchor, **kw):
+        '''link node to anchor'''
+        self.l.make(self.C.anchor_link, self, anchor, kw)
+
+    class Meta:
+        # anchor link label
+        anchor_link = ''
+        # fork link label
+        fork_link = ''
+        # full text index name
+        fts_index = ''
+        # index name
+        index = ''
+        # reference link label
+        reference_link = ''
+        # root index name
+        root_index = 'roots'
+        # slug field name
+        slug_field = ''
+        # snapshot link label
+        version_link = ''
+        # whether to track changes
+        versioned = False

File graphalchemy/objects/finders.py

+# -*- coding: utf-8 -*-
+'''graph model finder'''
+
+from appspace import NoAppError
+from appspace.six import string_types
+
+from graphalchemy.core import octopus, app
+
+__all__ = ('links', 'nodes')
+
+# settings
+conf = octopus.G
+
+
+class Finder(octopus.context.Thing):
+
+    '''model finder'''
+
+    def __call__(self, model=None, element=None):
+        '''
+        get default model
+
+        @param model: model class or model identifier (default: None)
+        @param element: graph database element
+        '''
+        if element is not None:
+            try:
+                return self.Q.app(element['_model'], self.S.userspace)
+            except (NoAppError, KeyError):
+                pass
+        elif isinstance(model, string_types):
+            return self.Q.app(model, self.S.userspace)
+        return model if model else self.generic
+
+
+class Links(Finder):
+
+    '''link model finder'''
+
+    # generic link model
+    generic = app(conf.generic.element.link, conf.generics)
+
+
+links = Links()
+
+
+class Nodes(Finder):
+
+    '''node model finder'''
+
+    # generic node model
+    generic = app(conf.generic.element.node, conf.generics)
+
+
+nodes = Nodes()

File graphalchemy/objects/properties.py

+# -*- coding: utf-8 -*-
+'''graph model properties'''
+
+import sys
+
+from spine.traits import Trait
+from spine.bases.keys import NoDefault
+from spine.traits import CheckedUnicode, Bool, Float as Ft, Integer, Unicode
+
+__all__ = (
+    'BooleanField', 'CharField', 'FloatField', 'IntegerField', 'StringField',
+    'TextField',
+)
+
+
+class PropertyMixin(Trait):
+
+    metadata = dict(property=True)
+
+    def __init__(self, initial=NoDefault, **kw):
+        kw.update(dict(property=True, indexed=kw.get('indexed', False)))
+        super(PropertyMixin, self).__init__(initial, **kw)
+
+
+class StringMixin(PropertyMixin):
+
+    def __init__(self, initial=NoDefault, **kw):
+        kw.update(dict(
+            escaped=kw.get('escaped', False),
+            slug_from=kw.get('slug_from', False)
+        ))
+        super(StringMixin, self).__init__(initial, **kw)
+
+
+class BooleanField(PropertyMixin, Bool):
+
+    '''boolean field'''
+
+
+class CharField(StringMixin, CheckedUnicode):
+
+    '''checked string field'''
+
+    def __init__(self, value='', minlen=0, maxlen=sys.maxsize, regex='', **kw):
+        super(CharField, self).__init__(value, minlen, maxlen, regex, **kw)
+
+
+class FloatField(PropertyMixin, Ft):
+
+    '''float field'''
+
+
+class IntegerField(PropertyMixin, Integer):
+
+    '''integer field'''
+
+
+class StringField(StringMixin, Unicode):
+
+    '''string field'''
+
+
+class TextField(StringField):
+
+    '''string that can be full text searched'''
+
+    def __init__(self, initial=NoDefault, **kw):
+        super(TextField, self).__init__(initial, **kw)
+        kw.update(dict(full_text=kw.get('full_text', False)))

File graphalchemy/objects/readers.py

+# -*- coding: utf-8 -*-
+'''graph object readers'''
+
+from appspace import AppLookupError
+
+from graphalchemy.core import octopus, app
+
+__all__ = ['Nodes']
+
+# settings
+conf = octopus.G
+
+
+class Reader(octopus.event.Worker):
+
+    '''graph objet reader mixin'''
+
+    def filter(self, index, queries, model=None):
+        '''
+        full text search using index
+
+        @param index: name of index
+        @param queries: queries built with query builder
+        @param model: element model (default: None)
+        '''
+        model = self._finder(model) if model is None else model
+        return self.r.filter(index, queries, model)
+
+    def filter_by(self, index, key, value):
+        '''
+        filter by keywords
+
+        @param index: index name
+        @param key: keyword in index
+        @param value: value in index (or second key)
+        '''
+        return self.r.filter_by(
+            index,
+            key,
+            value,
+            lambda x: self._finder(element=x)(x),
+        )
+
+    def get(self, element, model=None):
+        '''
+        get database element by id
+
+        @param element: element id
+        @param model: element model (default: None)
+        '''
+        model = self._finder(model) if model is None else model
+        return model(self.r.get(element))
+
+    def traverse(self, this, tester=None, links=None, unique=None):
+        '''
+        traverse graph, yielding graph elements
+
+        @param this: graph model instance
+        @param tester: filter for traversed elements (default: None)
+        @param links: links to traverse (default: None)
+        @param unique: how often to traverse the same element (default: None)
+        '''
+        finder = self._finder
+        for element in self.r.traverser(this, tester, links, unique):
+            yield finder(element=element)(element)
+
+
+class Nodes(Reader):
+
+    '''graph node reader'''
+
+    # model finder
+    _finder = app(conf.model.finder.node, conf.appspace)
+
+    def anchor(self, model=None):
+        '''
+        anchor node for a graph model
+
+        @param model: element model (default: None)
+        '''
+        try:
+            model = self._finder(model) if model is None else model
+            C = model.C
+            anchor_link = C.anchor_link
+            S = self.S
+            userspace = S.userspace
+            return self.Q.app(anchor_link, userspace)
+        except AppLookupError:
+            label = S.anchor.label
+            anchor = self.filter_by(C.index, label, label)
+            if anchor:
+                self.Q.register(anchor, anchor_link, userspace)
+                return anchor
+
+    def reference(self, model=None):
+        '''
+        reference node for a particular model
+
+        @param model: element model (default: None)
+        '''
+        try:
+            S = self.S
+            model = self._finder(model) if model is None else model
+            C = model.C
+            return self.Q.app(C.reference_link, S.userspace)
+        except AppLookupError:
+            label = S.reference.label
+            ref = self.filter_by(C.index, label, label)
+            if ref:
+                self.Q.register(ref, C.reference_link, S.userspace)
+                return ref
+
+    def root(self):
+        '''
+        reference root for graph
+
+        @param model: element model (default: None)
+        '''
+        try:
+            S = self.S
+            return self.Q.app(S.root, S.userspace)
+        except AppLookupError:
+            root = self.r.root
+            root = self._finder(element=root)(root)
+            self.Q.register(root, S.root, S.userspace)
+            return root
+
+    def walk(self, this, direction=None, label=None, keys=None, end='end'):
+        '''
+        iterate links and yield node from link
+
+        @param this: graph node instance
+        @param direction: direction of links (default: None)
+        @param label: label for links (default: None)
+        @param keys: keys to find on elements (default: None)
+        @param end: which end of link to return (default: 'end')
+        '''
+        finder = self._finder
+        for element in self.r.walker(this, direction, label, keys, end):
+            yield finder(element=element)(element)

File graphalchemy/objects/writers.py

+# -*- coding: utf-8 -*-
+'''graph model writers'''
+
+from itertools import count
+from functools import partial
+
+from markupsafe import escape
+
+from graphalchemy.mixins.writers import NodesMixin
+
+__all__ = ['Nodes']
+
+
+class Nodes(NodesMixin):
+
+    '''graph node writer'''
+
+    def _postprocess(self, this, model=None):
+        '''
+        postprocess graph node
+
+        @param this: graph model instance
+        @param model: graph model (default: None)
+        '''
+        if model is None:
+            model = self._finder(model)
+        this = model(this)
+        C = model.C
+        index = C.index
+        # index created date
+        index_one = self.index_one
+        if index:
+            index_one(index, 'created', self.now(), this)
+        # index any listed properties
+        indexed = C.indexed
+        if indexed:
+            self.index_many(index, this, indexed)
+        # add a slug if present
+        slug_from = C.slug_from
+        if slug_from:
+            index_one(index, slug_from, getattr(this, slug_from), this)
+        # index properties that support full text search
+        fts_indexed = C.fts_indexed
+        if fts_indexed:
+            self.index_many(C.fts_index, this, fts_indexed)
+        return this
+
+    def _preprocess(self, this, model=None):
+        '''
+        pre-process graph element
+
+        @param this: graph model instance
+        @param model: graph model (default: None)
+        '''
+        esc = self.escape
+        for k in model.C.escaped:
+            this[k] = esc(this[k])
+        this['_modified'] = self.now()
+        # slug field
+        self.autoslug(this, model)
+
+    def autoslug(self, properties, model=None):
+        '''
+        auto slug an element
+
+        @param properties: graph element properties
+        @param model: graph model (default: None)
+        '''
+        if model is None:
+            model = self._finder(model)
+        C = model.C
+        slug_from = C.slug_from
+        if not slug_from:
+            return
+        slug = tmpslug = self.Q.slugify(properties[slug_from])
+        slug_field = C.slug_field
+        # loop until unique slug is located
+        filter_by = self.r.filter_by
+        index = C.index
+        for cnt in count(1):
+            prev = filter_by(index, slug_field, tmpslug)
+            if not prev:
+                break
+            tmpslug = '{slug}-{count}'.format(slug=slug, count=cnt)
+        # add slug
+        properties[slug_field] = tmpslug
+
+    def anchor(self, model, **kw):
+        '''
+        create an anchor node
+
+        @param model: graph model (default: None)
+        '''
+        finder = self._finder
+        model = finder(model)
+        C = model.C
+        name = C.name
+        S = self.S.anchor
+        label = S.label
+        index_one = self.index_one
+        def callback(node): #@IgnorePep8
+            node = finder(element=node)(node)
+            # put in root index
+            root_index = C.root_index
+            if root_index:
+                index_one(root_index, label, name, node)
+            # put in model index
+            index = C.index
+            if index:
+                index_one(index, label, label, node)
+            return node
+        data = dict(
+            _created=self.now(),
+            _uuid=self.Q.uuid(),
+            text=S.description.format(name=name),
+        )
+        data.update(kw)
+        return self.w.build(
+            data,
+            C.reference_link,
+            model.nodes.reference(),
+            callback,
+        )
+
+    def build(self, data, model=None):
+        '''
+        build a new graph node
+
+        @param data: data
+        @param model: graph model (default: None)
+        '''
+        if model is None:
+            model = self._finder(model)
+        C = model.C
+        data.update(_created=self.now(), _model=C.name, _uuid=self.Q.uuid())
+        # number of versions to maintain
+        if C.versioned:
+            data['_versions'] = 0
+        self._preprocess(data, model)
+        return self.w.build(
+            data,
+            C.reference_link,
+            model.nodes.reference(),
+            partial(self._postprocess, model=model),
+        )
+
+    def clone(self, this, model=None):
+        '''
+        clone a graph node
+
+        @param this: graph model instance
+        @param model: graph model (default: None)
+        '''
+        created = self.now()