ob-metaflow 2.10.7.4__py2.py3-none-any.whl → 2.10.9.2__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ob-metaflow might be problematic. Click here for more details.
- metaflow/cards.py +2 -0
- metaflow/decorators.py +1 -1
- metaflow/metaflow_config.py +4 -0
- metaflow/plugins/__init__.py +4 -0
- metaflow/plugins/airflow/airflow_cli.py +1 -1
- metaflow/plugins/argo/argo_workflows.py +5 -0
- metaflow/plugins/argo/argo_workflows_cli.py +1 -1
- metaflow/plugins/aws/aws_utils.py +1 -1
- metaflow/plugins/aws/batch/batch.py +4 -0
- metaflow/plugins/aws/batch/batch_cli.py +3 -0
- metaflow/plugins/aws/batch/batch_client.py +40 -11
- metaflow/plugins/aws/batch/batch_decorator.py +1 -0
- metaflow/plugins/aws/step_functions/step_functions.py +1 -0
- metaflow/plugins/aws/step_functions/step_functions_cli.py +1 -1
- metaflow/plugins/azure/azure_exceptions.py +1 -1
- metaflow/plugins/cards/card_cli.py +413 -28
- metaflow/plugins/cards/card_client.py +16 -7
- metaflow/plugins/cards/card_creator.py +228 -0
- metaflow/plugins/cards/card_datastore.py +124 -26
- metaflow/plugins/cards/card_decorator.py +40 -86
- metaflow/plugins/cards/card_modules/base.html +12 -0
- metaflow/plugins/cards/card_modules/basic.py +74 -8
- metaflow/plugins/cards/card_modules/bundle.css +1 -170
- metaflow/plugins/cards/card_modules/card.py +65 -0
- metaflow/plugins/cards/card_modules/components.py +446 -81
- metaflow/plugins/cards/card_modules/convert_to_native_type.py +9 -3
- metaflow/plugins/cards/card_modules/main.js +250 -21
- metaflow/plugins/cards/card_modules/test_cards.py +117 -0
- metaflow/plugins/cards/card_resolver.py +0 -2
- metaflow/plugins/cards/card_server.py +361 -0
- metaflow/plugins/cards/component_serializer.py +506 -42
- metaflow/plugins/cards/exception.py +20 -1
- metaflow/plugins/datastores/azure_storage.py +1 -2
- metaflow/plugins/datastores/gs_storage.py +1 -2
- metaflow/plugins/datastores/s3_storage.py +2 -1
- metaflow/plugins/datatools/s3/s3.py +24 -11
- metaflow/plugins/env_escape/client.py +2 -12
- metaflow/plugins/env_escape/client_modules.py +18 -14
- metaflow/plugins/env_escape/server.py +18 -11
- metaflow/plugins/env_escape/utils.py +12 -0
- metaflow/plugins/gcp/gs_exceptions.py +1 -1
- metaflow/plugins/gcp/gs_utils.py +1 -1
- metaflow/plugins/kubernetes/kubernetes.py +43 -6
- metaflow/plugins/kubernetes/kubernetes_cli.py +40 -1
- metaflow/plugins/kubernetes/kubernetes_decorator.py +73 -6
- metaflow/plugins/kubernetes/kubernetes_job.py +536 -161
- metaflow/plugins/pypi/conda_environment.py +5 -6
- metaflow/plugins/pypi/pip.py +2 -2
- metaflow/plugins/pypi/utils.py +15 -0
- metaflow/task.py +1 -0
- metaflow/version.py +1 -1
- {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/METADATA +1 -1
- {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/RECORD +57 -55
- {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/LICENSE +0 -0
- {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/WHEEL +0 -0
- {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/entry_points.txt +0 -0
- {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/top_level.txt +0 -0
|
@@ -11,10 +11,53 @@ from .basic import (
|
|
|
11
11
|
from .card import MetaflowCardComponent
|
|
12
12
|
from .convert_to_native_type import TaskToDict, _full_classname
|
|
13
13
|
from .renderer_tools import render_safely
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_component_id(component):
|
|
18
|
+
uuid_bit = "".join(uuid.uuid4().hex.split("-"))[:6]
|
|
19
|
+
return type(component).__name__.lower() + "_" + uuid_bit
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def with_default_component_id(func):
|
|
23
|
+
def ret_func(self, *args, **kwargs):
|
|
24
|
+
if self.component_id is None:
|
|
25
|
+
self.component_id = create_component_id(self)
|
|
26
|
+
return func(self, *args, **kwargs)
|
|
27
|
+
|
|
28
|
+
return ret_func
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _warning_with_component(component, msg):
|
|
32
|
+
if component._logger is None:
|
|
33
|
+
return None
|
|
34
|
+
if component._warned_once:
|
|
35
|
+
return None
|
|
36
|
+
log_msg = "[@card-component WARNING] %s" % msg
|
|
37
|
+
component._logger(log_msg, timestamp=False, bad=True)
|
|
38
|
+
component._warned_once = True
|
|
14
39
|
|
|
15
40
|
|
|
16
41
|
class UserComponent(MetaflowCardComponent):
|
|
17
|
-
|
|
42
|
+
|
|
43
|
+
_warned_once = False
|
|
44
|
+
|
|
45
|
+
def update(self, *args, **kwargs):
|
|
46
|
+
cls_name = self.__class__.__name__
|
|
47
|
+
msg = (
|
|
48
|
+
"MetaflowCardComponent doesn't have an `update` method implemented "
|
|
49
|
+
"and is not compatible with realtime updates."
|
|
50
|
+
) % cls_name
|
|
51
|
+
_warning_with_component(self, msg)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StubComponent(UserComponent):
|
|
55
|
+
def __init__(self, component_id):
|
|
56
|
+
self._non_existing_comp_id = component_id
|
|
57
|
+
|
|
58
|
+
def update(self, *args, **kwargs):
|
|
59
|
+
msg = "Component with id %s doesn't exist. No updates will be made at anytime during runtime."
|
|
60
|
+
_warning_with_component(self, msg % self._non_existing_comp_id)
|
|
18
61
|
|
|
19
62
|
|
|
20
63
|
class Artifact(UserComponent):
|
|
@@ -39,6 +82,11 @@ class Artifact(UserComponent):
|
|
|
39
82
|
Use a truncated representation.
|
|
40
83
|
"""
|
|
41
84
|
|
|
85
|
+
REALTIME_UPDATABLE = True
|
|
86
|
+
|
|
87
|
+
def update(self, artifact):
|
|
88
|
+
self._artifact = artifact
|
|
89
|
+
|
|
42
90
|
def __init__(
|
|
43
91
|
self, artifact: Any, name: Optional[str] = None, compressed: bool = True
|
|
44
92
|
):
|
|
@@ -46,13 +94,16 @@ class Artifact(UserComponent):
|
|
|
46
94
|
self._name = name
|
|
47
95
|
self._task_to_dict = TaskToDict(only_repr=compressed)
|
|
48
96
|
|
|
97
|
+
@with_default_component_id
|
|
49
98
|
@render_safely
|
|
50
99
|
def render(self):
|
|
51
100
|
artifact = self._task_to_dict.infer_object(self._artifact)
|
|
52
101
|
artifact["name"] = None
|
|
53
102
|
if self._name is not None:
|
|
54
103
|
artifact["name"] = str(self._name)
|
|
55
|
-
|
|
104
|
+
af_component = ArtifactsComponent(data=[artifact])
|
|
105
|
+
af_component.component_id = self.component_id
|
|
106
|
+
return af_component.render()
|
|
56
107
|
|
|
57
108
|
|
|
58
109
|
class Table(UserComponent):
|
|
@@ -96,10 +147,21 @@ class Table(UserComponent):
|
|
|
96
147
|
Optional header row for the table.
|
|
97
148
|
"""
|
|
98
149
|
|
|
150
|
+
REALTIME_UPDATABLE = True
|
|
151
|
+
|
|
152
|
+
def update(self, *args, **kwargs):
|
|
153
|
+
msg = (
|
|
154
|
+
"`Table` doesn't have an `update` method implemented. "
|
|
155
|
+
"Components within a table can be updated individually "
|
|
156
|
+
"but the table itself cannot be updated."
|
|
157
|
+
)
|
|
158
|
+
_warning_with_component(self, msg)
|
|
159
|
+
|
|
99
160
|
def __init__(
|
|
100
161
|
self,
|
|
101
162
|
data: Optional[List[List[Union[str, MetaflowCardComponent]]]] = None,
|
|
102
163
|
headers: Optional[List[str]] = None,
|
|
164
|
+
disable_updates: bool = False,
|
|
103
165
|
):
|
|
104
166
|
data = data or [[]]
|
|
105
167
|
headers = headers or []
|
|
@@ -111,8 +173,15 @@ class Table(UserComponent):
|
|
|
111
173
|
if data_bool:
|
|
112
174
|
self._data = data
|
|
113
175
|
|
|
176
|
+
if disable_updates:
|
|
177
|
+
self.REALTIME_UPDATABLE = False
|
|
178
|
+
|
|
114
179
|
@classmethod
|
|
115
|
-
def from_dataframe(
|
|
180
|
+
def from_dataframe(
|
|
181
|
+
cls,
|
|
182
|
+
dataframe=None,
|
|
183
|
+
truncate: bool = True,
|
|
184
|
+
):
|
|
116
185
|
"""
|
|
117
186
|
Create a `Table` based on a Pandas dataframe.
|
|
118
187
|
|
|
@@ -129,14 +198,24 @@ class Table(UserComponent):
|
|
|
129
198
|
table_data = task_to_dict._parse_pandas_dataframe(
|
|
130
199
|
dataframe, truncate=truncate
|
|
131
200
|
)
|
|
132
|
-
return_val = cls(
|
|
201
|
+
return_val = cls(
|
|
202
|
+
data=table_data["data"],
|
|
203
|
+
headers=table_data["headers"],
|
|
204
|
+
disable_updates=True,
|
|
205
|
+
)
|
|
133
206
|
return return_val
|
|
134
207
|
else:
|
|
135
208
|
return cls(
|
|
136
209
|
headers=["Object type %s not supported" % object_type],
|
|
210
|
+
disable_updates=True,
|
|
137
211
|
)
|
|
138
212
|
|
|
139
213
|
def _render_subcomponents(self):
|
|
214
|
+
for row in self._data:
|
|
215
|
+
for col in row:
|
|
216
|
+
if isinstance(col, VegaChart):
|
|
217
|
+
col._chart_inside_table = True
|
|
218
|
+
|
|
140
219
|
return [
|
|
141
220
|
SectionComponent.render_subcomponents(
|
|
142
221
|
row,
|
|
@@ -155,11 +234,14 @@ class Table(UserComponent):
|
|
|
155
234
|
for row in self._data
|
|
156
235
|
]
|
|
157
236
|
|
|
237
|
+
@with_default_component_id
|
|
158
238
|
@render_safely
|
|
159
239
|
def render(self):
|
|
160
|
-
|
|
240
|
+
table_component = TableComponent(
|
|
161
241
|
headers=self._headers, data=self._render_subcomponents()
|
|
162
|
-
)
|
|
242
|
+
)
|
|
243
|
+
table_component.component_id = self.component_id
|
|
244
|
+
return table_component.render()
|
|
163
245
|
|
|
164
246
|
|
|
165
247
|
class Image(UserComponent):
|
|
@@ -213,21 +295,62 @@ class Image(UserComponent):
|
|
|
213
295
|
Optional label for the image.
|
|
214
296
|
"""
|
|
215
297
|
|
|
298
|
+
REALTIME_UPDATABLE = True
|
|
299
|
+
|
|
300
|
+
_PIL_IMAGE_MODULE_PATH = "PIL.Image.Image"
|
|
301
|
+
|
|
302
|
+
_MATPLOTLIB_FIGURE_MODULE_PATH = "matplotlib.figure.Figure"
|
|
303
|
+
|
|
304
|
+
_PLT_MODULE = None
|
|
305
|
+
|
|
306
|
+
_PIL_MODULE = None
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def _get_pil_module(cls):
|
|
310
|
+
if cls._PIL_MODULE == "NOT_PRESENT":
|
|
311
|
+
return None
|
|
312
|
+
if cls._PIL_MODULE is None:
|
|
313
|
+
try:
|
|
314
|
+
import PIL
|
|
315
|
+
except ImportError:
|
|
316
|
+
cls._PIL_MODULE = "NOT_PRESENT"
|
|
317
|
+
return None
|
|
318
|
+
cls._PIL_MODULE = PIL
|
|
319
|
+
return cls._PIL_MODULE
|
|
320
|
+
|
|
321
|
+
@classmethod
|
|
322
|
+
def _get_plt_module(cls):
|
|
323
|
+
if cls._PLT_MODULE == "NOT_PRESENT":
|
|
324
|
+
return None
|
|
325
|
+
if cls._PLT_MODULE is None:
|
|
326
|
+
try:
|
|
327
|
+
import matplotlib.pyplot as pyplt
|
|
328
|
+
except ImportError:
|
|
329
|
+
cls._PLT_MODULE = "NOT_PRESENT"
|
|
330
|
+
return None
|
|
331
|
+
cls._PLT_MODULE = pyplt
|
|
332
|
+
return cls._PLT_MODULE
|
|
333
|
+
|
|
216
334
|
@staticmethod
|
|
217
335
|
def render_fail_headline(msg):
|
|
218
336
|
return "[IMAGE_RENDER FAIL]: %s" % msg
|
|
219
337
|
|
|
220
|
-
def
|
|
221
|
-
self._error_comp = None
|
|
338
|
+
def _set_image_src(self, src, label=None):
|
|
222
339
|
self._label = label
|
|
223
|
-
|
|
224
|
-
|
|
340
|
+
self._src = None
|
|
341
|
+
self._error_comp = None
|
|
342
|
+
if src is None:
|
|
343
|
+
self._error_comp = ErrorComponent(
|
|
344
|
+
self.render_fail_headline("`Image` Component `src` cannot be `None`"),
|
|
345
|
+
"",
|
|
346
|
+
)
|
|
347
|
+
elif type(src) is not str:
|
|
225
348
|
try:
|
|
226
349
|
self._src = self._bytes_to_base64(src)
|
|
227
350
|
except TypeError:
|
|
228
351
|
self._error_comp = ErrorComponent(
|
|
229
352
|
self.render_fail_headline(
|
|
230
|
-
"
|
|
353
|
+
"The `Image` `src` argument should be of type `bytes` or valid image base64 string"
|
|
231
354
|
),
|
|
232
355
|
"Type of %s is invalid" % (str(type(src))),
|
|
233
356
|
)
|
|
@@ -248,11 +371,141 @@ class Image(UserComponent):
|
|
|
248
371
|
else:
|
|
249
372
|
self._error_comp = ErrorComponent(
|
|
250
373
|
self.render_fail_headline(
|
|
251
|
-
"
|
|
374
|
+
"The `Image` `src` argument should be of type `bytes` or valid image base64 string"
|
|
252
375
|
),
|
|
253
376
|
"String %s is invalid base64 string" % src,
|
|
254
377
|
)
|
|
255
378
|
|
|
379
|
+
def __init__(self, src=None, label=None, disable_updates: bool = True):
|
|
380
|
+
if disable_updates:
|
|
381
|
+
self.REALTIME_UPDATABLE = False
|
|
382
|
+
self._set_image_src(src, label=label)
|
|
383
|
+
|
|
384
|
+
def _update_image(self, img_obj, label=None):
|
|
385
|
+
task_to_dict = TaskToDict()
|
|
386
|
+
parsed_image, err_comp = None, None
|
|
387
|
+
|
|
388
|
+
# First set image for bytes/string type
|
|
389
|
+
if task_to_dict.object_type(img_obj) in ["bytes", "str"]:
|
|
390
|
+
self._set_image_src(img_obj, label=label)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
if task_to_dict.object_type(img_obj).startswith("PIL"):
|
|
394
|
+
parsed_image, err_comp = self._parse_pil_image(img_obj)
|
|
395
|
+
elif _full_classname(img_obj) == self._MATPLOTLIB_FIGURE_MODULE_PATH:
|
|
396
|
+
parsed_image, err_comp = self._parse_matplotlib(img_obj)
|
|
397
|
+
else:
|
|
398
|
+
parsed_image, err_comp = None, ErrorComponent(
|
|
399
|
+
self.render_fail_headline(
|
|
400
|
+
"Invalid Type. Object %s is not supported. Supported types: %s"
|
|
401
|
+
% (
|
|
402
|
+
type(img_obj),
|
|
403
|
+
", ".join(
|
|
404
|
+
[
|
|
405
|
+
"str",
|
|
406
|
+
"bytes",
|
|
407
|
+
self._PIL_IMAGE_MODULE_PATH,
|
|
408
|
+
self._MATPLOTLIB_FIGURE_MODULE_PATH,
|
|
409
|
+
]
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
),
|
|
413
|
+
"",
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if parsed_image is not None:
|
|
417
|
+
self._set_image_src(parsed_image, label=label)
|
|
418
|
+
else:
|
|
419
|
+
self._set_image_src(None, label=label)
|
|
420
|
+
self._error_comp = err_comp
|
|
421
|
+
|
|
422
|
+
@classmethod
|
|
423
|
+
def _pil_parsing_error(cls, error_type):
|
|
424
|
+
return None, ErrorComponent(
|
|
425
|
+
cls.render_fail_headline(
|
|
426
|
+
"first argument for `Image` should be of type %s"
|
|
427
|
+
% cls._PIL_IMAGE_MODULE_PATH
|
|
428
|
+
),
|
|
429
|
+
"Type of %s is invalid. Type of %s required"
|
|
430
|
+
% (error_type, cls._PIL_IMAGE_MODULE_PATH),
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
@classmethod
|
|
434
|
+
def _parse_pil_image(cls, pilimage):
|
|
435
|
+
parsed_value = None
|
|
436
|
+
error_component = None
|
|
437
|
+
import io
|
|
438
|
+
|
|
439
|
+
task_to_dict = TaskToDict()
|
|
440
|
+
_img_type = task_to_dict.object_type(pilimage)
|
|
441
|
+
|
|
442
|
+
if not _img_type.startswith("PIL"):
|
|
443
|
+
return cls._pil_parsing_error(_img_type)
|
|
444
|
+
|
|
445
|
+
# Set the module as a part of the class so that
|
|
446
|
+
# we don't keep reloading the module everytime
|
|
447
|
+
pil_module = cls._get_pil_module()
|
|
448
|
+
|
|
449
|
+
if pil_module is None:
|
|
450
|
+
return parsed_value, ErrorComponent(
|
|
451
|
+
cls.render_fail_headline("PIL cannot be imported"), ""
|
|
452
|
+
)
|
|
453
|
+
if not isinstance(pilimage, pil_module.Image.Image):
|
|
454
|
+
return cls._pil_parsing_error(_img_type)
|
|
455
|
+
|
|
456
|
+
img_byte_arr = io.BytesIO()
|
|
457
|
+
try:
|
|
458
|
+
pilimage.save(img_byte_arr, format="PNG")
|
|
459
|
+
except OSError as e:
|
|
460
|
+
return parsed_value, ErrorComponent(
|
|
461
|
+
cls.render_fail_headline("PIL Image Not Parsable"), "%s" % repr(e)
|
|
462
|
+
)
|
|
463
|
+
img_byte_arr = img_byte_arr.getvalue()
|
|
464
|
+
parsed_value = task_to_dict.parse_image(img_byte_arr)
|
|
465
|
+
return parsed_value, error_component
|
|
466
|
+
|
|
467
|
+
@classmethod
|
|
468
|
+
def _parse_matplotlib(cls, plot):
|
|
469
|
+
import io
|
|
470
|
+
import traceback
|
|
471
|
+
|
|
472
|
+
parsed_value = None
|
|
473
|
+
error_component = None
|
|
474
|
+
pyplt = cls._get_plt_module()
|
|
475
|
+
if pyplt is None:
|
|
476
|
+
return parsed_value, ErrorComponent(
|
|
477
|
+
cls.render_fail_headline("Matplotlib cannot be imported"),
|
|
478
|
+
"%s" % traceback.format_exc(),
|
|
479
|
+
)
|
|
480
|
+
# First check if it is a valid Matplotlib figure.
|
|
481
|
+
figure = None
|
|
482
|
+
if _full_classname(plot) == cls._MATPLOTLIB_FIGURE_MODULE_PATH:
|
|
483
|
+
figure = plot
|
|
484
|
+
|
|
485
|
+
# If it is not valid figure then check if it is matplotlib.axes.Axes or a matplotlib.axes._subplots.AxesSubplot
|
|
486
|
+
# These contain the `get_figure` function to get the main figure object.
|
|
487
|
+
if figure is None:
|
|
488
|
+
if getattr(plot, "get_figure", None) is None:
|
|
489
|
+
return parsed_value, ErrorComponent(
|
|
490
|
+
cls.render_fail_headline(
|
|
491
|
+
"Invalid Type. Object %s is not from `matplotlib`" % type(plot)
|
|
492
|
+
),
|
|
493
|
+
"",
|
|
494
|
+
)
|
|
495
|
+
else:
|
|
496
|
+
figure = plot.get_figure()
|
|
497
|
+
|
|
498
|
+
task_to_dict = TaskToDict()
|
|
499
|
+
img_bytes_arr = io.BytesIO()
|
|
500
|
+
figure.savefig(img_bytes_arr, format="PNG")
|
|
501
|
+
parsed_value = task_to_dict.parse_image(img_bytes_arr.getvalue())
|
|
502
|
+
pyplt.close(figure)
|
|
503
|
+
if parsed_value is not None:
|
|
504
|
+
return parsed_value, error_component
|
|
505
|
+
return parsed_value, ErrorComponent(
|
|
506
|
+
cls.render_fail_headline("Matplotlib plot's image is not parsable"), ""
|
|
507
|
+
)
|
|
508
|
+
|
|
256
509
|
@staticmethod
|
|
257
510
|
def _bytes_to_base64(bytes_arr):
|
|
258
511
|
task_to_dict = TaskToDict()
|
|
@@ -264,7 +517,9 @@ class Image(UserComponent):
|
|
|
264
517
|
return parsed_image
|
|
265
518
|
|
|
266
519
|
@classmethod
|
|
267
|
-
def from_pil_image(
|
|
520
|
+
def from_pil_image(
|
|
521
|
+
cls, pilimage, label: Optional[str] = None, disable_updates: bool = False
|
|
522
|
+
):
|
|
268
523
|
"""
|
|
269
524
|
Create an `Image` from a PIL image.
|
|
270
525
|
|
|
@@ -276,43 +531,29 @@ class Image(UserComponent):
|
|
|
276
531
|
Optional label for the image.
|
|
277
532
|
"""
|
|
278
533
|
try:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
PIL_IMAGE_PATH = "PIL.Image.Image"
|
|
282
|
-
task_to_dict = TaskToDict()
|
|
283
|
-
if task_to_dict.object_type(pilimage) != PIL_IMAGE_PATH:
|
|
284
|
-
return ErrorComponent(
|
|
285
|
-
cls.render_fail_headline(
|
|
286
|
-
"first argument for `Image` should be of type %s"
|
|
287
|
-
% PIL_IMAGE_PATH
|
|
288
|
-
),
|
|
289
|
-
"Type of %s is invalid. Type of %s required"
|
|
290
|
-
% (task_to_dict.object_type(pilimage), PIL_IMAGE_PATH),
|
|
291
|
-
)
|
|
292
|
-
img_byte_arr = io.BytesIO()
|
|
293
|
-
try:
|
|
294
|
-
pilimage.save(img_byte_arr, format="PNG")
|
|
295
|
-
except OSError as e:
|
|
296
|
-
return ErrorComponent(
|
|
297
|
-
cls.render_fail_headline("PIL Image Not Parsable"), "%s" % repr(e)
|
|
298
|
-
)
|
|
299
|
-
img_byte_arr = img_byte_arr.getvalue()
|
|
300
|
-
parsed_image = task_to_dict.parse_image(img_byte_arr)
|
|
534
|
+
parsed_image, error_comp = cls._parse_pil_image(pilimage)
|
|
301
535
|
if parsed_image is not None:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
536
|
+
img = cls(
|
|
537
|
+
src=parsed_image, label=label, disable_updates=disable_updates
|
|
538
|
+
)
|
|
539
|
+
else:
|
|
540
|
+
img = cls(src=None, label=label, disable_updates=disable_updates)
|
|
541
|
+
img._error_comp = error_comp
|
|
542
|
+
return img
|
|
306
543
|
except:
|
|
307
544
|
import traceback
|
|
308
545
|
|
|
309
|
-
|
|
546
|
+
img = cls(src=None, label=label, disable_updates=disable_updates)
|
|
547
|
+
img._error_comp = ErrorComponent(
|
|
310
548
|
cls.render_fail_headline("PIL Image Not Parsable"),
|
|
311
549
|
"%s" % traceback.format_exc(),
|
|
312
550
|
)
|
|
551
|
+
return img
|
|
313
552
|
|
|
314
553
|
@classmethod
|
|
315
|
-
def from_matplotlib(
|
|
554
|
+
def from_matplotlib(
|
|
555
|
+
cls, plot, label: Optional[str] = None, disable_updates: bool = False
|
|
556
|
+
):
|
|
316
557
|
"""
|
|
317
558
|
Create an `Image` from a Matplotlib plot.
|
|
318
559
|
|
|
@@ -323,64 +564,62 @@ class Image(UserComponent):
|
|
|
323
564
|
label : str, optional
|
|
324
565
|
Optional label for the image.
|
|
325
566
|
"""
|
|
326
|
-
import io
|
|
327
|
-
|
|
328
567
|
try:
|
|
329
|
-
|
|
330
|
-
import matplotlib.pyplot as pyplt
|
|
331
|
-
except ImportError:
|
|
332
|
-
return ErrorComponent(
|
|
333
|
-
cls.render_fail_headline("Matplotlib cannot be imported"),
|
|
334
|
-
"%s" % traceback.format_exc(),
|
|
335
|
-
)
|
|
336
|
-
# First check if it is a valid Matplotlib figure.
|
|
337
|
-
figure = None
|
|
338
|
-
if _full_classname(plot) == "matplotlib.figure.Figure":
|
|
339
|
-
figure = plot
|
|
340
|
-
|
|
341
|
-
# If it is not valid figure then check if it is matplotlib.axes.Axes or a matplotlib.axes._subplots.AxesSubplot
|
|
342
|
-
# These contain the `get_figure` function to get the main figure object.
|
|
343
|
-
if figure is None:
|
|
344
|
-
if getattr(plot, "get_figure", None) is None:
|
|
345
|
-
return ErrorComponent(
|
|
346
|
-
cls.render_fail_headline(
|
|
347
|
-
"Invalid Type. Object %s is not from `matplotlib`"
|
|
348
|
-
% type(plot)
|
|
349
|
-
),
|
|
350
|
-
"",
|
|
351
|
-
)
|
|
352
|
-
else:
|
|
353
|
-
figure = plot.get_figure()
|
|
354
|
-
|
|
355
|
-
task_to_dict = TaskToDict()
|
|
356
|
-
img_bytes_arr = io.BytesIO()
|
|
357
|
-
figure.savefig(img_bytes_arr, format="PNG")
|
|
358
|
-
parsed_image = task_to_dict.parse_image(img_bytes_arr.getvalue())
|
|
359
|
-
pyplt.close(figure)
|
|
568
|
+
parsed_image, error_comp = cls._parse_matplotlib(plot)
|
|
360
569
|
if parsed_image is not None:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
570
|
+
img = cls(
|
|
571
|
+
src=parsed_image, label=label, disable_updates=disable_updates
|
|
572
|
+
)
|
|
573
|
+
else:
|
|
574
|
+
img = cls(src=None, label=label, disable_updates=disable_updates)
|
|
575
|
+
img._error_comp = error_comp
|
|
576
|
+
return img
|
|
365
577
|
except:
|
|
366
578
|
import traceback
|
|
367
579
|
|
|
368
|
-
|
|
580
|
+
img = cls(src=None, label=label, disable_updates=disable_updates)
|
|
581
|
+
img._error_comp = ErrorComponent(
|
|
369
582
|
cls.render_fail_headline("Matplotlib plot's image is not parsable"),
|
|
370
583
|
"%s" % traceback.format_exc(),
|
|
371
584
|
)
|
|
585
|
+
return img
|
|
372
586
|
|
|
587
|
+
@with_default_component_id
|
|
373
588
|
@render_safely
|
|
374
589
|
def render(self):
|
|
375
590
|
if self._error_comp is not None:
|
|
376
591
|
return self._error_comp.render()
|
|
377
592
|
|
|
378
593
|
if self._src is not None:
|
|
379
|
-
|
|
594
|
+
img_comp = ImageComponent(src=self._src, label=self._label)
|
|
595
|
+
img_comp.component_id = self.component_id
|
|
596
|
+
return img_comp.render()
|
|
380
597
|
return ErrorComponent(
|
|
381
598
|
self.render_fail_headline("`Image` Component `src` argument is `None`"), ""
|
|
382
599
|
).render()
|
|
383
600
|
|
|
601
|
+
def update(self, image, label=None):
|
|
602
|
+
"""
|
|
603
|
+
Update the image.
|
|
604
|
+
|
|
605
|
+
Parameters
|
|
606
|
+
----------
|
|
607
|
+
image : PIL.Image or matplotlib.figure.Figure or matplotlib.axes.Axes or matplotlib.axes._subplots.AxesSubplot or bytes or str
|
|
608
|
+
The updated image object
|
|
609
|
+
label : str, optional
|
|
610
|
+
Optional label for the image.
|
|
611
|
+
"""
|
|
612
|
+
if not self.REALTIME_UPDATABLE:
|
|
613
|
+
msg = (
|
|
614
|
+
"The `Image` component is disabled for realtime updates. "
|
|
615
|
+
"Please set `disable_updates` to `False` while creating the `Image` object."
|
|
616
|
+
)
|
|
617
|
+
_warning_with_component(self, msg)
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
_label = label if label is not None else self._label
|
|
621
|
+
self._update_image(image, label=_label)
|
|
622
|
+
|
|
384
623
|
|
|
385
624
|
class Error(UserComponent):
|
|
386
625
|
"""
|
|
@@ -434,9 +673,135 @@ class Markdown(UserComponent):
|
|
|
434
673
|
Text formatted in Markdown.
|
|
435
674
|
"""
|
|
436
675
|
|
|
676
|
+
REALTIME_UPDATABLE = True
|
|
677
|
+
|
|
678
|
+
def update(self, text=None):
|
|
679
|
+
self._text = text
|
|
680
|
+
|
|
437
681
|
def __init__(self, text=None):
|
|
438
682
|
self._text = text
|
|
439
683
|
|
|
684
|
+
@with_default_component_id
|
|
685
|
+
@render_safely
|
|
686
|
+
def render(self):
|
|
687
|
+
comp = MarkdownComponent(self._text)
|
|
688
|
+
comp.component_id = self.component_id
|
|
689
|
+
return comp.render()
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
class ProgressBar(UserComponent):
|
|
693
|
+
"""
|
|
694
|
+
A Progress bar for tracking progress of any task.
|
|
695
|
+
|
|
696
|
+
Example:
|
|
697
|
+
```
|
|
698
|
+
progress_bar = ProgressBar(
|
|
699
|
+
max=100,
|
|
700
|
+
label="Progress Bar",
|
|
701
|
+
value=0,
|
|
702
|
+
unit="%",
|
|
703
|
+
metadata="0.1 items/s"
|
|
704
|
+
)
|
|
705
|
+
current.card.append(
|
|
706
|
+
progress_bar
|
|
707
|
+
)
|
|
708
|
+
for i in range(100):
|
|
709
|
+
progress_bar.update(i, metadata="%s items/s" % i)
|
|
710
|
+
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Parameters
|
|
714
|
+
----------
|
|
715
|
+
text : str
|
|
716
|
+
Text formatted in Markdown.
|
|
717
|
+
"""
|
|
718
|
+
|
|
719
|
+
type = "progressBar"
|
|
720
|
+
|
|
721
|
+
REALTIME_UPDATABLE = True
|
|
722
|
+
|
|
723
|
+
def __init__(
|
|
724
|
+
self,
|
|
725
|
+
max: int = 100,
|
|
726
|
+
label: str = None,
|
|
727
|
+
value: int = 0,
|
|
728
|
+
unit: str = None,
|
|
729
|
+
metadata: str = None,
|
|
730
|
+
):
|
|
731
|
+
self._label = label
|
|
732
|
+
self._max = max
|
|
733
|
+
self._value = value
|
|
734
|
+
self._unit = unit
|
|
735
|
+
self._metadata = metadata
|
|
736
|
+
|
|
737
|
+
def update(self, new_value: int, metadata: str = None):
|
|
738
|
+
self._value = new_value
|
|
739
|
+
if metadata is not None:
|
|
740
|
+
self._metadata = metadata
|
|
741
|
+
|
|
742
|
+
@with_default_component_id
|
|
743
|
+
@render_safely
|
|
744
|
+
def render(self):
|
|
745
|
+
data = {
|
|
746
|
+
"type": self.type,
|
|
747
|
+
"id": self.component_id,
|
|
748
|
+
"max": self._max,
|
|
749
|
+
"value": self._value,
|
|
750
|
+
}
|
|
751
|
+
if self._label:
|
|
752
|
+
data["label"] = self._label
|
|
753
|
+
if self._unit:
|
|
754
|
+
data["unit"] = self._unit
|
|
755
|
+
if self._metadata:
|
|
756
|
+
data["details"] = self._metadata
|
|
757
|
+
return data
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
class VegaChart(UserComponent):
|
|
761
|
+
type = "vegaChart"
|
|
762
|
+
|
|
763
|
+
REALTIME_UPDATABLE = True
|
|
764
|
+
|
|
765
|
+
def __init__(self, spec: dict, show_controls=False):
|
|
766
|
+
self._spec = spec
|
|
767
|
+
self._show_controls = show_controls
|
|
768
|
+
self._chart_inside_table = False
|
|
769
|
+
|
|
770
|
+
def update(self, spec=None):
|
|
771
|
+
if spec is not None:
|
|
772
|
+
self._spec = spec
|
|
773
|
+
|
|
774
|
+
@classmethod
|
|
775
|
+
def from_altair_chart(cls, altair_chart):
|
|
776
|
+
from metaflow.plugins.cards.card_modules.convert_to_native_type import (
|
|
777
|
+
_full_classname,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# This will feel slightly hacky but I am unable to find a natural way of determining the class
|
|
781
|
+
# name of the Altair chart. The only way I can think of is to use the full class name and then
|
|
782
|
+
# match with heuristics
|
|
783
|
+
|
|
784
|
+
fulclsname = _full_classname(altair_chart)
|
|
785
|
+
if not all([x in fulclsname for x in ["altair", "vegalite", "Chart"]]):
|
|
786
|
+
raise ValueError(fulclsname + " is not an altair chart")
|
|
787
|
+
|
|
788
|
+
altair_chart_dict = altair_chart.to_dict()
|
|
789
|
+
|
|
790
|
+
cht = cls(spec=altair_chart_dict)
|
|
791
|
+
return cht
|
|
792
|
+
|
|
793
|
+
@with_default_component_id
|
|
440
794
|
@render_safely
|
|
441
795
|
def render(self):
|
|
442
|
-
|
|
796
|
+
data = {
|
|
797
|
+
"type": self.type,
|
|
798
|
+
"id": self.component_id,
|
|
799
|
+
"spec": self._spec,
|
|
800
|
+
}
|
|
801
|
+
if not self._show_controls:
|
|
802
|
+
data["options"] = {"actions": False}
|
|
803
|
+
if "width" not in self._spec and not self._chart_inside_table:
|
|
804
|
+
data["spec"]["width"] = "container"
|
|
805
|
+
if self._chart_inside_table and "autosize" not in self._spec:
|
|
806
|
+
data["spec"]["autosize"] = "fit-x"
|
|
807
|
+
return data
|