Skip to content

Commit

Permalink
Moved documentation from GitHub wiki to the Sphinx docs and docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
vmagamedov committed Sep 1, 2016
1 parent 4b0b74e commit d82aba2
Show file tree
Hide file tree
Showing 11 changed files with 630 additions and 0 deletions.
21 changes: 21 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import sys
import os.path
import sphinx_rtd_theme

sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
extensions = ['sphinx.ext.autodoc']

templates_path = ['_templates']
html_static_path = ['_static']
source_suffix = '.rst'
master_doc = 'index'

project = 'Hiku'
copyright = '2016, Vladimir Magamedov'
author = 'Vladimir Magamedov'

version = 'dev'
release = 'dev'

html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
307 changes: 307 additions & 0 deletions docs/guide/definition.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
Graph definition
================

Prerequisites
~~~~~~~~~~~~~

Here we will try to describe our first graph. To begin with we
will need to setup an environment:

.. code-block:: shell
$ pip install hiku
And let's create a Python module for our playground (for example ``sandbox.py``):

.. code-block:: python
from typing import List, Any
from datetime import datetime
from collections import defaultdict
from hiku.graph import Graph, Root, Edge, Field, Link, One, Many
from hiku.engine import Engine
from hiku.executors.sync import SyncExecutor
engine = Engine(SyncExecutor())
graph = Graph([
Root([
Field('datetime', lambda _: [datetime.now().isoformat()]),
]),
])
if __name__ == '__main__':
from wsgiref.simple_server import make_server
from hiku.console.ui import ConsoleApplication
app = ConsoleApplication(graph, engine, debug=True)
http_server = make_server('localhost', 5000, app)
http_server.serve_forever()
This is the simplest Graph_ with only one Field_ in the Root_ edge. You can try
to query this field using special web console, which will start when we
will try to run our module:

.. code-block:: shell
$ python sandbox.py
Just open http://localhost:5000/ in your browser and make first query:

.. code-block:: clojure
[:datetime]
You should get this result:

.. code-block:: javascript
{
"datetime": "2015-10-21T07:28:00.000000"
}
In the reference documentation you can learn about
Field_ class and it's arguments. As you can see, we are using
lambda-function as ``func`` argument and ignoring first positional
argument. This argument is a ``fields`` argument of type
``Sequence[hiku.qeury.Field]``. It is ignored because this function
is used only to resolve one fields value.

Introducing Edge and Link
~~~~~~~~~~~~~~~~~~~~~~~~~

This is cool, but what if we want to return some application data?
First of all lets define our data:

.. code-block:: python
data = {
'character': {
1: dict(name='James T. Kirk', species='Human'),
2: dict(name='Spock', species='Vulcan/Human'),
3: dict(name='Leonard McCoy', species='Human'),
},
}
Then lets extend our graph with one Edge_ and one Link_:

.. code-block:: python
def get_character_data(fields: List[hiku.query.Field], ids: List[int]) \
-> List[List[Any]]:
result = []
for id_ in ids:
character = data['character'][id_]
result.append([character[field.name] for field in fields])
return result
graph = Graph([
Edge('character', [
Field('name', get_character_data),
Field('species', get_character_data),
]),
Root([
Field('datetime', lambda _: [datetime.now().isoformat()]),
Link('characters', Many, lambda: [1, 2, 3],
edge='character', requires=None),
]),
])
Then you will be able to try this query in the console:

.. code-block:: clojure
[{:characters [:name :species]}]
And get this result:

.. code-block:: javascript
{
"characters": [
{
"species": "Human",
"name": "James T. Kirk"
},
{
"species": "Vulcan/Human",
"name": "Spock"
},
{
"species": "Human",
"name": "Leonard McCoy"
}
]
}
``get_character_data`` function is used to resolve values for two
fields in the `character` edge. As you can see
it returns basically a list of lists with values in the same order as
it was requested in arguments (order of ids and fields should be
preserved).

This gives us ability to resolve some fields simultaneously for
different objects in just one simple function when this is possible and
will improve performance (to eliminate N+1 problem and load related
data together).

Linking Edge to Edge
~~~~~~~~~~~~~~~~~~~~

Let's extend our data with one more entity - `actor`:

.. code-block:: python
data = {
'character': {
1: dict(id=1, name='James T. Kirk', species='Human'),
2: dict(id=2, name='Spock', species='Vulcan/Human'),
3: dict(id=3, name='Leonard McCoy', species='Human'),
},
'actor': {
1: dict(id=1, name='William Shatner', character_id=1),
2: dict(id=2, name='Leonard Nimoy', character_id=2),
3: dict(id=3, name='DeForest Kelley', character_id=3),
4: dict(id=4, name='Chris Pine', character_id=1),
5: dict(id=5, name='Zachary Quinto', character_id=2),
6: dict(id=6, name='Karl Urban', character_id=3),
},
}
And actor will have a reference to the played character - `character_id`.

.. code-block:: python
def get_character_data(fields: List[hiku.query.Field], ids: List[int]) \
-> List[List[Any]]:
result = []
for id_ in ids:
character = data['character'][id_]
result.append([character[field.name] for field in fields])
return result
def get_actor_data(fields: List[hiku.query.Field], ids: List[int]) \
-> List[List[Any]]:
result = []
for id_ in ids:
actor = data['actor'][id_]
result.append([actor[field.name] for field in fields])
return result
def actors_link(ids: List[int]) -> List[List[int]]:
"""Function to map character id to the list of actor ids"""
mapping = defaultdict(list)
for row in data['actor'].values():
mapping[row['character_id']].append(row['id'])
return [mapping[id_] for id_ in ids]
def character_link(ids: List[int]) -> List[int]:
"""Function to map actor id to the character id"""
mapping = {}
for row in data['actor'].values():
mapping[row['id']] = row['character_id']
return [mapping[id_] for id_ in ids]
graph = Graph([
Edge('character', [
Field('id', get_character_data),
Field('name', get_character_data),
Field('species', get_character_data),
Link('actors', Many, actors_link,
edge='actor', requires='id'),
]),
Edge('actor', [
Field('id', get_actor_data),
Field('name', get_actor_data),
Link('character', One, character_link,
edge='character', requires='id'),
]),
Root([
Field('datetime', lambda _: [datetime.now().isoformat()]),
Link('characters', Many, lambda: [1, 2, 3],
edge='character', requires=None),
]),
])
``actors`` Link_, defined in the ``character`` edge, requires ``id`` field to
map `characters` to `actors`. That's why ``id`` field was added to the
``character`` edge. The same work should be done in the ``actor`` edge to
implement backward ``character`` link.

Now we can include linked edge fields in our query:

.. code-block:: clojure
[{:characters [:name {:actors [:name]}]}]
Result would be:

.. code-block:: javascript
{
"characters": [
{
"name": "James T. Kirk",
"actors": [
{
"name": "William Shatner"
},
{
"name": "Chris Pine"
}
]
},
{ ... },
{ ... }
]
}
We can go further and follow ``character`` link from the ``actor`` edge
and return fields from ``character`` edge. This is an example of the
cyclic links, which is normal when this feature is desired for us, as long
as query is a hierarchical finite structure and result follows
it's structure.

.. code-block:: clojure
[{:characters [:name {:actors [:name {:character [:name]}]}]}]
Result with cycle:

.. code-block:: javascript
{
"characters": [
{
"name": "James T. Kirk",
"actors": [
{
"name": "William Shatner",
"character": {
"name": "James T. Kirk"
}
},
{
"name": "Chris Pine",
"character": {
"name": "James T. Kirk"
}
}
]
},
{ ... },
{ ... }
]
}
Conclusions
~~~~~~~~~~~

1. Now you know how to describe data as graph;
2. You can present in graph any data from any source.

.. _Graph: ../Reference:-graph#graph
.. _Edge: ../Reference:-graph#edge
.. _Root: ../Reference:-graph#rootedge
.. _Field: ../Reference:-graph#field
.. _Link: ../Reference:-graph#link
7 changes: 7 additions & 0 deletions docs/guide/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Guide
=====

.. toctree::
:maxdepth: 2

definition
60 changes: 60 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
**Hiku** is a library to design Graph APIs

.. toctree::
:maxdepth: 2

guide/index
reference/index

Why graphs? – They are simple, predictable, flexible, easy to compose and
because of that, they are easy to reuse.

Hiku is intended to be an answer for questions about how to speak to your
services, how to implement them and how to avoid ORMs usage.

Why not ORM? – Databases are too low level, they are implementation details of the
application/service. It is hard to abstract them properly, even with very smart and
sophisticated ORMs. Because again, databases are too low level and real-life
entities are not always possible or practical to map as 1:1 to the database schema.

::

Every piece of knowledge must have a single, unambiguous, authoritative
representation within a system.

– This is a quote from DRY principle, it says that
business logic (domain logic) should have only one single definition and
should be reused everywhere in the project. Here we are trying to make this possible
and practical to use.

Concepts
~~~~~~~~

Graphs are composed of edges, fields and links.

You can define fields, links and edges right in the implicit **root** of the
graph, which means that to access them, you do not need to know their
identity (ID), so they are singleton objects.

All other data (probably the largest) are represented in the form of a
network of edges, which should be referenced by identity and can only
be reached via a link.

There are two types of links: pointing to one object and pointing
to many objects.

Two-level Graph
~~~~~~~~~~~~~~~

This is how to properly abstract databases and other data sources into highly
reusable high-level entities.

You describe low-level graph to map your database as 1:1, every edge will be
a table, every field would be a column and every link will be a query,
to map one table to another.

High-level graph are edges with expressions instead of simple fields,
each expression describes how to compute high-level value using low-level graph. When you
are asking to retrieve some values from high-level graph, `hiku` generates a
query for the low-level graph, to retrieve minimal required data from database
to compute expressions in high-level graph.

0 comments on commit d82aba2

Please sign in to comment.