-
Notifications
You must be signed in to change notification settings - Fork 266
Expand file tree
/
Copy pathgephi.py
More file actions
264 lines (212 loc) · 10.2 KB
/
gephi.py
File metadata and controls
264 lines (212 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# vim:ts=4:sw=4:sts=4:et
# -*- coding: utf-8 -*-
"""Classes that help igraph communicate with Gephi (http://www.gephi.org)."""
import json
import urllib.error
import urllib.parse
import urllib.request
__all__ = ("GephiConnection", "GephiGraphStreamer", "GephiGraphStreamingAPIFormat")
__docformat__ = "restructuredtext en"
class GephiConnection:
"""Object that represents a connection to a Gephi master server."""
def __init__(self, url=None, host="127.0.0.1", port=8080, workspace=1):
"""Constructs a connection to a Gephi master server.
The connection object can be constructed either by specifying the
``url`` directly, or by specifying the ``host``, ``port`` and
``workspace`` arguments. The latter three are evaluated only if
``url`` is None; otherwise the ``url`` will take precedence.
The ``url`` argument does not have to include the operation (e.g.,
``?operation=updateGraph``); the connection will take care of it.
E.g., if you wish to connect to workspace 2 in a local Gephi instance
on port 7341, the correct form to use for the ``url`` is as follows::
http://localhost:7341/workspace0
"""
self._pending_operations = []
self._autoflush_threshold = 1024
self.url = url or self._construct_default_url(host, port, workspace)
def __del__(self):
try:
self.close()
except urllib.error.URLError:
# Happens when Gephi is closed before the connection is destroyed
pass
def _construct_default_url(self, host, port, workspace):
return "http://%s:%d/workspace%d" % (host, port, workspace)
def close(self):
"""Flushes all the pending operations to the Gephi master server in a
single request."""
self.flush()
def flush(self):
"""Flushes all the pending operations to the Gephi master server in a
single request."""
data = b"".join(self._pending_operations)
self._pending_operations = []
conn = urllib.request.urlopen(self._update_url, data=data)
return conn.read()
@property
def url(self):
"""The URL of the Gephi workspace where the data will be sent."""
return self._url_root
@url.setter
def url(self, value):
self._url_root = value
self._get_url = self._url_root + "?operation=getGraph"
self._update_url = self._url_root + "?operation=updateGraph"
def write(self, data):
"""Sends the given raw data to the Gephi streaming master server in an HTTP
POST request."""
self._pending_operations.append(data)
if len(self._pending_operations) >= self._autoflush_threshold:
self.flush()
def __repr__(self):
return "%s(url=%r)" % (self.__class__.__name__, self.url)
class GephiGraphStreamingAPIFormat:
"""Object that implements the Gephi graph streaming API format and returns
Python objects corresponding to the events defined in the API.
"""
def get_add_node_event(self, identifier, attributes=None):
"""Generates a Python object corresponding to the event that adds a node
with the given identifier and attributes in the Gephi graph streaming API.
Example::
>>> api = GephiGraphStreamingAPIFormat()
>>> api.get_add_node_event("spam")
{'an': {'spam': {}}}
>>> api.get_add_node_event("spam", dict(ham="eggs"))
{'an': {'spam': {'ham': 'eggs'}}}
"""
return {"an": {identifier: attributes if attributes is not None else {}}}
def get_add_edge_event(self, identifier, source, target, directed, attributes=None):
"""Generates a Python object corresponding to the event that adds an edge
with the given source, target, directednessr and attributes in the Gephi
graph streaming API.
"""
result = dict(attributes if attributes is not None else {})
result["source"] = source
result["target"] = target
result["directed"] = bool(directed)
return {"ae": {identifier: result}}
def get_change_node_event(self, identifier, attributes):
"""Generates a Python object corresponding to the event that changes the
attributes of some node in the Gephi graph streaming API. The given attributes
are merged into the existing ones; use C{None} as the attribute value to
delete a given attribute.
Example::
>>> api = GephiGraphStreamingAPIFormat()
>>> api.get_change_node_event("spam", dict(ham="eggs"))
{'cn': {'spam': {'ham': 'eggs'}}}
>>> api.get_change_node_event("spam", dict(ham=None))
{'cn': {'spam': {'ham': None}}}
"""
return {"cn": {identifier: attributes}}
def get_change_edge_event(self, identifier, attributes):
"""Generates a Python object corresponding to the event that changes the
attributes of some edge in the Gephi graph streaming API. The given attributes
are merged into the existing ones; use C{None} as the attribute value to
delete a given attribute.
Example::
>>> api = GephiGraphStreamingAPIFormat()
>>> api.get_change_edge_event("spam", dict(ham="eggs"))
{'ce': {'spam': {'ham': 'eggs'}}}
>>> api.get_change_edge_event("spam", dict(ham=None))
{'ce': {'spam': {'ham': None}}}
"""
return {"ce": {identifier: attributes}}
def get_delete_node_event(self, identifier):
"""Generates a Python object corresponding to the event that deletes a
node with the given identifier in the Gephi graph streaming API.
Example::
>>> api = GephiGraphStreamingAPIFormat()
>>> api.get_delete_node_event("spam")
{'dn': {'spam': {}}}
"""
return {"dn": {identifier: {}}}
def get_delete_edge_event(self, identifier):
"""Generates a Python object corresponding to the event that deletes an
edge with the given identifier in the Gephi graph streaming API.
Example::
>>> api = GephiGraphStreamingAPIFormat()
>>> api.get_delete_edge_event("spam:ham")
{'de': {'spam:ham': {}}}
"""
return {"de": {identifier: {}}}
class GephiGraphStreamer:
"""Class that produces JSON event objects that stream an igraph graph to
Gephi using the Gephi Graph Streaming API.
The Gephi graph streaming format is a simple JSON-based format that can be used
to post mutations to a graph (i.e. node and edge additions, removals and updates)
to a remote component. For instance, one can open up Gephi (http://www.gephi.org),
install the Gephi graph streaming plugin and then send a graph from igraph
straight into the Gephi window by using `GephiGraphStreamer` with the
appropriate URL where Gephi is listening.
Example::
>>> from cStringIO import StringIO
>>> from igraph import Graph
>>> buf = StringIO()
>>> streamer = GephiGraphStreamer()
>>> graph = Graph.Formula("A --> B, B --> C")
>>> streamer.post(graph, buf)
>>> print(buf.getvalue()) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
{"an": {"igraph:...:v:0": {"name": "A"}}}
{"an": {"igraph:...:v:1": {"name": "B"}}}
{"an": {"igraph:...:v:2": {"name": "C"}}}
{"ae": {"igraph:...:e:0:1": {...}}}
{"ae": {"igraph:...:e:1:2": {...}}}
<BLANKLINE>
"""
def __init__(self, encoder=None):
"""Constructs a Gephi graph streamer that will post graphs to a
given file-like object or a Gephi connection.
``encoder`` must either be ``None`` or an instance of ``json.JSONEncoder``
and it must contain the JSON encoder to be used when posting JSON objects.
"""
self.encoder = encoder or json.JSONEncoder(ensure_ascii=True)
self.format = GephiGraphStreamingAPIFormat()
def iterjsonobj(self, graph):
"""Iterates over the JSON objects that build up the graph using the
Gephi graph streaming API. The objects returned from this function are
Python objects; they must be formatted with ``json.dumps`` before
sending them to the destination.
"""
# Construct a unique ID prefix
id_prefix = "igraph:%s" % (hex(id(graph)),)
# Add the vertices
add_node = self.format.get_add_node_event
for vertex in graph.vs:
yield add_node("%s:v:%d" % (id_prefix, vertex.index), vertex.attributes())
# Add the edges
add_edge = self.format.get_add_edge_event
directed = graph.is_directed()
for edge in graph.es:
yield add_edge(
"%s:e:%d:%d" % (id_prefix, edge.source, edge.target),
"%s:v:%d" % (id_prefix, edge.source),
"%s:v:%d" % (id_prefix, edge.target),
directed,
edge.attributes(),
)
def post(self, graph, destination, encoder=None):
"""Posts the given graph to the destination of the streamer using the
given JSON encoder. When ``encoder`` is ``None``, it falls back to the
default JSON encoder of the streamer in the `encoder` property.
``destination`` must be a file-like object or an instance of
`GephiConnection`.
"""
encoder = encoder or self.encoder
for jsonobj in self.iterjsonobj(graph):
self.send_event(jsonobj, destination, encoder=encoder, flush=False)
destination.flush()
def send_event(self, event, destination, encoder=None, flush=True):
"""Sends a single JSON event to the given destination using the given
JSON encoder. When ``encoder`` is ``None``, it falls back to the
default JSON encoder of the streamer in the `encoder` property.
``destination`` must be a file-like object or an instance of
`GephiConnection`.
The method flushes the destination after sending the event. If you want
to avoid this (e.g., because you are sending many events), set
``flush`` to ``False``.
"""
encoder = encoder or self.encoder
destination.write(encoder.encode(event).encode("utf-8"))
destination.write(b"\r\n")
if flush:
destination.flush()