ob-metaflow 2.10.7.4__py2.py3-none-any.whl → 2.10.9.1__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.

Files changed (52) hide show
  1. metaflow/cards.py +2 -0
  2. metaflow/decorators.py +1 -1
  3. metaflow/metaflow_config.py +2 -0
  4. metaflow/plugins/__init__.py +4 -0
  5. metaflow/plugins/airflow/airflow_cli.py +1 -1
  6. metaflow/plugins/argo/argo_workflows_cli.py +1 -1
  7. metaflow/plugins/aws/aws_utils.py +1 -1
  8. metaflow/plugins/aws/batch/batch.py +4 -0
  9. metaflow/plugins/aws/batch/batch_cli.py +3 -0
  10. metaflow/plugins/aws/batch/batch_client.py +40 -11
  11. metaflow/plugins/aws/batch/batch_decorator.py +1 -0
  12. metaflow/plugins/aws/step_functions/step_functions.py +1 -0
  13. metaflow/plugins/aws/step_functions/step_functions_cli.py +1 -1
  14. metaflow/plugins/azure/azure_exceptions.py +1 -1
  15. metaflow/plugins/cards/card_cli.py +413 -28
  16. metaflow/plugins/cards/card_client.py +16 -7
  17. metaflow/plugins/cards/card_creator.py +228 -0
  18. metaflow/plugins/cards/card_datastore.py +124 -26
  19. metaflow/plugins/cards/card_decorator.py +40 -86
  20. metaflow/plugins/cards/card_modules/base.html +12 -0
  21. metaflow/plugins/cards/card_modules/basic.py +74 -8
  22. metaflow/plugins/cards/card_modules/bundle.css +1 -170
  23. metaflow/plugins/cards/card_modules/card.py +65 -0
  24. metaflow/plugins/cards/card_modules/components.py +446 -81
  25. metaflow/plugins/cards/card_modules/convert_to_native_type.py +9 -3
  26. metaflow/plugins/cards/card_modules/main.js +250 -21
  27. metaflow/plugins/cards/card_modules/test_cards.py +117 -0
  28. metaflow/plugins/cards/card_resolver.py +0 -2
  29. metaflow/plugins/cards/card_server.py +361 -0
  30. metaflow/plugins/cards/component_serializer.py +506 -42
  31. metaflow/plugins/cards/exception.py +20 -1
  32. metaflow/plugins/datastores/azure_storage.py +1 -2
  33. metaflow/plugins/datastores/gs_storage.py +1 -2
  34. metaflow/plugins/datastores/s3_storage.py +2 -1
  35. metaflow/plugins/datatools/s3/s3.py +24 -11
  36. metaflow/plugins/env_escape/client.py +2 -12
  37. metaflow/plugins/env_escape/client_modules.py +18 -14
  38. metaflow/plugins/env_escape/server.py +18 -11
  39. metaflow/plugins/env_escape/utils.py +12 -0
  40. metaflow/plugins/gcp/gs_exceptions.py +1 -1
  41. metaflow/plugins/gcp/gs_utils.py +1 -1
  42. metaflow/plugins/pypi/conda_environment.py +5 -6
  43. metaflow/plugins/pypi/pip.py +2 -2
  44. metaflow/plugins/pypi/utils.py +15 -0
  45. metaflow/task.py +1 -0
  46. metaflow/version.py +1 -1
  47. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.1.dist-info}/METADATA +1 -1
  48. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.1.dist-info}/RECORD +52 -50
  49. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.1.dist-info}/LICENSE +0 -0
  50. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.1.dist-info}/WHEEL +0 -0
  51. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.1.dist-info}/entry_points.txt +0 -0
  52. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.1.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
- pass
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
- return ArtifactsComponent(data=[artifact]).render()
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(cls, dataframe=None, truncate: bool = True):
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(data=table_data["data"], headers=table_data["headers"])
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
- return TableComponent(
240
+ table_component = TableComponent(
161
241
  headers=self._headers, data=self._render_subcomponents()
162
- ).render()
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 __init__(self, src=None, label=None):
221
- self._error_comp = None
338
+ def _set_image_src(self, src, label=None):
222
339
  self._label = label
223
-
224
- if type(src) is not str:
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
- "first argument should be of type `bytes` or valid image base64 string"
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
- "first argument should be of type `bytes` or valid image base64 string"
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(cls, pilimage, label: Optional[str] = None):
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
- import io
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
- return cls(src=parsed_image, label=label)
303
- return ErrorComponent(
304
- cls.render_fail_headline("PIL Image Not Parsable"), ""
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
- return ErrorComponent(
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(cls, plot, label: Optional[str] = None):
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
- try:
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
- return cls(src=parsed_image, label=label)
362
- return ErrorComponent(
363
- cls.render_fail_headline("Matplotlib plot's image is not parsable"), ""
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
- return ErrorComponent(
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
- return ImageComponent(src=self._src, label=self._label).render()
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
- return MarkdownComponent(self._text).render()
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