├── .gitignore
├── setup.py
├── LICENSE
├── README.md
└── graphcommons.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | .idea
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='graphcommons',
5 | version='1.0.1',
6 | py_modules=['graphcommons'],
7 | url='https://github.com/graphcommons/graphcommons-python',
8 | license='MIT',
9 | author='fatiherikli',
10 | author_email='fatiherikli@gmail.com',
11 | description='Python Wrapper For Graph Commons API.',
12 | install_requires=['requests==2.5.3'],
13 | )
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Graph Commons
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Python Wrapper For Graph Commons API.
2 |
3 | More detailed API documentation:
4 |
5 |
6 |
7 | ### Installation
8 |
9 | ```
10 | pip install graphcommons
11 | ```
12 |
13 | ### Usage
14 |
15 | #### Authentication
16 |
17 | ```python
18 | >>> from graphcommons import GraphCommons
19 | >>> graphcommons = GraphCommons('')
20 | >>> graphcommons.status()
21 | {u'msg': u'Working'}
22 | ```
23 |
24 | ### Get graph
25 | ```python
26 | graph = graphcommons.graphs('7141da86-2a40-4fdc-a7ac-031b434b9653')
27 | print(graph['name']) # Hello from python
28 |
29 | for node in graph.nodes:
30 | print(node['name'])
31 |
32 | print(graph.edges_from(node)) # edges directed from the node
33 | print(graph.edges_to(node)) # edges directed to the node
34 | ```
35 |
36 | ### New Graph
37 | ```python
38 | from graphcommons import Signal
39 |
40 | graph = graphcommons.new_graph(
41 | name="Hello from python",
42 | description="Python Wrapper Test",
43 | signals=[
44 | Signal(
45 | action="node_create",
46 | name="Ahmet",
47 | type="Person",
48 | description="nice guy"
49 | ),
50 | Signal(
51 | action="edge_create",
52 | from_name="Ahmet",
53 | from_type="Person",
54 | to_name="Burak",
55 | to_type="Person",
56 | name="COLLABORATED",
57 | weight=2
58 | )
59 | ]
60 | )
61 |
62 |
63 | print(graph['id']) # added graph's id
64 | ```
65 |
66 | ### Update Graph
67 |
68 | ```python
69 | from graphcommons import Signal
70 |
71 | graphcommons.update_graph(
72 | id="7141da86-2a40-4fdc-a7ac-031b434b9653",
73 | signals=[
74 | Signal(
75 | action="node_create",
76 | name="Ahmet",
77 | type="Person",
78 | description="nice guy"
79 | ),
80 | Signal(
81 | action="edge_create",
82 | from_name="Ahmet",
83 | from_type="Person",
84 | to_name="Burak",
85 | to_type="Person",
86 | name="COLLABORATED",
87 | weight=2
88 | )
89 | ]
90 | )
91 | ```
92 |
--------------------------------------------------------------------------------
/graphcommons.py:
--------------------------------------------------------------------------------
1 | from collections import UserDict
2 | from requests.api import request
3 | from itertools import chain
4 |
5 |
6 | class Entity(UserDict):
7 | """Base class for all objects."""
8 |
9 |
10 | class Signal(Entity):
11 | """Represents a signal operation in the Graph Commons infrastructure."""
12 |
13 |
14 | class Path(Entity):
15 | """Represents a path object in the Graph Commons infrastructure."""
16 |
17 |
18 | class Edge(Entity):
19 | """Represents an edge object in the Graph Commons infrastructure."""
20 |
21 | def to_signal(self, action, graph):
22 | # signal types: create or update
23 | action = "edge_%s" % action
24 |
25 | from_id, to_id = map(self.get, ('from', 'to'))
26 | from_node = graph.get_node(from_id)
27 | to_node = graph.get_node(to_id)
28 | kwargs = dict(action=action,
29 | name=self['edge_type'],
30 | from_name=from_node['name'],
31 | from_type=from_node['type'],
32 | to_name=to_node['name'],
33 | to_type=to_node['type'],
34 | reference=self.get('reference', None),
35 | weight=self.get('weight'),
36 | properties=self['properties'])
37 | return Signal(**kwargs)
38 |
39 |
40 | class Node(Entity):
41 | """Represents a node object in the Graph Commons infrastructure."""
42 |
43 | def to_signal(self, action, graph):
44 | # signal types: create or update
45 | action = "node_%s" % action
46 | node_type = graph.get_node_type(self['type_id'])
47 | kwargs = dict(action=action,
48 | name=self['name'],
49 | type=self['type'],
50 | reference=self.get('reference', None),
51 | image=self.get('image', None),
52 | color=node_type['color'],
53 | url=self.get('url', None),
54 | description=self['description'],
55 | properties=self['properties'])
56 | return Signal(**kwargs)
57 |
58 |
59 | class EdgeType(Entity):
60 | """Represents an edge type object in the Graph Commons infrastructure."""
61 |
62 | def to_signal(self, action):
63 | action = "edgetype_%s" % action
64 | kwargs = dict(action=action)
65 | keys = ['name', 'color', 'name_alias', 'weighted', 'properties',
66 | 'image_as_icon', 'image']
67 | kwargs.update(dict((k, self.get(k, None)) for k in keys))
68 | return Signal(**kwargs)
69 |
70 |
71 | class NodeType(Entity):
72 | """Represents a node type object in the Graph Commons infrastructure."""
73 |
74 | def to_signal(self, action):
75 | action = "nodetype_%s" % action
76 | kwargs = dict(action=action)
77 | keys = ['name', 'color', 'name_alias', 'size_limit', 'properties',
78 | 'image_as_icon', 'image', 'size']
79 | kwargs.update(dict((k, self.get(k, None)) for k in keys))
80 | return Signal(**kwargs)
81 |
82 |
83 | class Graph(Entity):
84 | def __init__(self, *args, **kwargs):
85 | super(Graph, self).__init__(*args, **kwargs)
86 | self.edges = list(map(Edge, self['edges'] or []))
87 | self.nodes = list(map(Node, self['nodes'] or []))
88 | self.node_types = list(map(NodeType, self['nodeTypes'] or []))
89 | self.edge_types = list(map(EdgeType, self['edgeTypes'] or []))
90 |
91 | # hash for quick search
92 | self._edges = dict((edge['id'], edge) for edge in self.edges)
93 | self._nodes = dict((node['id'], node) for node in self.nodes)
94 | self._node_types = dict((t['id'], t) for t in self.node_types)
95 | self._edge_types = dict((t['id'], t) for t in self.edge_types)
96 |
97 | def get_node(self, node_id):
98 | return self._nodes.get(node_id, None)
99 |
100 | def get_edge_type(self, edge_type_id):
101 | return self._edge_types.get(edge_type_id, None)
102 |
103 | def get_node_type(self, node_type_id):
104 | return self._node_types.get(node_type_id, None)
105 |
106 | def edges_for(self, node, direction):
107 | if isinstance(node, str):
108 | node = self.get_node(node)
109 |
110 | return [edge for edge in self.edges
111 | if edge[direction] == node['id']]
112 |
113 | def edges_from(self, node):
114 | return self.edges_for(node, 'from')
115 |
116 | def edges_to(self, node):
117 | return self.edges_for(node, 'to')
118 |
119 | def sync(self, graph_commons):
120 | """Synchronize local and remote representations."""
121 | if self['id'] is None:
122 | return
123 |
124 | remote_graph = graph_commons.graphs(self['id'])
125 |
126 | # TODO: less forceful, more elegant
127 | self.edges = remote_graph.edges
128 | self.nodes = remote_graph.nodes
129 | self.node_types = remote_graph.node_types
130 | self.edge_types = remote_graph.edge_types
131 | self._edges = dict((edge['id'], edge) for edge in self.edges)
132 | self._nodes = dict((node['id'], node) for node in self.nodes)
133 | self._node_types = dict((t['id'], t) for t in self.node_types)
134 | self._edge_types = dict((t['id'], t) for t in self.edge_types)
135 |
136 |
137 | class GraphCommonsException(Exception):
138 | def __init__(self, status_code, message):
139 | self.status_code = status_code
140 |
141 | # if isinstance(message, unicode):
142 | # message = message.encode("utf-8") # Otherwise, it will not be printed.
143 |
144 | self.message = message
145 |
146 | def __str__(self):
147 | return self.message
148 |
149 |
150 | class GraphCommons(object):
151 | BASE_URL = "https://graphcommons.com/api/v1"
152 | ERROR_CODES = [400, 401, 403, 404, 405, 500]
153 |
154 | def __init__(self, api_key, base_url=None):
155 | self.api_key = api_key
156 | self.base_url = base_url or self.BASE_URL
157 |
158 | def build_url(self, endpoint, id=None):
159 | parts = filter(bool, [self.base_url, endpoint, id])
160 | return '/'.join(parts)
161 |
162 | @staticmethod
163 | def get_error_message(response):
164 | try:
165 | bundle = response.json()
166 | except (ValueError, TypeError):
167 | return response.content
168 | return bundle['msg']
169 |
170 | def make_request(self, method, endpoint, data=None, id=None):
171 | response = request(method, self.build_url(endpoint, id), json=data,
172 | headers={"Authentication": self.api_key,
173 | "Content-Type": "application/json"
174 | }
175 | )
176 |
177 | if response.status_code in self.ERROR_CODES:
178 | raise GraphCommonsException(status_code=response.status_code,
179 | message=GraphCommons.get_error_message(response))
180 |
181 | return response
182 |
183 | def status(self):
184 | response = self.make_request('get', 'status')
185 | return Entity(**response.json())
186 |
187 | def graphs(self, id=None):
188 | response = self.make_request('get', 'graphs', id=id)
189 | return Graph(**response.json()['graph'])
190 |
191 | def nodes(self, id=None):
192 | response = self.make_request('get', 'nodes', id=id)
193 | return Node(**response.json()['node'])
194 |
195 | def new_graph(self, signals=None, **kwargs):
196 | if signals is not None:
197 | kwargs['signals'] = list(map(dict, signals))
198 | response = self.make_request('post', 'graphs', data=kwargs)
199 | return Graph(**response.json()['graph'])
200 |
201 | def update_graph(self, id, signals=None, **kwargs):
202 | if signals is not None:
203 | kwargs['signals'] = list(map(dict, signals))
204 | endpoint = 'graphs/%s/add' % id
205 | response = self.make_request('put', endpoint, data=kwargs)
206 | return Graph(**response.json()['graph'])
207 |
208 | def clear_graph(self, graph_id):
209 | graph = self.graphs(graph_id)
210 | # Remove all nodes. (This also removes all edges.)
211 | signals = map(lambda node: dict(action="node_delete", id=node.id), graph.nodes)
212 | endpoint = "graphs/{}/add".format(graph_id)
213 | kwargs = dict(signals=signals)
214 | response = self.make_request("put", endpoint, data=kwargs)
215 | return Graph(**response.json()['graph'])
216 |
217 | def create_graph_from_path(self, name, paths, base_graph):
218 |
219 | kwargs = dict((k, base_graph.get(k, None)) for k in ['status', 'license', 'users', 'layout'])
220 | kwargs['name'] = name
221 | subtitle = ""
222 | description = ""
223 | edges = []
224 | nodes = []
225 | edge_type_ids = []
226 | node_type_ids = []
227 | for path in paths:
228 | description = u"{}\n{}".format(description, path.path_string)
229 | subtitle = u"{}\n{}".format(subtitle, path.path_string)
230 |
231 | # Type ids
232 | edge_type_ids.extend([edge.type_id for edge in path.edges])
233 | node_type_ids.extend([node.type['id'] for node in path.nodes])
234 |
235 | edges.extend(path.edges)
236 | nodes.extend(path.nodes)
237 |
238 | # Add explanation of original graph.
239 | kwargs['description'] = u"{}\n{}".format(description, base_graph.description)
240 | kwargs['subtitle'] = u"{}\n{}".format(subtitle, base_graph.subtitle)
241 |
242 | # Get edge_types by using their ids.
243 | edge_types = map(base_graph.get_edge_type, set(edge_type_ids))
244 | node_types = map(base_graph.get_node_type, set(node_type_ids))
245 |
246 | # Add node and edge type Signals first.
247 | signals = map(lambda entity: entity.to_signal('create'), chain(node_types, edge_types))
248 |
249 | edge_ids = set()
250 | node_ids = set()
251 |
252 | # Remove duplicates for efficiency in graph creation.
253 | edges = [e for e in edges if not (e.id in edge_ids or edge_ids.add(e.id))]
254 | nodes = [n for n in nodes if not (n.id in node_ids or node_ids.add(n.id))]
255 |
256 | # Add node and edge Signals.
257 | signals.extend(map(lambda entity: entity.to_signal('create', base_graph), chain(edges, nodes)))
258 | return self.new_graph(signals=signals, **kwargs)
259 |
260 | def paths(self, graph_id, kwargs):
261 | end_point = 'graphs/%s/paths' % graph_id
262 | response = self.make_request("get", end_point, data=kwargs).json()
263 | edges = response['edges']
264 | nodes = response['nodes']
265 | paths = []
266 | for path in response['paths']:
267 | p = {'edges': map(Edge, (edges[edge_id] for edge_id in path['edges'])),
268 | 'nodes': map(Node, (nodes[node_id] for node_id in path['nodes'])), 'dirs': path['dirs'],
269 | 'path_string': path['path_string']}
270 | paths.append(Path(**p))
271 | return paths
272 |
--------------------------------------------------------------------------------