/
plugin.py
352 lines (299 loc) · 14.7 KB
/
plugin.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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
"""
Visualization plugins: instantiate/deserialize data and models
from a query string and render a webpage based on those data.
"""
import os
import copy
import mako
from galaxy.managers import api_keys
from galaxy.visualization.plugins import interactive_environments
from galaxy.visualization.plugins import pluginframework
from galaxy.visualization.plugins import resource_parser
from galaxy.visualization.plugins import utils
import logging
log = logging.getLogger(__name__)
# =============================================================================
# TODO:
# move mixins to facade'd objects
# allow config to override static/template settings
# allow config detection in alternate places: galaxy-visualization.xml
# =============================================================================
class ServesStaticPluginMixin(object):
"""
An object that serves static files from the server.
"""
def _set_up_static_plugin(self, **kwargs):
"""
Detect and set up static paths and urls if needed.
"""
# TODO: allow config override
self.serves_static = False
if self._is_static_plugin():
self.static_path = self._build_static_path()
self.static_url = self._build_static_url()
self.serves_static = True
return self.serves_static
def _is_static_plugin(self):
"""
Detect whether this plugin should serve static resources.
"""
return os.path.isdir(self._build_static_path())
def _build_static_path(self):
return os.path.join(self.path, 'static')
def _build_static_url(self):
return '/'.join([self.base_url, 'static'])
# =============================================================================
class ServesTemplatesPluginMixin(object):
"""
An object that renders (mako) template files from the server.
"""
#: default number of templates to search for plugin template lookup
DEFAULT_TEMPLATE_COLLECTION_SIZE = 10
#: default encoding of plugin templates
DEFAULT_TEMPLATE_ENCODING = 'utf-8'
def _set_up_template_plugin(self, template_cache_dir, additional_template_paths=None, **kwargs):
"""
Detect and set up template paths if the plugin serves templates.
"""
self.serves_templates = False
if self._is_template_plugin():
self.template_path = self._build_template_path()
self.template_lookup = self._build_template_lookup(template_cache_dir,
additional_template_paths=additional_template_paths)
self.serves_templates = True
return self.serves_templates
def _is_template_plugin(self):
return os.path.isdir(self._build_template_path())
def _build_template_path(self):
return os.path.join(self.path, 'templates')
def _build_template_lookup(self, template_cache_dir, additional_template_paths=None,
collection_size=DEFAULT_TEMPLATE_COLLECTION_SIZE, output_encoding=DEFAULT_TEMPLATE_ENCODING):
"""
Build a mako template filename lookup for the plugin.
"""
template_lookup_paths = self.template_path
if additional_template_paths:
template_lookup_paths = [template_lookup_paths] + additional_template_paths
return mako.lookup.TemplateLookup(
directories=template_lookup_paths,
module_directory=template_cache_dir,
collection_size=collection_size,
output_encoding=output_encoding)
# =============================================================================
class VisualizationPlugin(pluginframework.Plugin, ServesStaticPluginMixin, ServesTemplatesPluginMixin):
"""
A plugin that instantiates resources, serves static files, and uses mako
templates to render web pages.
"""
# AKA: MakoVisualizationPlugin
# config[ 'entry_point' ][ 'type' ] == 'mako'
# TODO: concept/name collision between plugin config and visualization config
def __init__(self, app, path, name, config, context=None, **kwargs):
super(VisualizationPlugin, self).__init__(app, path, name, config, context=None, **kwargs)
context = context or {}
self.config = config
base_url = context.get('base_url', '')
self.base_url = '/'.join([base_url, self.name]) if base_url else self.name
self._set_up_static_plugin()
template_cache_dir = context.get('template_cache_dir', None)
additional_template_paths = context.get('additional_template_paths', [])
self._set_up_template_plugin(template_cache_dir, additional_template_paths=additional_template_paths)
self.resource_parser = resource_parser.ResourceParser(app)
def render(self, trans=None, embedded=None, **kwargs):
"""
Render and return the text of the non-saved plugin webpage/fragment.
"""
# not saved - no existing config
config = {}
# set up render vars based on plugin.config and kwargs
render_vars = self._build_render_vars(config, trans=trans, **kwargs)
return self._render(render_vars, trans=trans, embedded=embedded)
def render_saved(self, visualization, trans=None, embedded=None, **kwargs):
"""
Render and return the text of the plugin webpage/fragment using the
config/data of a saved visualization.
"""
config = self._get_saved_visualization_config(visualization, **kwargs)
# pass the saved visualization config for parsing into render vars
render_vars = self._build_render_vars(config, trans=trans, **kwargs)
# update any values that were loaded from the saved Visualization
render_vars.update(dict(
title=visualization.latest_revision.title,
saved_visualization=visualization,
visualization_id=trans.security.encode_id(visualization.id),
))
return self._render(render_vars, trans=trans, embedded=embedded)
def _get_saved_visualization_config(self, visualization, revision=None, **kwargs):
"""
Return the config of a saved visualization and revision.
If no revision given, default to latest revision.
"""
# TODO: allow loading a specific revision - should be part of UsesVisualization
return copy.copy(visualization.latest_revision.config)
# ---- non-public
def _build_render_vars(self, config, trans=None, **kwargs):
"""
Build all the variables that will be passed into the renderer.
"""
render_vars = {}
# Meta variables passed to the template/renderer to describe the visualization being rendered.
render_vars.update(
visualization_name=self.name,
visualization_display_name=self.config['name'],
title=kwargs.get('title', None),
saved_visualization=None,
visualization_id=None,
# NOTE: passing *unparsed* kwargs as query
query=kwargs,
)
# config based on existing or kwargs
config = self._build_config(config, trans=trans, **kwargs)
render_vars['config'] = config
# further parse config to resources (models, etc.) used in template based on registry config
resources = self._config_to_resources(trans, config)
render_vars.update(resources)
return render_vars
def _build_config(self, config, trans=None, **kwargs):
"""
Build the configuration for this new/saved visualization by combining
any existing config and the kwargs (gen. from the url query).
"""
# first, pull from any existing config
if config:
config = copy.copy(config)
else:
config = {}
# then, overwrite with keys/values from kwargs (gen. a query string)
config_from_kwargs = self._kwargs_to_config(trans, kwargs)
config.update(config_from_kwargs)
# to object format for easier querying
config = utils.OpenObject(**config)
return config
# TODO: the difference between config & resources is unclear in this section - is it needed?
def _kwargs_to_config(self, trans, kwargs):
"""
Given a kwargs dict (gen. a query string dict from a controller action), parse
and return any key/value pairs found in the plugin's `params` section.
"""
expected_params = self.config.get('params', {})
config = self.resource_parser.parse_config(trans, expected_params, kwargs)
return config
def _config_to_resources(self, trans, config):
"""
Instantiate/deserialize the resources (HDAs, LDDAs, etc.) given in a
visualization config into models/variables a visualization renderer can use.
"""
expected_params = self.config.get('params', {})
param_modifiers = self.config.get('param_modifiers', {})
resources = self.resource_parser.parse_parameter_dictionary(trans, expected_params, config, param_modifiers)
return resources
def _render(self, render_vars, trans=None, embedded=None, **kwargs):
"""
Render the visualization via Mako and the plugin's template file.
"""
render_vars['embedded'] = self._parse_embedded(embedded)
# NOTE: (mako specific) vars is a dictionary for shared data in the template
# this feels hacky to me but it's what mako recommends:
# http://docs.makotemplates.org/en/latest/runtime.html
render_vars.update(vars={})
template_filename = self.config['entry_point']['file']
return trans.fill_template(template_filename, template_lookup=self.template_lookup, **render_vars)
def _parse_embedded(self, embedded):
"""
Parse information on dimensions, readonly, etc. from the embedded query val.
"""
# as is for now
return embedded
# =============================================================================
class InteractiveEnvironmentPlugin(VisualizationPlugin):
"""
Serves web-based REPLs such as Jupyter and RStudio.
"""
INTENV_REQUEST_FACTORY = interactive_environments.InteractiveEnvironmentRequest
def __init__(self, app, path, name, config, context=None, **kwargs):
# TODO: this is a hack until we can get int envs seperated from the vis reg and into their own framework
context['base_url'] = 'interactive_environments'
super(InteractiveEnvironmentPlugin, self).__init__(app, path, name, config, context=context, **kwargs)
def _render(self, render_vars, trans=None, embedded=None, **kwargs):
"""
Override to add interactive environment specific template vars.
"""
render_vars['embedded'] = self._parse_embedded(embedded)
# NOTE: (mako specific) vars is a dictionary for shared data in the template
# this feels hacky to me but it's what mako recommends:
# http://docs.makotemplates.org/en/latest/runtime.html
render_vars.update(vars={})
# No longer needed but being left around for a few releases as jupyter-galaxy
# as an external visualization plugin is deprecated in favor of core interactive
# environment plugin.
if 'get_api_key' not in render_vars:
def get_api_key():
return api_keys.ApiKeyManager(trans.app).get_or_create_api_key(trans.user)
render_vars['get_api_key'] = get_api_key
if 'plugin_path' not in render_vars:
render_vars['plugin_path'] = os.path.abspath(self.path)
if self.config.get('plugin_type', 'visualization') == "interactive_environment":
try:
request = self.INTENV_REQUEST_FACTORY(trans, self)
except:
log.exception("IE plugin request handling failed")
return trans.fill_template('message.mako',
message='Loading the interactive environment failed, please contact the {admin_tag} for assistance'.format(
admin_tag='<a href="mailto:{admin_mail}">Galaxy administrator</a>'.format(
admin_mail=trans.app.config.error_email_to)
if trans.app.config.error_email_to else 'Galaxy administrator'),
status='error')
render_vars["ie_request"] = request
template_filename = self.config['entry_point']['file']
return trans.fill_template(template_filename, template_lookup=self.template_lookup, **render_vars)
# =============================================================================
class ScriptVisualizationPlugin(VisualizationPlugin):
"""
A visualization plugin that starts by loading a single (js) script.
The script is loaded into a pre-defined mako template:
`config/plugins/visualizations/common/templates/script_entry_point.mako`
"""
MAKO_TEMPLATE = 'script_entry_point.mako'
def _is_template_plugin(self):
"""
Override to always yield true since this plugin type always uses the
pre-determined mako template.
"""
return True
def _render(self, render_vars, trans=None, embedded=None, **kwargs):
"""
Override to add script attributes and point mako at the script entry point
template.
"""
render_vars['embedded'] = self._parse_embedded(embedded)
render_vars.update(vars={})
render_vars.update({
"script_tag_attributes" : self.config['entry_point']['attr']
})
template_filename = os.path.join(self.MAKO_TEMPLATE)
return trans.fill_template(template_filename, template_lookup=self.template_lookup, **render_vars)
# =============================================================================
class StaticFileVisualizationPlugin(VisualizationPlugin):
"""
A visualiztion plugin that starts by loading a static html file defined
in the visualization's config file.
"""
# TODO: these are not embeddable by their nature - update config
# TODO: should do render/render_saved here since most of the calc done there is unneeded in this case
def _render(self, render_vars, trans=None, embedded=None, **kwargs):
"""
Render the static file simply by reading and returning it.
"""
render_vars['embedded'] = self._parse_embedded(embedded)
render_vars.update(vars={})
static_file_path = self.config['entry_point']['file']
static_file_path = os.path.join(self.path, static_file_path)
with open(static_file_path, 'r') as outfile:
return outfile.read()
# # =============================================================================
# class PyGeneratedVisualizationPlugin( VisualizationPlugin ):
# """
# Selectively import one module and call a specified fn within it to generate the
# HTML served.
# """
# pass