Basics#
Here we will try to describe our first graph. To begin with we will need to setup an environment:
$ pip install hiku
Simplest one-field graph#
Note
Source code of this example can be found on GitHub.
Let’s define graph with only one field, which is easy to compute, for example it would be a current time:
from datetime import datetime
from hiku.graph import Graph, Root, Field
GRAPH = Graph([
Root([
Field('now', None, lambda _: [datetime.now().isoformat()]),
]),
])
This is the simplest Graph
with one
Field
in the Root
node.
Note
We are using lambda-function and ignoring it’s first argument because
this function is used to load only one field and this field in the
Root
node. In other cases you will need to use this
and possibly other required arguments.
Then this field could be queried using this query:
{ now }
To perform this query let’s define a helper function execute
:
from hiku.engine import Engine
from hiku.result import denormalize
from hiku.executors.sync import SyncExecutor
from hiku.readers.graphql import read
hiku_engine = Engine(SyncExecutor())
def execute(graph, query_string):
query = read(query_string)
result = hiku_engine.execute(graph, query)
return denormalize(graph, result)
Then we will be ready to execute our query:
result = execute(GRAPH, '[:now]')
assert result == {'now': '2015-10-21T07:28:00'}
You can also test this graph using special web console application, this is how to setup and run it:
from hiku.console.ui import ConsoleApplication
app = ConsoleApplication(GRAPH, hiku_engine, debug=True)
if __name__ == '__main__':
from wsgiref.simple_server import make_server
http_server = make_server('localhost', 5000, app)
http_server.serve_forever()
Then just open http://localhost:5000/ url in your browser and perform query from the console.
Introducing nodes and links#
Note
Source code of this example can be found on GitHub.
This is cool, but what if we want to return some application data? First of all lets define our data:
data = {
'Character': {
1: dict(name='James T. Kirk', species='Human'),
2: dict(name='Spock', species='Vulcan/Human'),
3: dict(name='Leonard McCoy', species='Human'),
},
}
Note
For simplicity we will use in-memory data structures to store our data. How to load data from more sophisticated sources like databases will be explained in the next chapters.
Then lets define our graph with one Node
and one
Link
:
1from hiku.graph import Graph, Root, Field, Node, Link
2from hiku.types import TypeRef, Sequence
3from hiku.engine import Engine
4from hiku.result import denormalize
5from hiku.readers.graphql import read
6from hiku.executors.sync import SyncExecutor
7
8def character_data(fields, ids):
9 result = []
10 for id_ in ids:
11 character = data['Character'][id_]
12 result.append([character[field.name] for field in fields])
13 return result
14
15def to_characters_link():
16 return [1, 2, 3]
17
18GRAPH = Graph([
19 Node('Character', [
20 Field('name', None, character_data),
21 Field('species', None, character_data),
22 ]),
23 Root([
24 Link('characters', Sequence[TypeRef['Character']],
25 to_characters_link, requires=None),
26 ]),
27])
character_data
function [8] is used to resolve values for two fields
in the Character
node. 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 function used twice in the graph [20-21] – for two fields, this is how Hiku understands that both these fields can be loaded using this one function and one function call. Hiku groups fields by function, to load them together.
This gives us ability to resolve many fields for many objects (ids) using just
one simple function (when possible) to efficiently load data without introducing
lots of queries (to eliminate N+1
problem, for example).
to_characters_link
function [15] is used to make a link
[24-25] from the Root
node to the Character
node. This function should return character ids.
So now you are able to try this query in the console:
{ characters { name species } }
Or in the program:
result = execute(GRAPH, "{ characters { name species } }")
assert result == {
"characters": [
{
"species": "Human",
"name": "James T. Kirk",
},
{
"species": "Vulcan/Human",
"name": "Spock",
},
{
"species": "Human",
"name": "Leonard McCoy",
},
],
}
Linking node to node#
Note
Source code of this example can be found on GitHub.
Let’s extend our data with one more entity - Actor
:
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, character_id=1, name='William Shatner'),
2: dict(id=2, character_id=2, name='Leonard Nimoy'),
3: dict(id=3, character_id=3, name='DeForest Kelley'),
4: dict(id=4, character_id=1, name='Chris Pine'),
5: dict(id=5, character_id=2, name='Zachary Quinto'),
6: dict(id=6, character_id=3, name='Karl Urban'),
},
}
Where actor will have a reference to the played character – character_id
.
We will also need id
fields in both nodes in order to link them with each
other.
Here is our extended graph definition:
1from collections import defaultdict
2
3from hiku.graph import Graph, Root, Field, Node, Link
4from hiku.types import TypeRef, Sequence
5
6def character_data(fields, ids):
7 result = []
8 for id_ in ids:
9 character = data['Character'][id_]
10 result.append([character[field.name] for field in fields])
11 return result
12
13def actor_data(fields, ids):
14 result = []
15 for id_ in ids:
16 actor = data['Actor'][id_]
17 result.append([actor[field.name] for field in fields])
18 return result
19
20def character_to_actors_link(ids):
21 mapping = defaultdict(list)
22 for row in data['Actor'].values():
23 mapping[row['character_id']].append(row['id'])
24 return [mapping[id_] for id_ in ids]
25
26def actor_to_character_link(ids):
27 mapping = {}
28 for row in data['Actor'].values():
29 mapping[row['id']] = row['character_id']
30 return [mapping[id_] for id_ in ids]
31
32def to_characters_link():
33 return [1, 2, 3]
34
35GRAPH = Graph([
36 Node('Character', [
37 Field('id', None, character_data),
38 Field('name', None, character_data),
39 Field('species', None, character_data),
40 Link('actors', Sequence[TypeRef['Actor']],
41 character_to_actors_link, requires='id'),
42 ]),
43 Node('Actor', [
44 Field('id', None, actor_data),
45 Field('name', None, actor_data),
46 Link('character', TypeRef['Character'],
47 actor_to_character_link, requires='id'),
48 ]),
49 Root([
50 Link('characters', Sequence[TypeRef['Character']],
51 to_characters_link, requires=None),
52 ]),
53])
Here actors
Link
[40-41], defined in the
Character
node [36], requires='id'
field [37] to map
characters to actors. That’s why id
field [37] was added to the
Character
node [36]. The same work should be done in the Actor
node [43] to implement backward character
link [46-47].
requires
argument can be specified as a list of fields, in this case
Hiku
will resolve all of them and pass a list
of dict
to resolver.
character_to_actors_link
function [20] accepts ids of the characters
and should return list of lists – ids of the actors, in the same order, so
every character id can be associated with a list of actor ids. This is how
one to many links works.
actor_to_character_link
function [26] requires/accepts ids of the
actors and returns ids of the characters in the same order. This is how
many to one links works.
So now we can include linked node fields in our query:
result = execute(GRAPH, "{ characters { name actors { name } } }")
assert result == {
'characters': [
{'name': 'James T. Kirk',
'actors': [{'name': 'William Shatner'},
{'name': 'Chris Pine'}]},
{'name': 'Spock',
'actors': [{'name': 'Leonard Nimoy'},
{'name': 'Zachary Quinto'}]},
{'name': 'Leonard McCoy',
'actors': [{'name': 'DeForest Kelley'},
{'name': 'Karl Urban'}]},
],
}
We can go further and follow character
link from the Actor
node and
return fields from Character
node. This is an example of the cyclic links,
which is normal when this feature is desired, as long as query is a hierarchical
finite structure and result follows it’s structure.
1result = execute(GRAPH, "{ characters { name actors { name character { name } } } }")
2assert result == {
3 'characters': [
4 {'name': 'James T. Kirk',
5 'actors': [{'name': 'William Shatner',
6 'character': {'name': 'James T. Kirk'}},
7 {'name': 'Chris Pine',
8 'character': {'name': 'James T. Kirk'}}]},
9 {'name': 'Spock',
10 'actors': [{'name': 'Leonard Nimoy',
11 'character': {'name': 'Spock'}},
12 {'name': 'Zachary Quinto',
13 'character': {'name': 'Spock'}}]},
14 {'name': 'Leonard McCoy',
15 'actors': [{'name': 'DeForest Kelley',
16 'character': {'name': 'Leonard McCoy'}},
17 {'name': 'Karl Urban',
18 'character': {'name': 'Leonard McCoy'}}]},
19 ],
20}
As you can see, there are duplicate entries in the result [11,13,15] – this is how our cycle can be seen, the same character Spock seen multiple times.