-
-
Notifications
You must be signed in to change notification settings - Fork 473
/
rest.py
182 lines (152 loc) · 5.91 KB
/
rest.py
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
import json
import os
import pkg_resources
import tempfile
import traceback
from unittest.mock import MagicMock
from urllib.parse import parse_qs
import param
from runpy import run_path
from tornado import web
from tornado.wsgi import WSGIContainer
from .state import state
class HTTPError(web.HTTPError):
"""
Custom HTTPError type
"""
class BaseHandler(web.RequestHandler):
def write_error(self, status_code, **kwargs):
self.set_header('Content-Type', 'application/json')
if self.settings.get("serve_traceback") and "exc_info" in kwargs:
# in debug mode, try to send a traceback
lines = []
for line in traceback.format_exception(*kwargs["exc_info"]):
lines.append(line)
self.finish(json.dumps({
'error': {
'code': status_code,
'message': self._reason,
'traceback': lines,
}
}))
else:
self.finish(json.dumps({
'error': {
'code': status_code,
'message': self._reason,
}
}))
class ParamHandler(BaseHandler):
def __init__(self, app, request, **kwargs):
self.root = kwargs.pop('root', None)
super().__init__(app, request, **kwargs)
@classmethod
def serialize(cls, parameterized, parameters):
values = {p: getattr(parameterized, p) for p in parameters}
return parameterized.param.serialize_parameters(values)
@classmethod
def deserialize(cls, parameterized, parameters):
for p in parameters:
if p not in parameterized.param:
reason = f"'{p}' query parameter not recognized."
raise HTTPError(reason=reason, status_code=400)
return {p: parameterized.param.deserialize_value(p, v)
for p, v in parameters.items()}
async def get(self):
path = self.request.path
endpoint = path[path.index(self.root)+len(self.root):]
parameterized, parameters, _ = state._rest_endpoints.get(
endpoint, (None, None, None)
)
if not parameterized:
return
args = parse_qs(self.request.query)
params = self.deserialize(parameterized[0], args)
parameterized[0].param.set_param(**params)
self.set_header('Content-Type', 'application/json')
self.write(self.serialize(parameterized[0], parameters))
def build_tranquilize_application(files):
from tranquilizer.handler import ScriptHandler, NotebookHandler
from tranquilizer.main import make_app, UnsupportedFileType
functions = []
for filename in files:
extension = filename.split('.')[-1]
if extension == 'py':
source = ScriptHandler(filename)
elif extension == 'ipynb':
try:
import nbconvert # noqa
except ImportError as e: # pragma no cover
raise ImportError("Please install nbconvert to serve Jupyter Notebooks.") from e
source = NotebookHandler(filename)
else:
raise UnsupportedFileType('{} is not a script (.py) or notebook (.ipynb)'.format(filename))
functions.extend(source.tranquilized_functions)
return make_app(functions, 'Panel REST API', prefix='rest/')
def tranquilizer_rest_provider(files, endpoint):
"""
Returns a Tranquilizer based REST API. Builds the API by evaluating
the scripts and notebooks being served and finding all tranquilized
functions inside them.
Arguments
---------
files: list(str)
A list of paths being served
endpoint: str
The endpoint to serve the REST API on
Returns
-------
A Tornado routing pattern containing the route and handler
"""
app = build_tranquilize_application(files)
tr = WSGIContainer(app)
return [(r"^/%s/.*" % endpoint, web.FallbackHandler, dict(fallback=tr))]
def param_rest_provider(files, endpoint):
"""
Returns a Param based REST API given the scripts or notebooks
containing the tranquilized functions.
Arguments
---------
files: list(str)
A list of paths being served
endpoint: str
The endpoint to serve the REST API on
Returns
-------
A Tornado routing pattern containing the route and handler
"""
for filename in files:
extension = filename.split('.')[-1]
if extension == 'py':
try:
run_path(filename)
except Exception:
param.main.warning("Could not run app script on REST server startup.")
elif extension == 'ipynb':
try:
import nbconvert # noqa
except ImportError:
raise ImportError("Please install nbconvert to serve Jupyter Notebooks.")
from nbconvert import ScriptExporter
exporter = ScriptExporter()
source, _ = exporter.from_filename(filename)
source_dir = os.path.dirname(filename)
with tempfile.NamedTemporaryFile(mode='w', dir=source_dir, delete=True) as tmp:
tmp.write(source)
tmp.flush()
try:
run_path(tmp.name, init_globals={'get_ipython': MagicMock()})
except Exception:
param.main.warning("Could not run app notebook on REST server startup.")
else:
raise ValueError('{} is not a script (.py) or notebook (.ipynb)'.format(filename))
if endpoint and not endpoint.endswith('/'):
endpoint += '/'
return [((r"^/%s.*" % endpoint if endpoint else r"^.*"), ParamHandler, dict(root=endpoint))]
REST_PROVIDERS = {
'tranquilizer': tranquilizer_rest_provider,
'param': param_rest_provider
}
# Populate REST Providers from external extensions
for entry_point in pkg_resources.iter_entry_points('panel.io.rest'):
REST_PROVIDERS[entry_point.name] = entry_point.resolve()