1
|
6
|
import json
|
2
|
6
|
import os
|
3
|
6
|
import pkg_resources
|
4
|
6
|
import tempfile
|
5
|
6
|
import traceback
|
6
|
|
|
7
|
6
|
from unittest.mock import MagicMock
|
8
|
6
|
from urllib.parse import parse_qs
|
9
|
|
|
10
|
6
|
import param
|
11
|
|
|
12
|
6
|
from runpy import run_path
|
13
|
6
|
from tornado import web
|
14
|
6
|
from tornado.wsgi import WSGIContainer
|
15
|
|
|
16
|
6
|
from .state import state
|
17
|
|
|
18
|
|
|
19
|
6
|
class HTTPError(web.HTTPError):
|
20
|
|
"""
|
21
|
|
Custom HTTPError type
|
22
|
|
"""
|
23
|
|
|
24
|
|
|
25
|
6
|
class BaseHandler(web.RequestHandler):
|
26
|
|
|
27
|
6
|
def write_error(self, status_code, **kwargs):
|
28
|
0
|
self.set_header('Content-Type', 'application/json')
|
29
|
0
|
if self.settings.get("serve_traceback") and "exc_info" in kwargs:
|
30
|
|
# in debug mode, try to send a traceback
|
31
|
0
|
lines = []
|
32
|
0
|
for line in traceback.format_exception(*kwargs["exc_info"]):
|
33
|
0
|
lines.append(line)
|
34
|
0
|
self.finish(json.dumps({
|
35
|
|
'error': {
|
36
|
|
'code': status_code,
|
37
|
|
'message': self._reason,
|
38
|
|
'traceback': lines,
|
39
|
|
}
|
40
|
|
}))
|
41
|
|
else:
|
42
|
0
|
self.finish(json.dumps({
|
43
|
|
'error': {
|
44
|
|
'code': status_code,
|
45
|
|
'message': self._reason,
|
46
|
|
}
|
47
|
|
}))
|
48
|
|
|
49
|
6
|
class ParamHandler(BaseHandler):
|
50
|
|
|
51
|
6
|
def __init__(self, app, request, **kwargs):
|
52
|
0
|
self.root = kwargs.pop('root', None)
|
53
|
0
|
super().__init__(app, request, **kwargs)
|
54
|
|
|
55
|
6
|
@classmethod
|
56
|
1
|
def serialize(cls, parameterized, parameters):
|
57
|
0
|
values = {p: getattr(parameterized, p) for p in parameters}
|
58
|
0
|
return parameterized.param.serialize_parameters(values)
|
59
|
|
|
60
|
6
|
@classmethod
|
61
|
1
|
def deserialize(cls, parameterized, parameters):
|
62
|
0
|
for p in parameters:
|
63
|
0
|
if p not in parameterized.param:
|
64
|
0
|
reason = f"'{p}' query parameter not recognized."
|
65
|
0
|
raise HTTPError(reason=reason, status_code=400)
|
66
|
0
|
return {p: parameterized.param.deserialize_value(p, v)
|
67
|
|
for p, v in parameters.items()}
|
68
|
|
|
69
|
6
|
async def get(self):
|
70
|
0
|
path = self.request.path
|
71
|
0
|
endpoint = path[path.index(self.root)+len(self.root):]
|
72
|
0
|
parameterized, parameters, _ = state._rest_endpoints.get(
|
73
|
|
endpoint, (None, None, None)
|
74
|
|
)
|
75
|
0
|
if not parameterized:
|
76
|
0
|
return
|
77
|
0
|
args = parse_qs(self.request.query)
|
78
|
0
|
params = self.deserialize(parameterized[0], args)
|
79
|
0
|
parameterized[0].param.set_param(**params)
|
80
|
0
|
self.set_header('Content-Type', 'application/json')
|
81
|
0
|
self.write(self.serialize(parameterized[0], parameters))
|
82
|
|
|
83
|
|
|
84
|
6
|
def build_tranquilize_application(files):
|
85
|
0
|
from tranquilizer.handler import ScriptHandler, NotebookHandler
|
86
|
0
|
from tranquilizer.main import make_app, UnsupportedFileType
|
87
|
|
|
88
|
0
|
functions = []
|
89
|
0
|
for filename in files:
|
90
|
0
|
extension = filename.split('.')[-1]
|
91
|
0
|
if extension == 'py':
|
92
|
0
|
source = ScriptHandler(filename)
|
93
|
0
|
elif extension == 'ipynb':
|
94
|
0
|
try:
|
95
|
0
|
import nbconvert # noqa
|
96
|
|
except ImportError as e: # pragma no cover
|
97
|
|
raise ImportError("Please install nbconvert to serve Jupyter Notebooks.") from e
|
98
|
|
|
99
|
0
|
source = NotebookHandler(filename)
|
100
|
|
else:
|
101
|
0
|
raise UnsupportedFileType('{} is not a script (.py) or notebook (.ipynb)'.format(filename))
|
102
|
0
|
functions.extend(source.tranquilized_functions)
|
103
|
0
|
return make_app(functions, 'Panel REST API', prefix='rest/')
|
104
|
|
|
105
|
|
|
106
|
6
|
def tranquilizer_rest_provider(files, endpoint):
|
107
|
|
"""
|
108
|
|
Returns a Tranquilizer based REST API. Builds the API by evaluating
|
109
|
|
the scripts and notebooks being served and finding all tranquilized
|
110
|
|
functions inside them.
|
111
|
|
|
112
|
|
Arguments
|
113
|
|
---------
|
114
|
|
files: list(str)
|
115
|
|
A list of paths being served
|
116
|
|
endpoint: str
|
117
|
|
The endpoint to serve the REST API on
|
118
|
|
|
119
|
|
Returns
|
120
|
|
-------
|
121
|
|
A Tornado routing pattern containing the route and handler
|
122
|
|
"""
|
123
|
0
|
app = build_tranquilize_application(files)
|
124
|
0
|
tr = WSGIContainer(app)
|
125
|
0
|
return [(r"^/%s/.*" % endpoint, web.FallbackHandler, dict(fallback=tr))]
|
126
|
|
|
127
|
|
|
128
|
6
|
def param_rest_provider(files, endpoint):
|
129
|
|
"""
|
130
|
|
Returns a Param based REST API given the scripts or notebooks
|
131
|
|
containing the tranquilized functions.
|
132
|
|
|
133
|
|
Arguments
|
134
|
|
---------
|
135
|
|
files: list(str)
|
136
|
|
A list of paths being served
|
137
|
|
endpoint: str
|
138
|
|
The endpoint to serve the REST API on
|
139
|
|
|
140
|
|
Returns
|
141
|
|
-------
|
142
|
|
A Tornado routing pattern containing the route and handler
|
143
|
|
"""
|
144
|
6
|
for filename in files:
|
145
|
0
|
extension = filename.split('.')[-1]
|
146
|
0
|
if extension == 'py':
|
147
|
0
|
try:
|
148
|
0
|
run_path(filename)
|
149
|
0
|
except Exception:
|
150
|
0
|
param.main.warning("Could not run app script on REST server startup.")
|
151
|
0
|
elif extension == 'ipynb':
|
152
|
0
|
try:
|
153
|
0
|
import nbconvert # noqa
|
154
|
0
|
except ImportError:
|
155
|
0
|
raise ImportError("Please install nbconvert to serve Jupyter Notebooks.")
|
156
|
0
|
from nbconvert import ScriptExporter
|
157
|
0
|
exporter = ScriptExporter()
|
158
|
0
|
source, _ = exporter.from_filename(filename)
|
159
|
0
|
source_dir = os.path.dirname(filename)
|
160
|
0
|
with tempfile.NamedTemporaryFile(mode='w', dir=source_dir, delete=True) as tmp:
|
161
|
0
|
tmp.write(source)
|
162
|
0
|
tmp.flush()
|
163
|
0
|
try:
|
164
|
0
|
run_path(tmp.name, init_globals={'get_ipython': MagicMock()})
|
165
|
0
|
except Exception:
|
166
|
0
|
param.main.warning("Could not run app notebook on REST server startup.")
|
167
|
|
else:
|
168
|
0
|
raise ValueError('{} is not a script (.py) or notebook (.ipynb)'.format(filename))
|
169
|
|
|
170
|
6
|
if endpoint and not endpoint.endswith('/'):
|
171
|
6
|
endpoint += '/'
|
172
|
6
|
return [((r"^/%s.*" % endpoint if endpoint else r"^.*"), ParamHandler, dict(root=endpoint))]
|
173
|
|
|
174
|
|
|
175
|
6
|
REST_PROVIDERS = {
|
176
|
|
'tranquilizer': tranquilizer_rest_provider,
|
177
|
|
'param': param_rest_provider
|
178
|
|
}
|
179
|
|
|
180
|
|
# Populate REST Providers from external extensions
|
181
|
6
|
for entry_point in pkg_resources.iter_entry_points('panel.io.rest'):
|
182
|
0
|
REST_PROVIDERS[entry_point.name] = entry_point.resolve()
|