├── .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 | --------------------------------------------------------------------------------