-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Moved documentation from GitHub wiki to the Sphinx docs and docstrings
- Loading branch information
1 parent
4b0b74e
commit d82aba2
Showing
11 changed files
with
630 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
Guide | ||
===== | ||
|
||
.. toctree:: | ||
:maxdepth: 2 | ||
|
||
definition |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Oops, something went wrong.