| 1 |
|
|---|
| 2 | class IsoldeRESTClient:
|
|---|
| 3 | '''
|
|---|
| 4 | Client-side code to interface to ISOLDE's REST API for remote control from
|
|---|
| 5 | another program. NOTE: this uses a non-secure HTTP connection which a
|
|---|
| 6 | malicious user could in theory use to run arbitrary code on your machine.
|
|---|
| 7 | Should be used only on your local machine or a local area network, protected
|
|---|
| 8 | by a firewall.
|
|---|
| 9 |
|
|---|
| 10 | On running the connect() function, the client instance will be populated
|
|---|
| 11 | with methods based on those defined on the server. A dict giving the
|
|---|
| 12 | available method names, required/optional arguments and docstrings can be
|
|---|
| 13 | obtained with get_available_methods().
|
|---|
| 14 | '''
|
|---|
| 15 | def __init__(self, address, port, timeout=20):
|
|---|
| 16 | self._address = address
|
|---|
| 17 | self._port = port
|
|---|
| 18 | self._timeout = timeout
|
|---|
| 19 | self._headers = {'Content-type': 'application/json'}
|
|---|
| 20 |
|
|---|
| 21 | def connect(self):
|
|---|
| 22 | import http.client
|
|---|
| 23 | self._connection = http.client.HTTPConnection('{}:{}'.format(self._address, self._port), timeout=self._timeout)
|
|---|
| 24 | self.get_available_methods()
|
|---|
| 25 |
|
|---|
| 26 | def _get_available_methods(self):
|
|---|
| 27 | if not self.connected:
|
|---|
| 28 | raise RuntimeError('Not connected to server!')
|
|---|
| 29 | self._connection.request('GET', '')
|
|---|
| 30 | method_dict = self._get_result()
|
|---|
| 31 | return method_dict
|
|---|
| 32 |
|
|---|
| 33 | def get_available_methods(self):
|
|---|
| 34 | method_dict = self._get_available_methods()
|
|---|
| 35 | self._method_factory(self._get_available_methods())
|
|---|
| 36 | return method_dict
|
|---|
| 37 |
|
|---|
| 38 | def _method_factory(self, method_dict):
|
|---|
| 39 | cls = self.__class__
|
|---|
| 40 | for fname, func_def in method_dict.items():
|
|---|
| 41 | if not hasattr(cls, fname):
|
|---|
| 42 | args = func_def.get('args', [])
|
|---|
| 43 | kwargs = func_def.get('kwargs', {})
|
|---|
| 44 | f_str = '''
|
|---|
| 45 | def server_method(self, {}):
|
|---|
| 46 | """
|
|---|
| 47 | {}
|
|---|
| 48 | """
|
|---|
| 49 | send_dict = dict()
|
|---|
| 50 | args = [{}]
|
|---|
| 51 | kwargs = {}
|
|---|
| 52 |
|
|---|
| 53 | send_dict['cmd'] = "{}"
|
|---|
| 54 | send_dict['args'] = args
|
|---|
| 55 | send_dict['kwargs'] = kwargs
|
|---|
| 56 |
|
|---|
| 57 | import json
|
|---|
| 58 | self._connection.request('POST', '', json.dumps(send_dict).encode('utf-8'), self._headers)
|
|---|
| 59 | result = self._get_result()
|
|---|
| 60 | if 'error' in result.keys():
|
|---|
| 61 | raise RuntimeError(result['error'] + '; Server traceback: \\n' + result.get('traceback', 'None provided'))
|
|---|
| 62 | return result
|
|---|
| 63 | '''.format(
|
|---|
| 64 | ', '.join((
|
|---|
| 65 | ', '.join("{}:'{}'".format(arg, argtype['type']) for arg, argtype in args.items()),
|
|---|
| 66 | ', '.join(["{}:'{}'={}".format(kw, val['type'], val['default']) for kw, val in kwargs.items()])
|
|---|
| 67 | )),
|
|---|
| 68 | func_def['docstring'],
|
|---|
| 69 | ', '.join(args),
|
|---|
| 70 | "dict( [{}] )".format(', '.join('("{}", {})'.format(kw, kw) for kw in kwargs.keys())),
|
|---|
| 71 | fname,
|
|---|
| 72 | )
|
|---|
| 73 | # print(f_str)
|
|---|
| 74 | exec(f_str, globals())
|
|---|
| 75 | setattr(cls, fname, server_method)
|
|---|
| 76 |
|
|---|
| 77 |
|
|---|
| 78 |
|
|---|
| 79 | @property
|
|---|
| 80 | def connected(self):
|
|---|
| 81 | return (hasattr(self, '_connection') and self._connection is not None)
|
|---|
| 82 | @property
|
|---|
| 83 | def server_address(self):
|
|---|
| 84 | return self._address
|
|---|
| 85 |
|
|---|
| 86 | @property
|
|---|
| 87 | def server_port(self):
|
|---|
| 88 | return self._port
|
|---|
| 89 |
|
|---|
| 90 | def _get_result(self):
|
|---|
| 91 | from cgi import parse_header
|
|---|
| 92 | result = self._connection.getresponse()
|
|---|
| 93 | ctype, pdict = parse_header(result.headers.get('content-type'))
|
|---|
| 94 | if ctype != 'application/json':
|
|---|
| 95 | err_string = ('Server returned a non-supported type, "{}". Only "{}" '
|
|---|
| 96 | 'is allowed.').format(ctype, 'application/json')
|
|---|
| 97 | raise TypeError(err_string)
|
|---|
| 98 | import json
|
|---|
| 99 | return json.loads(result.read().decode('utf-8'))
|
|---|
| 100 |
|
|---|
| 101 | if __name__=='__main__':
|
|---|
| 102 | ic = IsoldeRESTClient('localhost', '12365')
|
|---|
| 103 | ic.connect()
|
|---|
| 104 | print(ic.run(['open 1a0m structureFactors true']))
|
|---|