/
http_reply.py
327 lines (263 loc) · 9.25 KB
/
http_reply.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
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# -*- coding: utf-8 -*-
u"""response generation
:copyright: Copyright (c) 2018 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
from __future__ import absolute_import, division, print_function
from pykern import pkcollections
from pykern import pkconfig
from pykern import pkconst
from pykern import pkio
from pykern.pkcollections import PKDict
from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp
import flask
import mimetypes
import pykern.pkinspect
import re
import sirepo.http_request
import sirepo.uri
import sirepo.util
import werkzeug.exceptions
#: data.state for srException
SR_EXCEPTION_STATE = 'srException'
#: mapping of extension (json, js, html) to MIME type
MIME_TYPE = None
_ERROR_STATE = 'error'
_STATE = 'state'
#: Default response
_RESPONSE_OK = PKDict({_STATE: 'ok'})
#: Parsing errors from subprocess
_SUBPROCESS_ERROR_RE = re.compile(r'(?:warning|exception|error): ([^\n]+?)(?:;|\n|$)', flags=re.IGNORECASE)
#: routes that will require a reload
_RELOAD_JS_ROUTES = None
def as_attachment(resp, content_type, filename):
resp.mimetype = content_type
resp.headers['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return resp
def gen_exception(exc):
"""Generate from an Exception
Args:
exc (Exception): valid convert into a response
"""
# If an exception occurs here, we'll fall through
# to flask, which will have code to handle this case.
if isinstance(exc, sirepo.util.Reply):
return _gen_exception_reply(exc)
if isinstance(exc, werkzeug.exceptions.HTTPException):
return _gen_exception_werkzeug(exc)
return _gen_exception_error(exc)
def gen_file_as_attachment(content_or_path, filename=None, content_type=None):
"""Generate a flask file attachment response
Args:
content_or_path (bytes or py.path): File contents
filename (str): Name of file [content_or_path.basename]
content_type (str): MIMETYPE of file [guessed]
Returns:
flask.Response: reply object
"""
def f():
if isinstance(content_or_path, pkconst.PY_PATH_LOCAL_TYPE):
return flask.send_file(str(content_or_path))
return flask.current_app.response_class(content_or_path)
if filename is None:
# dies if content_or_path is not a path
filename = content_or_path.basename
if content_type is None:
content_type, _ = mimetypes.guess_type(filename)
if content_type is None:
content_type = 'application/octet-stream'
# overrule mimetypes for this case
elif content_type == 'text/x-python':
content_type = 'text/plain'
return headers_for_no_cache(
as_attachment(f(), content_type, filename))
def gen_json(value, pretty=False, response_kwargs=None):
"""Generate JSON flask response
Args:
value (dict): what to format
pretty (bool): pretty print [False]
Returns:
flask.Response: reply object
"""
app = flask.current_app
if not response_kwargs:
response_kwargs = pkcollections.Dict()
return app.response_class(
simulation_db.generate_json(value, pretty=pretty),
mimetype=MIME_TYPE.json,
**response_kwargs
)
def gen_json_ok(*args, **kwargs):
"""Generate state=ok JSON flask response
Returns:
flask.Response: reply object
"""
if not args:
# do not cache this, see #1390
return gen_json(_RESPONSE_OK)
assert len(args) == 1
res = args[0]
res.update(_RESPONSE_OK)
return gen_json(res)
def gen_redirect(uri):
"""Redirect to uri
Args:
uri (str): any valid uri (even with anchor)
Returns:
flask.Response: reply object
"""
return gen_redirect_for_anchor(uri=uri)
def gen_redirect_for_anchor(uri, **kwargs):
"""Redirect uri with an anchor using javascript
Safari browser doesn't support redirects with anchors so we do this
in all cases.
Args:
uri (str): where to redirect to
Returns:
flask.Response: reply object
"""
return render_static(
'javascript-redirect',
'html',
pkcollections.Dict(redirect_uri=uri),
)
def gen_redirect_for_app_root(sim_type):
"""Redirect to app root for sim_type
Args:
sim_type (str): valid sim_type or None [http_request.sim_type]
Returns:
flask.Response: reply object
"""
return gen_redirect_for_anchor(sirepo.uri.app_root(sim_type))
def gen_redirect_for_local_route(sim_type=None, route=None, params=None, query=None):
"""Generate a javascript redirect to sim_type/route/params
Default route (None) only supported for ``default``
application_mode/appMode.
Args:
sim_type (str): how to find the schema [http_request.sim_type]
route (str): name in localRoutes [None: use default route]
params (dict): parameters for route (including :Name)
Returns:
flask.Response: reply object
"""
return gen_redirect_for_anchor(
sirepo.uri.local_route(sim_type, route, params, query),
)
def headers_for_no_cache(resp):
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
resp.headers['Pragma'] = 'no-cache'
return resp
def init(app, **imports):
global MIME_TYPE, _RELOAD_JS_ROUTES, _app
_app = app
sirepo.util.setattr_imports(imports)
MIME_TYPE = pkcollections.Dict(
html='text/html',
js='application/javascript',
json=app.config.get('JSONIFY_MIMETYPE', 'application/json'),
py='text/x-python',
)
s = simulation_db.get_schema(sim_type=None)
_RELOAD_JS_ROUTES = frozenset(
(k for k, v in s.localRoutes.items() if v.get('requireReload')),
)
def render_static(base, ext, j2_ctx, cache_ok=False):
"""Call flask.render_template appropriately
Args:
base (str): base name of file, e.g. ``user-state``
ext (str): suffix of file, e.g. ``js``
j2_ctx (dict): jinja context
cache_ok (bool): OK to cache the result? [default: False]
Returns:
object: Flask.Response
"""
fn = '{}/{}.{}'.format(ext, base, ext)
r = flask.Response(
flask.render_template(fn, **j2_ctx),
mimetype=MIME_TYPE[ext],
)
if not cache_ok:
r = headers_for_no_cache(r)
return r
def _gen_exception_error(exc):
pkdlog('unsupported exception={} msg={}', type(exc), exc)
return gen_redirect_for_local_route(None, route='error')
def _gen_exception_reply(exc):
f = getattr(
pykern.pkinspect.this_module(),
'_gen_exception_reply_' + exc.__class__.__name__,
None,
)
pkdc('exception={} sr_args={}', exc, exc.sr_args)
if not f:
return _gen_exception_error(exc)
return f(exc.sr_args)
def _gen_exception_reply_Error(args):
try:
t = sirepo.http_request.sim_type(args.pkdel('sim_type'))
s = simulation_db.get_schema(sim_type=t)
except Exception:
# sim_type is bad so don't cascade errors, just
# try to get the schema without the type
t = None
s = simulation_db.get_schema(sim_type=None)
if flask.request.method == 'POST':
return gen_json(args.pkupdate({_STATE: _ERROR_STATE}))
q = PKDict()
for k, v in args.items():
try:
v = str(v)
assert len(v) < 200, 'value is too long (>=200 chars)'
except Exception as e:
pkdlog('error in "error" query {}={} exception={}', k, v, e)
continue
q[k] = v
return gen_redirect_for_local_route(t, route='error', query=q)
def _gen_exception_reply_Redirect(args):
return gen_redirect(args.uri)
def _gen_exception_reply_Response(args):
r = args.response
assert isinstance(r, _app.response_class), \
'invalid class={} response={}'.format(type(r), r)
return r
def _gen_exception_reply_SRException(args):
r = args.routeName
p = args.params or PKDict()
try:
t = sirepo.http_request.sim_type(p.pkdel('sim_type'))
s = simulation_db.get_schema(sim_type=t)
except Exception as e:
pkdc('exception={} stack={}', e, pkdexc())
# sim_type is bad so don't cascade errors, just
# try to get the schema without the type
t = None
s = simulation_db.get_schema(sim_type=None)
# If default route or always redirect/reload
if r:
assert r in s.localRoutes, \
'route={} not found in schema for type={}'.format(r, t)
else:
r = sirepo.uri.default_local_route_name(s)
p = PKDict(reload_js=True)
if (
# must be first, to always delete reload_js
not p.pkdel('reload_js')
and flask.request.method == 'POST'
and r not in _RELOAD_JS_ROUTES
):
pkdc('POST response={} route={} params={}', SR_EXCEPTION_STATE, r, p)
return gen_json(
PKDict({
_STATE: SR_EXCEPTION_STATE,
SR_EXCEPTION_STATE: args,
}),
)
pkdc('redirect to route={} params={} type={}', r, p, t)
return gen_redirect_for_local_route(t, route=r, params=p)
def _gen_exception_reply_UserAlert(args):
return gen_json(
PKDict({_STATE: _ERROR_STATE, _ERROR_STATE: args.error}),
)
def _gen_exception_werkzeug(exc):
#TODO(robnagler) convert exceptions to our own
raise exc