Skip to content

Commit

Permalink
done designing the vertex DSL
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielfalcao committed Aug 19, 2017
1 parent 7ead680 commit 3d237f3
Show file tree
Hide file tree
Showing 22 changed files with 479 additions and 405 deletions.
18 changes: 9 additions & 9 deletions docs/source/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@ Concepts

Influenced by the `hexastore <http://www.vldb.org/pvldb/1/1453965.pdf>`_ paper.

- Every *subject name* is a root tree in the repository.
- Every *edge name* is a root tree in the repository.
- Every *object* is stored as a git blob, but has a unique uuid which can be accessed through a special index.
- Every *indexed* **predicate** is a sub-tree containing blobs whose name in the tree is the blob_id of the original object, its value is the indexed value itself.
- Objects are stored in the tree under the path: ``SubjectName/objects/:blob_id``
- The blob-id of an **Object** can be retrieved at ``SubjectName/_ids/:uuid4``
- The *uuid4* of an **Object** can be retrieved at ``SubjectName/_uuids/:blob_id``
- Indexed predicates are stored in the tree with the path: ``SubjectName/indexes/<index name>/:blob_id``
- Objects are stored in the tree under the path: ``EdgeName/objects/:blob_id``
- The blob-id of an **Object** can be retrieved at ``EdgeName/_ids/:uuid4``
- The *uuid4* of an **Object** can be retrieved at ``EdgeName/_uuids/:blob_id``
- Indexed predicates are stored in the tree with the path: ``EdgeName/indexes/<index name>/:blob_id``


Supported Operations
~~~~~~~~~~~~~~~~~~~~

- Create/Merge subjects by ``uuid4``
- Retrieve subjects by ``uuid4``
- Retrieve subjects by ``blob_id``
- Retrieve subjects by *indexed predicates*
- Create/Merge edges by ``uuid4``
- Retrieve edges by ``uuid4``
- Retrieve edges by ``blob_id``
- Retrieve edges by *indexed predicates*
- Delete nodes with all their references
18 changes: 9 additions & 9 deletions docs/source/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ Instalation
pip install plural
Declaring Subjects
Declaring Edges
------------------


.. code:: python
from plural import Plural
from plural import Subject
from plural import Edge
store = Plural('my-git-cms')
class Document(Subject):
class Document(Edge):
indexes = {'title'}
predicates = (
('title', codec.Unicode),
Expand Down Expand Up @@ -78,11 +78,11 @@ One By UUID

.. code:: python
# Using the class Document as subject type
docs1 = store.get_subject_by_uuid(Document, uuid1)
# Using the class Document as edge type
docs1 = store.get_edge_by_uuid(Document, uuid1)
# Using the subject label
docs2 = store.get_subject_by_uuid('Document', uuid2)
# Using the edge label
docs2 = store.get_edge_by_uuid('Document', uuid2)
Many By Indexed Predicate
Expand All @@ -97,11 +97,11 @@ Many By Indexed Predicate
# DSL
query = predicate('title').contains('Blog')
blog_documents = set(store.match_subjects_by_index(Document, 'title', query))
blog_documents = set(store.match_edges_by_index(Document, 'title', query))
# With Regex
query = predicate('title').matches('([Bb]log|[Ee]ssa[yi]s?)')
blogs_and_essays = set(store.match_subjects_by_index(Document, 'title', query))
blogs_and_essays = set(store.match_edges_by_index(Document, 'title', query))
Update
------
Expand Down
34 changes: 24 additions & 10 deletions plural/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,40 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from plural.models.edges import resolve_subject_name
from plural.models.edges import resolve_subject
from plural.models.edges import resolve_edge_name
from plural.models.edges import resolve_edge
from plural.store import serialize_commit
from plural.store import PluralStore
from plural.models.edges import Subject
from plural.exceptions import SubjectDefinitionNotFound
from plural.exceptions import InvalidSubjectDefinition
from plural.models.edges import Edge
from plural.models.vertexes import Vertex
from plural.models.vertexes import IncomingVertex
from plural.models.vertexes import OutgoingVertex
from plural.models.vertexes import IndirectVertex
from plural.models.meta.vertexes import incoming_vertex
from plural.models.meta.vertexes import outgoing_vertex
from plural.models.meta.vertexes import indirect_vertex
from plural.exceptions import EdgeDefinitionNotFound
from plural.exceptions import InvalidEdgeDefinition


class Plural(PluralStore):
pass


__all__ = (
'resolve_subject',
'resolve_subject_name',
'resolve_edge',
'resolve_edge_name',
'serialize_commit',
'PluralStore',
'Plural',
'Subject',
'SubjectDefinitionNotFound',
'InvalidSubjectDefinition',
'Edge',
'Vertex',
'IncomingVertex',
'OutgoingVertex',
'IndirectVertex',
'incoming_vertex',
'outgoing_vertex',
'indirect_vertex',
'EdgeDefinitionNotFound',
'InvalidEdgeDefinition',
)
16 changes: 12 additions & 4 deletions plural/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.


class InvalidSubjectDefinition(Exception):
"""raised when a :py:class:`Subject` has an invalid definition"""
class InvalidElementDefinition(Exception):
"""raised when a :py:class:`Element` has an invalid definition"""


class InvalidEdgeDefinition(InvalidElementDefinition):
"""raised when a :py:class:`Edge` has an invalid definition"""


class InvalidVertexDefinition(InvalidElementDefinition):
"""raised when a :py:class:`Vertex` has an invalid definition"""


class ElementDefinitionNotFound(Exception):
Expand All @@ -29,5 +37,5 @@ class VertexDefinitionNotFound(ElementDefinitionNotFound):
"""raised when a :py:class:`Vertex` has an invalid definition"""


class SubjectDefinitionNotFound(ElementDefinitionNotFound):
"""raised when a :py:class:`Subject` has an invalid definition"""
class EdgeDefinitionNotFound(ElementDefinitionNotFound):
"""raised when a :py:class:`Edge` has an invalid definition"""
79 changes: 52 additions & 27 deletions plural/models/edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,90 +15,115 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from plural.models.meta.edges import MetaSubject
from plural.models.meta.edges import MetaEdge
from plural.models.meta.edges import SUBJECTS
# from plural.models.meta.edges import is_edge_subclass

from plural.models.element import Element

from plural.exceptions import SubjectDefinitionNotFound
from plural.exceptions import EdgeDefinitionNotFound


def resolve_subject_name(subj):
def resolve_edge_name(subj):
"""
:param subj: an :py:class:`Subject` instance or string
:param subj: an :py:class:`Edge` instance or string
:returns: a string
"""
if isinstance(subj, type) and issubclass(subj, Subject):
if isinstance(subj, type) and issubclass(subj, Edge):
return subj.__name__
elif subj is None:
return '*'
elif isinstance(subj, basestring):
return subj
else:
msg = (
'resolve_subject_name() takes a Subject subclass, '
'resolve_edge_name() takes a Edge subclass, '
'a string or None. Got {}'
)
raise TypeError(msg.format(repr(subj)))


def resolve_subject(subj):
def resolve_edge(subj):
"""
:param subj: an :py:class:`Subject` instance or string
:returns: a :py:class:`Subject` subclass
:param subj: an :py:class:`Edge` instance or string
:returns: a :py:class:`Edge` subclass
"""
if isinstance(subj, type) and issubclass(subj, Subject):
if isinstance(subj, type) and issubclass(subj, Edge):
return subj
elif isinstance(subj, basestring):
return SUBJECTS[subj]
else:
msg = (
'resolve_subject() takes a Subject subclass or '
'resolve_edge() takes a Edge subclass or '
'a string. Got {}'
)
raise TypeError(msg.format(repr(subj)))


class Subject(Element):
class Edge(Element):
"""represents a node type (or "model", if you will)."""
__metaclass__ = MetaSubject
__metaclass__ = MetaEdge

@staticmethod
def definition(name):
"""
resolves a :py:class:`Subject` by its type name
resolves a :py:class:`Edge` by its type name
:param name: a dictionary
:returns: the given :py:class:`Subject` subclass
:returns: the given :py:class:`Edge` subclass
"""
try:
return resolve_subject(name)
return resolve_edge(name)
except KeyError:
raise SubjectDefinitionNotFound('there are no subject subclass defined with the name "{}"'.format(name))
raise EdgeDefinitionNotFound('there are no edge subclass defined with the name "{}"'.format(name))

def get_related_blob_paths(self, repository):
"""
returns a list of all possible blob paths of a :py:class:`Subject` instance.
returns a list of all possible blob paths of a :py:class:`Edge` instance.
:param repository: a ``pygit2.Repository``
:returns: the given :py:class:`Subject` subclass
:returns: the given :py:class:`Edge` subclass
"""
subject_name = self.__class__.__name__
edge_name = self.__class__.__name__
uuid = self.uuid
blob_id_path = '{subject_name}/_ids/{uuid}'.format(**locals())
blob_id_path = '{edge_name}/_ids/{uuid}'.format(**locals())
blob_id = repository[repository.index[blob_id_path].oid].data
context = {
'subject_name': subject_name,
'edge_name': edge_name,
'blob_id': blob_id,
'uuid': uuid,
}
paths = [
'{subject_name}/_ids/{uuid}',
'{subject_name}/_uuids/{blob_id}',
'{subject_name}/indexes/uuid/{blob_id}',
'{subject_name}/objects/{blob_id}',
'{edge_name}/_ids/{uuid}',
'{edge_name}/_uuids/{blob_id}',
'{edge_name}/indexes/uuid/{blob_id}',
'{edge_name}/objects/{blob_id}',
]
for predicate in self.indexes:
context['predicate'] = predicate
paths.append('{subject_name}/indexes/{predicate}/{blob_id}'.format(**context))
paths.append('{edge_name}/indexes/{predicate}/{blob_id}'.format(**context))

return map(lambda path: path.format(**context), paths)

@classmethod
def from_data(cls, ___name___=None, **kw):
"""
creates a new instance of the given :py:class:`Element` with the provided ``**kwargs``
:param ___name___: the name of the element type
:param ``**kw``: a dictionary
:returns: an instance of the given :py:class:`Element` subclass
"""
name = ___name___ or cls.__name__
Definition = cls.definition(name)
return Definition(**kw)

@classmethod
def from_dict(Definition, data):
"""
creates a new instance of the given :py:class:`Element` with the provided ``data``
:param data: a dictionary
:returns: an instance of the given :py:class:`Element` subclass
"""
return Definition(**data)
70 changes: 2 additions & 68 deletions plural/models/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@
from hashlib import sha256
from plural.util import generate_uuid

from plural.exceptions import ElementDefinitionNotFound


class Element(object):
"""represents a node type (or "model", if you will)."""
"""json-serializable data-model for Edges and Vertices"""

def __init__(self, uuid=None, **kw):
self.uuid = kw.get('uuid', uuid)
Expand Down Expand Up @@ -76,78 +74,14 @@ def encode_field(self, name, value):

return codec.encode(value)

@staticmethod
def definition(name):
"""
resolves a :py:class:`Element` by its type name
:param name: a dictionary
:returns: the given :py:class:`Element` subclass
"""
try:
return resolve_element(name)
except KeyError:
raise ElementDefinitionNotFound('there are no element subclass defined with the name "{}"'.format(name))

@classmethod
def from_data(cls, ___name___=None, **kw):
"""
creates a new instance of the given :py:class:`Element` with the provided ``**kwargs``
:param ___name___: the name of the element type
:param ``**kw``: a dictionary
:returns: an instance of the given :py:class:`Element` subclass
"""
name = ___name___ or cls.__name__
Definition = cls.definition(name)
return Definition(**kw)

@classmethod
def from_dict(Definition, data):
"""
creates a new instance of the given :py:class:`Element` with the provided ``data``
:param data: a dictionary
:returns: an instance of the given :py:class:`Element` subclass
"""
return Definition(**data)

def get_related_blob_paths(self, repository):
"""
returns a list of all possible blob paths of a element instance.
:param repository: a ``pygit2.Repository``
:returns: the given :py:class:`Element` subclass
"""
element_name = self.__class__.__name__
uuid = self.uuid
blob_id_path = '{element_name}/_ids/{uuid}'.format(**locals())
blob_id = repository[repository.index[blob_id_path].oid].data
context = {
'element_name': element_name,
'blob_id': blob_id,
'uuid': uuid,
}
paths = [
'{element_name}/_ids/{uuid}',
'{element_name}/_uuids/{blob_id}',
'{element_name}/indexes/uuid/{blob_id}',
'{element_name}/objects/{blob_id}',
]
for predicate in self.indexes:
context['predicate'] = predicate
paths.append('{element_name}/indexes/{predicate}/{blob_id}'.format(**context))

return map(lambda path: path.format(**context), paths)

def __hash__(self):
return int(sha256(self.to_json()).hexdigest(), 16)

def __str__(self):
return ''.join([self.__class__.__name__, self.to_json()])

def __repr__(self):
return bytes(self)
return ".".join([self.__class__.__module__, bytes(self)])

def __eq__(self, other):
return isinstance(other, Element) and self.to_dict() == other.to_dict()

0 comments on commit 3d237f3

Please sign in to comment.