motifapi 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- motifapi/__init__.py +5 -0
- motifapi/api.py +328 -0
- motifapi/schedule.py +2 -0
- motifapi/server.crt +19 -0
- motifapi/stream.py +100 -0
- motifapi/util.py +16 -0
- motifapi-0.2.0.dist-info/METADATA +19 -0
- motifapi-0.2.0.dist-info/RECORD +10 -0
- motifapi-0.2.0.dist-info/WHEEL +5 -0
- motifapi-0.2.0.dist-info/top_level.txt +1 -0
motifapi/__init__.py
ADDED
motifapi/api.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
import ssl
|
|
4
|
+
import socket
|
|
5
|
+
import os.path
|
|
6
|
+
import subprocess
|
|
7
|
+
import logging
|
|
8
|
+
import urllib.request
|
|
9
|
+
import urllib.error
|
|
10
|
+
import http.client
|
|
11
|
+
|
|
12
|
+
DEFAULT_HTTP_TIMEOUT = 10 # seconds
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# http://code.activestate.com/recipes/577548-https-httplib-client-connection-with-certificate-v/
|
|
16
|
+
# http://stackoverflow.com/questions/1875052/using-paired-certificates-with-urllib2
|
|
17
|
+
|
|
18
|
+
class HTTPSClientAuthHandler(urllib.request.HTTPSHandler):
|
|
19
|
+
"""
|
|
20
|
+
Allows sending a client certificate with the HTTPS connection.
|
|
21
|
+
This version also validates the peer (server) certificate.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, key=None, cert=None, ca_certs=None):
|
|
25
|
+
self.ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
26
|
+
self.ctx.check_hostname = False
|
|
27
|
+
if ca_certs:
|
|
28
|
+
self.ctx.verify_mode = ssl.CERT_REQUIRED
|
|
29
|
+
self.ctx.load_verify_locations(ca_certs)
|
|
30
|
+
else:
|
|
31
|
+
self.ctx.verify_mode = ssl.CERT_NONE
|
|
32
|
+
if cert:
|
|
33
|
+
self.ctx.load_cert_chain(cert, key)
|
|
34
|
+
urllib.request.HTTPSHandler.__init__(self, context=self.ctx)
|
|
35
|
+
|
|
36
|
+
def https_open(self, req):
|
|
37
|
+
return self.do_open(self.get_connection, req)
|
|
38
|
+
|
|
39
|
+
def get_connection(self, host, timeout=DEFAULT_HTTP_TIMEOUT):
|
|
40
|
+
return HTTPSConnection(host, timeout=timeout, context=self.ctx)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HTTPSConnection(http.client.HTTPSConnection):
|
|
44
|
+
"""
|
|
45
|
+
Overridden to allow peer certificate validation using an SSLContext.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, host, **kwargs):
|
|
49
|
+
self.ctx = kwargs.pop('context', None)
|
|
50
|
+
http.client.HTTPSConnection.__init__(self, host, **kwargs)
|
|
51
|
+
|
|
52
|
+
def connect(self):
|
|
53
|
+
sock = socket.create_connection((self.host, self.port), self.timeout)
|
|
54
|
+
|
|
55
|
+
if self._tunnel_host:
|
|
56
|
+
self.sock = sock
|
|
57
|
+
self._tunnel()
|
|
58
|
+
|
|
59
|
+
if self.ctx:
|
|
60
|
+
self.sock = self.ctx.wrap_socket(sock, server_hostname=self.host)
|
|
61
|
+
else:
|
|
62
|
+
self.sock = sock
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MethodRequest(urllib.request.Request):
|
|
66
|
+
# See: https://gist.github.com/logic/2715756
|
|
67
|
+
|
|
68
|
+
def __init__(self, *args, **kwargs):
|
|
69
|
+
if 'method' in kwargs:
|
|
70
|
+
self._method = kwargs['method']
|
|
71
|
+
del kwargs['method']
|
|
72
|
+
else:
|
|
73
|
+
self._method = None
|
|
74
|
+
return urllib.request.Request.__init__(self, *args, **kwargs)
|
|
75
|
+
|
|
76
|
+
def get_method(self, *args, **kwargs):
|
|
77
|
+
return self._method if self._method is not None else urllib.request.Request.get_method(self, *args, **kwargs)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MotifError(Exception):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MotifApiError(MotifError):
|
|
85
|
+
def __init__(self, message, code):
|
|
86
|
+
self.code = code
|
|
87
|
+
Exception.__init__(self, message)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MotifApi(object):
|
|
91
|
+
|
|
92
|
+
STREAM_TYPE_IMAGE = 1
|
|
93
|
+
STREAM_TYPE_STATE = 2
|
|
94
|
+
|
|
95
|
+
API = {r'version$': 'GET',
|
|
96
|
+
r'cameras$': 'GET',
|
|
97
|
+
r'cameras/configure$': 'PATCH',
|
|
98
|
+
r'cameras/read/(?P<name>[^\s /]+)$': 'GET',
|
|
99
|
+
r'camera/(?P<serial>[^\s /]+)$': 'GET',
|
|
100
|
+
r'camera/(?P<serial>[^\s /]+)/configure$': 'PATCH',
|
|
101
|
+
r'camera/(?P<serial>[^\s /]+)/read/(?P<name>[^\s /]+)$': 'GET',
|
|
102
|
+
r'camera/(?P<serial>[^\s /]+)/recording/start$': 'POST',
|
|
103
|
+
r'camera/(?P<serial>[^\s /]+)/recording/stop$': 'POST',
|
|
104
|
+
r'camera/(?P<serial>[^\s /]+)/recordings$': 'GET',
|
|
105
|
+
r'camera/(?P<serial>[^\s /]+)/recordings/copy_all$': 'POST',
|
|
106
|
+
r'camera/(?P<serial>[^\s /]+)/recordings/export_all$': 'POST',
|
|
107
|
+
r'camera/(?P<serial>[^\s /]+)/io/(?P<name>[^\s /]+)/set': 'POST',
|
|
108
|
+
r'camera/(?P<serial>[^\s /]+)/io/log': 'POST',
|
|
109
|
+
r'camera/(?P<serial>[^\s /]+)/io/read': 'GET',
|
|
110
|
+
r'recording/start$': 'POST',
|
|
111
|
+
r'recording/stop$': 'POST',
|
|
112
|
+
r'recordings$': 'GET',
|
|
113
|
+
r'recordings/copy_all$': 'POST',
|
|
114
|
+
r'recordings/export_all$': 'POST',
|
|
115
|
+
r'schedule$': 'GET',
|
|
116
|
+
r'schedule/clear$': 'POST',
|
|
117
|
+
r'schedule/(?P<identifier>[^\s /]+)/clear$': 'DELETE',
|
|
118
|
+
r'schedule/recording/start$': 'POST',
|
|
119
|
+
r'schedule/camera/(?P<serial>[^\s /]+)/recording/start$': 'POST',
|
|
120
|
+
r'schedule/recordings/copy_all': 'POST',
|
|
121
|
+
r'schedule/camera/(?P<serial>[^\s /]+)/recordings/copy_all$': 'POST',
|
|
122
|
+
r'schedule/recordings/export_all': 'POST',
|
|
123
|
+
r'schedule/camera/(?P<serial>[^\s /]+)/recordings/export_all$': 'POST',
|
|
124
|
+
r'schedule/io/(?P<name>[^\s /]+)/set': 'POST',
|
|
125
|
+
r'schedule/camera/(?P<serial>[^\s /]+)/io/(?P<name>[^\s /]+)/set': 'POST',
|
|
126
|
+
r'schedule/cameras/configure/(?P<name>[^\s /]+)': 'POST',
|
|
127
|
+
r'schedule/camera/(?P<serial>[^\s /]+)/configure/(?P<name>[^\s /]+)': 'POST',
|
|
128
|
+
r'io/(?P<io_serial>[^\s /]+)/(?P<io_port>[^\s /]+)/set': 'POST',
|
|
129
|
+
r'io/(?P<name>[^\s /]+)/set': 'POST',
|
|
130
|
+
r'io/log': 'POST',
|
|
131
|
+
r'io/read': 'GET',
|
|
132
|
+
r'camera/(?P<serial>[^\s /]+)/experiment/message$': 'POST',
|
|
133
|
+
r'experiment/message$': 'POST',
|
|
134
|
+
r'multicam/synchronize': 'POST',
|
|
135
|
+
r'multicam/connect_camera/(?P<serial>[^\s /]+)': 'POST',
|
|
136
|
+
r'multicam/disconnect_camera/(?P<serial>[^\s /]+)': 'POST',
|
|
137
|
+
r'multicam/connect_all': 'POST',
|
|
138
|
+
r'multicam/disconnect_all': 'POST',
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def __init__(self, host=None, api_key=None, port=None, ca_cert=None, api_version=1):
|
|
142
|
+
self._log = logging.getLogger('motifapi')
|
|
143
|
+
|
|
144
|
+
if port is None:
|
|
145
|
+
try:
|
|
146
|
+
port = int(os.environ.get('MOTIF_PORT', 6083))
|
|
147
|
+
except:
|
|
148
|
+
port = 6083
|
|
149
|
+
else:
|
|
150
|
+
port = int(port)
|
|
151
|
+
|
|
152
|
+
if host is None:
|
|
153
|
+
try:
|
|
154
|
+
host = os.environ['MOTIF_HOST']
|
|
155
|
+
self._log.debug('took host from environment')
|
|
156
|
+
except KeyError:
|
|
157
|
+
pass
|
|
158
|
+
if not host:
|
|
159
|
+
host = '127.0.0.1'
|
|
160
|
+
|
|
161
|
+
if api_key is None:
|
|
162
|
+
try:
|
|
163
|
+
api_key = os.environ['MOTIF_API_KEY']
|
|
164
|
+
self._log.debug('took api_key from environment')
|
|
165
|
+
except KeyError:
|
|
166
|
+
pass
|
|
167
|
+
if not api_key:
|
|
168
|
+
try:
|
|
169
|
+
api_key = subprocess.check_output(['recnode-apikey']).strip()
|
|
170
|
+
self._log.debug('took api-key from recnode-apikey subprocess')
|
|
171
|
+
except OSError:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
if not api_key:
|
|
175
|
+
raise ValueError('API key must be specified')
|
|
176
|
+
|
|
177
|
+
if ca_cert is None:
|
|
178
|
+
ca_cert = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'server.crt')
|
|
179
|
+
|
|
180
|
+
if not os.path.exists(ca_cert):
|
|
181
|
+
raise ValueError('could not find certificate: %s' % ca_cert)
|
|
182
|
+
|
|
183
|
+
self._prefix = 'api/%d/' % api_version
|
|
184
|
+
|
|
185
|
+
client_cert_key = None
|
|
186
|
+
client_cert_pem = None # file path
|
|
187
|
+
handler = HTTPSClientAuthHandler(
|
|
188
|
+
key=client_cert_key,
|
|
189
|
+
cert=client_cert_pem,
|
|
190
|
+
ca_certs=ca_cert)
|
|
191
|
+
self._http = urllib.request.build_opener(handler)
|
|
192
|
+
|
|
193
|
+
self._host = host
|
|
194
|
+
self._api_key = api_key
|
|
195
|
+
self._port = port
|
|
196
|
+
|
|
197
|
+
def _build_request(self, endpoint, data=None, method='GET'):
|
|
198
|
+
if endpoint[0] == '/':
|
|
199
|
+
endpoint = endpoint[1:]
|
|
200
|
+
|
|
201
|
+
if endpoint != 'version':
|
|
202
|
+
endpoint = self._prefix + endpoint
|
|
203
|
+
|
|
204
|
+
if data is not None:
|
|
205
|
+
try:
|
|
206
|
+
data = json.dumps(data).encode("utf-8")
|
|
207
|
+
except TypeError:
|
|
208
|
+
raise ValueError('Arguments must be JSON serializable (they were %r)' % (data,))
|
|
209
|
+
|
|
210
|
+
url = 'https://%s:%s/%s' % (self._host, self._port, endpoint)
|
|
211
|
+
|
|
212
|
+
req = MethodRequest(url, data, method=method)
|
|
213
|
+
req.add_header('X-Api-Key', self._api_key)
|
|
214
|
+
req.add_header('Content-Type', 'application/json')
|
|
215
|
+
|
|
216
|
+
self._log.debug('%s %s (%d bytes %s)' % (method,
|
|
217
|
+
url,
|
|
218
|
+
0 if data is None else len(data),
|
|
219
|
+
type(data)))
|
|
220
|
+
|
|
221
|
+
return req
|
|
222
|
+
|
|
223
|
+
def _call(self, req):
|
|
224
|
+
try:
|
|
225
|
+
resp = self._http.open(req)
|
|
226
|
+
data = resp.read()
|
|
227
|
+
resp.close()
|
|
228
|
+
return data
|
|
229
|
+
except urllib.error.HTTPError as e:
|
|
230
|
+
try:
|
|
231
|
+
raw = e.read()
|
|
232
|
+
err = json.loads(raw.decode('utf-8'))
|
|
233
|
+
exc = MotifApiError(err['error'], err['status_code'])
|
|
234
|
+
except Exception:
|
|
235
|
+
raise ValueError('unknown API error')
|
|
236
|
+
raise exc
|
|
237
|
+
except urllib.error.URLError:
|
|
238
|
+
raise MotifError('motif not running or reachable')
|
|
239
|
+
|
|
240
|
+
def call(self, endpoint, method=None, data=None, **kwargs):
|
|
241
|
+
meth = ep = None
|
|
242
|
+
for _ep, _meth in self.API.items():
|
|
243
|
+
if re.match(_ep, endpoint):
|
|
244
|
+
meth = _meth
|
|
245
|
+
ep = _ep
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
if meth is None:
|
|
249
|
+
raise ValueError("unknown endpoint '%s' (are you missing/adding '/')" % endpoint)
|
|
250
|
+
|
|
251
|
+
req = self._build_request(endpoint,
|
|
252
|
+
data=kwargs or None,
|
|
253
|
+
method=meth)
|
|
254
|
+
out = self._call(req)
|
|
255
|
+
if out:
|
|
256
|
+
try:
|
|
257
|
+
return json.loads(out.decode('utf-8'))
|
|
258
|
+
except ValueError as e:
|
|
259
|
+
raise ValueError('Invalid JSON response: %s' % e.message)
|
|
260
|
+
else:
|
|
261
|
+
return {}
|
|
262
|
+
|
|
263
|
+
def is_recording(self, serial):
|
|
264
|
+
r = self.call('camera/%s' % serial)
|
|
265
|
+
if r['camera_info'].get('filename'):
|
|
266
|
+
return True
|
|
267
|
+
elif r['camera_info'].get('status') == 'pending':
|
|
268
|
+
return True
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
def is_copying(self, serial):
|
|
272
|
+
r = self.call('camera/%s' % serial)
|
|
273
|
+
return r['playback_info']['status'] == 'copying'
|
|
274
|
+
|
|
275
|
+
def is_exporting(self, serial):
|
|
276
|
+
r = self.call('camera/%s' % serial)
|
|
277
|
+
status = r['playback_info']['status']
|
|
278
|
+
return status.startswith('export') and ('finished' not in status)
|
|
279
|
+
|
|
280
|
+
def send_message(self, message, serial=None):
|
|
281
|
+
if serial:
|
|
282
|
+
return self.call('camera/%s/experiment/message' % serial, message=message)
|
|
283
|
+
return self.call('experiment/message', message=message)
|
|
284
|
+
|
|
285
|
+
def get_stream(self, serial=None, stream_type=STREAM_TYPE_IMAGE, force_host=None):
|
|
286
|
+
from .stream import ImageStreamer, StateStreamer
|
|
287
|
+
|
|
288
|
+
if serial is None:
|
|
289
|
+
try:
|
|
290
|
+
# get the first camera
|
|
291
|
+
for c in self.call('cameras').get('cameras', []):
|
|
292
|
+
serial = c['serial']
|
|
293
|
+
break
|
|
294
|
+
except (urllib.error.URLError, MotifApiError):
|
|
295
|
+
raise MotifError('motif not running or reachable')
|
|
296
|
+
|
|
297
|
+
if serial is None:
|
|
298
|
+
raise MotifError('no cameras connected or running')
|
|
299
|
+
|
|
300
|
+
def _get_host_port(_stream_name):
|
|
301
|
+
_stat = self.call('camera/%s' % serial)
|
|
302
|
+
_host = _stat['camera_info']['stream'][_stream_name]['host']
|
|
303
|
+
_port = int(_stat['camera_info']['stream'][_stream_name]['port'])
|
|
304
|
+
|
|
305
|
+
if force_host is not None:
|
|
306
|
+
_host = force_host
|
|
307
|
+
elif (_host == '0.0.0.0') and (self._host != '0.0.0.0'):
|
|
308
|
+
_host = self._host
|
|
309
|
+
|
|
310
|
+
return _host, _port
|
|
311
|
+
|
|
312
|
+
if stream_type in (MotifApi.STREAM_TYPE_IMAGE, MotifApi.STREAM_TYPE_STATE):
|
|
313
|
+
try:
|
|
314
|
+
if stream_type == MotifApi.STREAM_TYPE_IMAGE:
|
|
315
|
+
host, port = _get_host_port('image')
|
|
316
|
+
return ImageStreamer(host, port)
|
|
317
|
+
else:
|
|
318
|
+
host, port = _get_host_port('state')
|
|
319
|
+
return StateStreamer(host, port)
|
|
320
|
+
except (urllib.error.URLError, MotifApiError):
|
|
321
|
+
raise MotifError('camera with serial %s not found or running' % serial)
|
|
322
|
+
except KeyError:
|
|
323
|
+
raise MotifError('realtime stream not enabled on camera')
|
|
324
|
+
else:
|
|
325
|
+
raise ValueError('unknown stream type')
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
motifapi/schedule.py
ADDED
motifapi/server.crt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDGzCCAgOgAwIBAgIJAOjl0YTg0v1KMA0GCSqGSIb3DQEBCwUAMCQxCzAJBgNV
|
|
3
|
+
BAYTAkFVMRUwEwYDVQQKDAxsb29wYmlvIGdtYmgwHhcNMTcxMDI4MTM1MDQzWhcN
|
|
4
|
+
MjcxMDI2MTM1MDQzWjAkMQswCQYDVQQGEwJBVTEVMBMGA1UECgwMbG9vcGJpbyBn
|
|
5
|
+
bWJoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuBDT2rvtlC3z330H
|
|
6
|
+
p5Zl1ilA4gvSM7Zlvpwt+kQETb7rCS49+oPaAa89tDEbwrk3rDzxnGG2+YL73h07
|
|
7
|
+
YpTlJk/7WkciGgR7K83CwEiSVUUZsmxfXH/t0V5km4d+Ul7lRWoRI9BcnWu9Mvwr
|
|
8
|
+
572NQJf0cWs0FJu5qYmY0nI7+EjN/qB7OFYYTYSJZQYwoRZzdGFNWCiTkxTBOQc/
|
|
9
|
+
1dyPzmtMbRQONYryMqMHKLUeG4aEYwCsm2XNSTHn7JCxc/dHc8BnBMkAOqNp+XP+
|
|
10
|
+
n4blQ6e4C4e4MWEk/yrv0p8u0m6NIoBvI/nZDhgvdIQTFrj+eIOSCPCtxzAQEnDd
|
|
11
|
+
f9FEiQIDAQABo1AwTjAdBgNVHQ4EFgQU1kn+nli9zgqxumR2zgMFS5WljhgwHwYD
|
|
12
|
+
VR0jBBgwFoAU1kn+nli9zgqxumR2zgMFS5WljhgwDAYDVR0TBAUwAwEB/zANBgkq
|
|
13
|
+
hkiG9w0BAQsFAAOCAQEAdr76akM25qXFURAgWszcjJAaaimp9RRUaXwO5UxVegCR
|
|
14
|
+
x1FCZyfm/TIWDgfKjlPFiPgCS3WBL0gLWummZhJ5YFwd0lI34H9KXl9JgKidyNrH
|
|
15
|
+
aDHBj4On0eu84f6nMxqFPFDDscJLlMvmfBYRFq/7QryU/LuWeSWhn/oQs4vakoRH
|
|
16
|
+
aJxzot67YvH9N8qxX/6Nc2TeEAz4X5AyEYe46mAc99rRROWjnYD39dfmWzv/5Cge
|
|
17
|
+
42YwNxtba8njGyPnCb+/kKerc+jXNBoopBGECBsMyGx3p5cVCnz//NhgvWmvquEU
|
|
18
|
+
msK/FxvWDIB5Y/KEfeaghB4D4hgy3IF9Z6T9ZBsLsg==
|
|
19
|
+
-----END CERTIFICATE-----
|
motifapi/stream.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import threading
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
import zmq
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if sys.version_info > (3,):
|
|
11
|
+
buffer = memoryview
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
LOG = logging.getLogger('motifapi.stream')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def recv_array(socket, flags=0, copy=True, track=False):
|
|
18
|
+
md = socket.recv_json(flags=flags)
|
|
19
|
+
msg = socket.recv(flags=flags, copy=copy, track=track)
|
|
20
|
+
# note: I don't actually think this is necessary, in PY2 this is type<str> and np.frombuffer(str) does
|
|
21
|
+
# the right thing, in PY3 this is type<bytes> which supports buffer protocol
|
|
22
|
+
buf = buffer(msg)
|
|
23
|
+
A = np.frombuffer(buf, dtype=md.pop('dtype'))
|
|
24
|
+
return A.reshape(md.pop('shape')), md
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ImageStreamer(object):
|
|
28
|
+
|
|
29
|
+
stream = None
|
|
30
|
+
|
|
31
|
+
def __init__(self, host, port):
|
|
32
|
+
ctx = zmq.Context()
|
|
33
|
+
address = "tcp://%s:%d" % (host, port)
|
|
34
|
+
LOG.debug('image stream connecting to: %s' % address)
|
|
35
|
+
sock = ctx.socket(zmq.PULL)
|
|
36
|
+
sock.connect(address)
|
|
37
|
+
self.stream = sock
|
|
38
|
+
|
|
39
|
+
def get_next_image(self, block=True, copy=True):
|
|
40
|
+
while True:
|
|
41
|
+
while self.stream.poll(0, zmq.POLLIN):
|
|
42
|
+
return recv_array(self.stream, copy=copy)
|
|
43
|
+
if not block:
|
|
44
|
+
return None, None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StateStreamer(object):
|
|
48
|
+
|
|
49
|
+
def __init__(self, host, port, channel='j'):
|
|
50
|
+
ctx = zmq.Context()
|
|
51
|
+
address = "tcp://%s:%d" % (host, port)
|
|
52
|
+
LOG.debug('state stream connecting to: %s' % address)
|
|
53
|
+
sock = ctx.socket(zmq.SUB)
|
|
54
|
+
sock.connect(address)
|
|
55
|
+
sock.setsockopt(zmq.LINGER, 0)
|
|
56
|
+
if sys.version_info > (3,):
|
|
57
|
+
sock.setsockopt_string(zmq.SUBSCRIBE, channel)
|
|
58
|
+
else:
|
|
59
|
+
sock.setsockopt(zmq.SUBSCRIBE, channel)
|
|
60
|
+
self.stream = sock
|
|
61
|
+
|
|
62
|
+
def get_next_state(self, block=True):
|
|
63
|
+
while True:
|
|
64
|
+
_, msg = self.stream.recv_multipart()
|
|
65
|
+
return json.loads(msg)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class StateMirror(threading.Thread):
|
|
69
|
+
|
|
70
|
+
daemon = True
|
|
71
|
+
|
|
72
|
+
def __init__(self, streamer):
|
|
73
|
+
assert isinstance(streamer, StateStreamer)
|
|
74
|
+
threading.Thread.__init__(self)
|
|
75
|
+
self._lock = threading.Lock()
|
|
76
|
+
self._state = {}
|
|
77
|
+
self._streamer = streamer
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_recording(self):
|
|
81
|
+
return self._state.get('is_recording', False)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def frame_number(self):
|
|
85
|
+
return self._state.get('frame_number', None)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def frame_time(self):
|
|
89
|
+
return self._state.get('frame_time', None)
|
|
90
|
+
|
|
91
|
+
def get_state(self, **extra):
|
|
92
|
+
with self._lock:
|
|
93
|
+
extra.update(self._state)
|
|
94
|
+
return extra
|
|
95
|
+
|
|
96
|
+
def run(self):
|
|
97
|
+
while True:
|
|
98
|
+
_, msg = self._streamer.stream.recv_multipart()
|
|
99
|
+
with self._lock:
|
|
100
|
+
self._state.update(json.loads(msg))
|
motifapi/util.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_experiment_metadata():
|
|
6
|
+
try:
|
|
7
|
+
mdf = os.environ['MOTIF_METADATA_JSON_PATH']
|
|
8
|
+
with open(mdf, 'r') as f:
|
|
9
|
+
return json.load(f)
|
|
10
|
+
except KeyError:
|
|
11
|
+
# not defined
|
|
12
|
+
pass
|
|
13
|
+
except Exception as exc:
|
|
14
|
+
print("Error parsing metadata file", exc)
|
|
15
|
+
|
|
16
|
+
return {}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: motifapi
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python interface to Motif recording systems
|
|
5
|
+
Author: John Stowers
|
|
6
|
+
Author-email: john@loopbio.com
|
|
7
|
+
License: BSD
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: author-email
|
|
13
|
+
Dynamic: classifier
|
|
14
|
+
Dynamic: description
|
|
15
|
+
Dynamic: license
|
|
16
|
+
Dynamic: requires-python
|
|
17
|
+
Dynamic: summary
|
|
18
|
+
|
|
19
|
+
Python interface to Motif recording systems
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
motifapi/__init__.py,sha256=BJ_PWYb2B9QfBnMJuZW3O3COcc5vRN4Zk_gxkid7TUE,157
|
|
2
|
+
motifapi/api.py,sha256=zkv2T52_vGKsxWyHU6YkZyyxwUZX0hsGokfCL7Qbs4M,11923
|
|
3
|
+
motifapi/schedule.py,sha256=j6bhUfI69Xd-OsQ0FZgpDKxRn9Y4dxofAFLFe63q6lo,108
|
|
4
|
+
motifapi/server.crt,sha256=T93bqtfNR8z83vWhPNtlwyr9mKqJrlWDBDUU0Rgw_zM,1139
|
|
5
|
+
motifapi/stream.py,sha256=EkBbemtKJeQm9GdCcO2WWN8u6DNqCQznE9OpM43oY-A,2732
|
|
6
|
+
motifapi/util.py,sha256=BXxxqBN6ocC15IBZr7aDlpnVlKl-WSDp1jSERX-5vKw,333
|
|
7
|
+
motifapi-0.2.0.dist-info/METADATA,sha256=j__1XU-vKsfceEWRTTcMshLM4ep0U4XV9mOepjF_LUY,480
|
|
8
|
+
motifapi-0.2.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
9
|
+
motifapi-0.2.0.dist-info/top_level.txt,sha256=2fm6i3MFwhkeeVTSo3o6ISjkyA0s57V3zk_yJQKZlRw,9
|
|
10
|
+
motifapi-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
motifapi
|