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.

Files changed (57) hide show
  1. metaflow/cards.py +2 -0
  2. metaflow/decorators.py +1 -1
  3. metaflow/metaflow_config.py +4 -0
  4. metaflow/plugins/__init__.py +4 -0
  5. metaflow/plugins/airflow/airflow_cli.py +1 -1
  6. metaflow/plugins/argo/argo_workflows.py +5 -0
  7. metaflow/plugins/argo/argo_workflows_cli.py +1 -1
  8. metaflow/plugins/aws/aws_utils.py +1 -1
  9. metaflow/plugins/aws/batch/batch.py +4 -0
  10. metaflow/plugins/aws/batch/batch_cli.py +3 -0
  11. metaflow/plugins/aws/batch/batch_client.py +40 -11
  12. metaflow/plugins/aws/batch/batch_decorator.py +1 -0
  13. metaflow/plugins/aws/step_functions/step_functions.py +1 -0
  14. metaflow/plugins/aws/step_functions/step_functions_cli.py +1 -1
  15. metaflow/plugins/azure/azure_exceptions.py +1 -1
  16. metaflow/plugins/cards/card_cli.py +413 -28
  17. metaflow/plugins/cards/card_client.py +16 -7
  18. metaflow/plugins/cards/card_creator.py +228 -0
  19. metaflow/plugins/cards/card_datastore.py +124 -26
  20. metaflow/plugins/cards/card_decorator.py +40 -86
  21. metaflow/plugins/cards/card_modules/base.html +12 -0
  22. metaflow/plugins/cards/card_modules/basic.py +74 -8
  23. metaflow/plugins/cards/card_modules/bundle.css +1 -170
  24. metaflow/plugins/cards/card_modules/card.py +65 -0
  25. metaflow/plugins/cards/card_modules/components.py +446 -81
  26. metaflow/plugins/cards/card_modules/convert_to_native_type.py +9 -3
  27. metaflow/plugins/cards/card_modules/main.js +250 -21
  28. metaflow/plugins/cards/card_modules/test_cards.py +117 -0
  29. metaflow/plugins/cards/card_resolver.py +0 -2
  30. metaflow/plugins/cards/card_server.py +361 -0
  31. metaflow/plugins/cards/component_serializer.py +506 -42
  32. metaflow/plugins/cards/exception.py +20 -1
  33. metaflow/plugins/datastores/azure_storage.py +1 -2
  34. metaflow/plugins/datastores/gs_storage.py +1 -2
  35. metaflow/plugins/datastores/s3_storage.py +2 -1
  36. metaflow/plugins/datatools/s3/s3.py +24 -11
  37. metaflow/plugins/env_escape/client.py +2 -12
  38. metaflow/plugins/env_escape/client_modules.py +18 -14
  39. metaflow/plugins/env_escape/server.py +18 -11
  40. metaflow/plugins/env_escape/utils.py +12 -0
  41. metaflow/plugins/gcp/gs_exceptions.py +1 -1
  42. metaflow/plugins/gcp/gs_utils.py +1 -1
  43. metaflow/plugins/kubernetes/kubernetes.py +43 -6
  44. metaflow/plugins/kubernetes/kubernetes_cli.py +40 -1
  45. metaflow/plugins/kubernetes/kubernetes_decorator.py +73 -6
  46. metaflow/plugins/kubernetes/kubernetes_job.py +536 -161
  47. metaflow/plugins/pypi/conda_environment.py +5 -6
  48. metaflow/plugins/pypi/pip.py +2 -2
  49. metaflow/plugins/pypi/utils.py +15 -0
  50. metaflow/task.py +1 -0
  51. metaflow/version.py +1 -1
  52. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/METADATA +1 -1
  53. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/RECORD +57 -55
  54. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/LICENSE +0 -0
  55. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/WHEEL +0 -0
  56. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/entry_points.txt +0 -0
  57. {ob_metaflow-2.10.7.4.dist-info → ob_metaflow-2.10.9.2.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,17 @@
1
1
  from .card_modules import MetaflowCardComponent
2
2
  from .card_modules.basic import ErrorComponent, SectionComponent
3
- from .card_modules.components import UserComponent
3
+ from .card_modules.components import (
4
+ UserComponent,
5
+ create_component_id,
6
+ StubComponent,
7
+ )
8
+ from .exception import ComponentOverwriteNotSupportedException
9
+ from metaflow.metaflow_config import RUNTIME_CARD_RENDER_INTERVAL
4
10
  import uuid
5
11
  import json
12
+ import platform
13
+ from collections import OrderedDict
14
+ import time
6
15
 
7
16
  _TYPE = type
8
17
 
@@ -16,11 +25,374 @@ def get_card_class(card_type):
16
25
  return filtered_cards[0]
17
26
 
18
27
 
28
+ def _component_is_valid(component):
29
+ """
30
+ Validates if the component is of the correct class.
31
+ """
32
+ return issubclass(type(component), MetaflowCardComponent)
33
+
34
+
35
+ def warning_message(message, logger=None, ts=False):
36
+ if logger:
37
+ msg = "[@card WARNING] %s" % message
38
+ logger(msg, timestamp=ts, bad=True)
39
+
40
+
19
41
  class WarningComponent(ErrorComponent):
20
42
  def __init__(self, warning_message):
21
43
  super().__init__("@card WARNING", warning_message)
22
44
 
23
45
 
46
+ class ComponentStore:
47
+ """
48
+ The `ComponentStore` object helps store the components for a single card in memory.
49
+ This class has combination of a array/dictionary like interfaces to access/change the stored components.
50
+
51
+ It exposes the `append` /`extend` methods (like an array) to add components.
52
+ It also exposes the `__getitem__`/`__setitem__` methods (like a dictionary) to access the components by their Ids.
53
+ """
54
+
55
+ def _set_component_map(self):
56
+ """
57
+ The `_component_map` attribute is supposed to be a dictionary so that we can access the components by their ids.
58
+ But we also want to maintain order in which components are inserted since all of these components are going to be visible on a UI.
59
+ Since python3.6 dictionaries are ordered by default so we can use the default python `dict`.
60
+ For python3.5 and below we need to use an OrderedDict since `dict`'s are not ordered by default.
61
+ """
62
+ python_version = int(platform.python_version_tuple()[0]) * 10 + int(
63
+ platform.python_version_tuple()[1]
64
+ )
65
+ if python_version < 36:
66
+ self._component_map = OrderedDict()
67
+ else:
68
+ self._component_map = {}
69
+
70
+ def __init__(self, logger, card_type=None, components=None, user_set_id=None):
71
+ self._logger = logger
72
+ self._card_type = card_type
73
+ self._user_set_id = user_set_id
74
+ self._layout_last_changed_on = time.time()
75
+ self._set_component_map()
76
+ if components is not None:
77
+ for c in list(components):
78
+ self._store_component(c, component_id=None)
79
+
80
+ @property
81
+ def layout_last_changed_on(self):
82
+ """This property helps the CardComponentManager identify when the layout of the card has changed so that it can trigger a re-render of the card."""
83
+ return self._layout_last_changed_on
84
+
85
+ def _realtime_updateable_components(self):
86
+ for c in self._component_map.values():
87
+ if c.REALTIME_UPDATABLE:
88
+ yield c
89
+
90
+ def _store_component(self, component, component_id=None):
91
+ if not _component_is_valid(component):
92
+ warning_message(
93
+ "Component (%s) is not a valid MetaflowCardComponent. It will not be stored."
94
+ % str(component),
95
+ self._logger,
96
+ )
97
+ return
98
+ if component_id is not None:
99
+ component.component_id = component_id
100
+ elif component.component_id is None:
101
+ component.component_id = create_component_id(component)
102
+ setattr(component, "_logger", self._logger)
103
+ self._component_map[component.component_id] = component
104
+ self._layout_last_changed_on = time.time()
105
+
106
+ def _remove_component(self, component_id):
107
+ del self._component_map[component_id]
108
+ self._layout_last_changed_on = time.time()
109
+
110
+ def __iter__(self):
111
+ return iter(self._component_map.values())
112
+
113
+ def __setitem__(self, key, value):
114
+ if self._component_map.get(key) is not None:
115
+ # This is the equivalent of calling `current.card.components["mycomponent"] = Markdown("## New Component")`
116
+ # We don't support the replacement of individual components in the card.
117
+ # Instead we support rewriting the entire component array instead.
118
+ # So users can run `current.card[ID] = [FirstComponent, SecondComponent]` which will instantiate an entirely
119
+ # new ComponentStore.
120
+ # So we should throw an error over here, since it is clearly an operation which is not supported.
121
+ raise ComponentOverwriteNotSupportedException(
122
+ key, self._user_set_id, self._card_type
123
+ )
124
+ else:
125
+ self._store_component(value, component_id=key)
126
+
127
+ def __getitem__(self, key):
128
+ if key not in self._component_map:
129
+ # Store a stub-component in place since `key` doesnt exist.
130
+ # If the user does a `current.card.append(component, id=key)`
131
+ # then the stub component will be replaced by the actual component.
132
+ self._store_component(StubComponent(key), component_id=key)
133
+ return self._component_map[key]
134
+
135
+ def __delitem__(self, key):
136
+ if key not in self._component_map:
137
+ raise KeyError(
138
+ "MetaflowCardComponent with id `%s` not found. Available components for the cards include : %s"
139
+ % (key, ", ".join(self.keys()))
140
+ )
141
+ self._remove_component(key)
142
+
143
+ def __contains__(self, key):
144
+ return key in self._component_map
145
+
146
+ def append(self, component, id=None):
147
+ self._store_component(component, component_id=id)
148
+
149
+ def extend(self, components):
150
+ for c in components:
151
+ self._store_component(c, component_id=None)
152
+
153
+ def clear(self):
154
+ self._component_map.clear()
155
+
156
+ def keys(self):
157
+ return list(self._component_map.keys())
158
+
159
+ def values(self):
160
+ return self._component_map.values()
161
+
162
+ def __str__(self):
163
+ return "Card components present in the card: `%s` " % ("`, `".join(self.keys()))
164
+
165
+ def __len__(self):
166
+ return len(self._component_map)
167
+
168
+
169
+ def _object_is_json_serializable(obj):
170
+ try:
171
+ json.dumps(obj)
172
+ return True
173
+ except TypeError as e:
174
+ return False
175
+
176
+
177
+ class CardComponentManager:
178
+ """
179
+ This class manages the card's state for a single card.
180
+ - It uses the `ComponentStore` to manage the storage of the components
181
+ - It exposes methods to add, remove and access the components.
182
+ - It exposes a `refresh` method that will allow refreshing a card with new data
183
+ for realtime(ish) updates.
184
+ - The `CardComponentCollector` exposes convenience methods similar to this class for a default
185
+ editable card. These methods include :
186
+ - `append`
187
+ - `extend`
188
+ - `clear`
189
+ - `refresh`
190
+ - `components`
191
+ - `__iter__`
192
+
193
+ ## Usage Patterns :
194
+
195
+ ```python
196
+ current.card["mycardid"].append(component, id="comp123")
197
+ current.card["mycardid"].extend([component])
198
+ current.card["mycardid"].refresh(data) # refreshes the card with new data
199
+ current.card["mycardid"].components["comp123"] # returns the component with id "comp123"
200
+ current.card["mycardid"].components["comp123"].update()
201
+ current.card["mycardid"].components.clear() # Wipe all the components
202
+ del current.card["mycardid"].components["mycomponentid"] # Delete a component
203
+ ```
204
+ """
205
+
206
+ def __init__(
207
+ self,
208
+ card_uuid,
209
+ decorator_attributes,
210
+ card_creator,
211
+ components=None,
212
+ logger=None,
213
+ no_warnings=False,
214
+ user_set_card_id=None,
215
+ runtime_card=False,
216
+ card_options=None,
217
+ refresh_interval=5,
218
+ ):
219
+ self._card_creator_args = dict(
220
+ card_uuid=card_uuid,
221
+ user_set_card_id=user_set_card_id,
222
+ runtime_card=runtime_card,
223
+ decorator_attributes=decorator_attributes,
224
+ card_options=card_options,
225
+ logger=logger,
226
+ )
227
+ self._card_creator = card_creator
228
+ self._refresh_interval = refresh_interval
229
+ self._last_layout_change = None
230
+ self._latest_user_data = None
231
+ self._last_refresh = 0
232
+ self._last_render = 0
233
+ self._render_seq = 0
234
+ self._logger = logger
235
+ self._no_warnings = no_warnings
236
+ self._warn_once = {
237
+ "update": {},
238
+ "not_implemented": {},
239
+ }
240
+ card_type = decorator_attributes["type"]
241
+
242
+ if components is None:
243
+ self._components = ComponentStore(
244
+ logger=self._logger,
245
+ card_type=card_type,
246
+ user_set_id=user_set_card_id,
247
+ components=None,
248
+ )
249
+ else:
250
+ self._components = ComponentStore(
251
+ logger=self._logger,
252
+ card_type=card_type,
253
+ user_set_id=user_set_card_id,
254
+ components=list(components),
255
+ )
256
+
257
+ def append(self, component, id=None):
258
+ self._components.append(component, id=id)
259
+
260
+ def extend(self, components):
261
+ self._components.extend(components)
262
+
263
+ def clear(self):
264
+ self._components.clear()
265
+
266
+ def _card_proc(self, mode, sync=False):
267
+ self._card_creator.create(**self._card_creator_args, mode=mode, sync=sync)
268
+
269
+ def refresh(self, data=None, force=False):
270
+ self._latest_user_data = data
271
+ nu = time.time()
272
+ first_render = True if self._last_render == 0 else False
273
+
274
+ if nu - self._last_refresh < self._refresh_interval:
275
+ # rate limit refreshes: silently ignore requests that
276
+ # happen too frequently
277
+ return
278
+ self._last_refresh = nu
279
+
280
+ # This block of code will render the card in `render_runtime` mode when:
281
+ # 1. refresh is called with `force=True`
282
+ # 2. Layout of the components in the card has changed. i.e. The actual elements in the component array have changed.
283
+ # 3. The last time the card was rendered was more the minimum interval after which they should be rendered.
284
+ last_rendered_before_minimum_interval = (
285
+ nu - self._last_refresh
286
+ ) > RUNTIME_CARD_RENDER_INTERVAL
287
+ layout_has_changed = (
288
+ self._last_layout_change != self.components.layout_last_changed_on
289
+ or self._last_layout_change is None
290
+ )
291
+ if force or last_rendered_before_minimum_interval or layout_has_changed:
292
+ self._render_seq += 1
293
+ self._last_render = nu
294
+ self._card_proc("render_runtime")
295
+ # The below `if not first_render` condition is a special case for the following scenario:
296
+ # Lets assume the case that the user is only doing `current.card.append` followed by `refresh`.
297
+ # In this case, there will be no process executed in `refresh` mode since `layout_has_changed`
298
+ # will always be true and as a result there will be no data update that informs the UI of the RELOAD_TOKEN change.
299
+ # This will cause the UI to seek for the data update object but will constantly find None. So if it is not
300
+ # the first render then we should also have a `refresh` call followed by a `render_runtime` call so
301
+ # that the UI can always be updated with the latest data.
302
+ if not first_render:
303
+ # For the general case, the CardCreator's ProcessManager run's the `refresh` / `render_runtime` in a asynchronous manner.
304
+ # Due to this when the `render_runtime` call is happening, an immediately subsequent call to `refresh` will not be able to
305
+ # execute since the card-process manager will be busy executing the `render_runtime` call and ignore the `refresh` call.
306
+ # Hence we need to pass the `sync=True` argument to the `refresh` call so that the `refresh` call is executed synchronously and waits for the
307
+ # `render_runtime` call to finish.
308
+ self._card_proc("refresh", sync=True)
309
+ # We set self._last_layout_change so that when self._last_layout_change is not the same
310
+ # as `self.components.layout_last_changed_on`, then the component array itself
311
+ # has been modified. So we should force a re-render of the card.
312
+ self._last_layout_change = self.components.layout_last_changed_on
313
+ else:
314
+ self._card_proc("refresh")
315
+
316
+ @property
317
+ def components(self):
318
+ return self._components
319
+
320
+ def _warning(self, message):
321
+ msg = "[@card WARNING] %s" % message
322
+ self._logger(msg, timestamp=False, bad=True)
323
+
324
+ def _get_latest_data(self, final=False, mode=None):
325
+ """
326
+ This function returns the data object that is passed down to :
327
+ - `MetaflowCard.render_runtime`
328
+ - `MetaflowCard.refresh`
329
+ - `MetaflowCard.reload_content_token`
330
+
331
+ The return value of this function contains all the necessary state information for Metaflow Cards to make decisions on the following:
332
+ 1. What components are rendered
333
+ 2. Should the card be reloaded on the UI
334
+ 3. What data to pass down to the card.
335
+
336
+ Parameters
337
+ ----------
338
+ final : bool, optional
339
+ If True, it implies that the final "rendering" sequence is taking place (which involves calling a `render` and a `refresh` function.)
340
+ When final is set the `render_seq` is set to "final" so that the reload token in the card is set to final
341
+ and the card is not reloaded again on the user interface.
342
+ mode : str
343
+ This parameter is passed down to the object returned by this function. Can be one of `render_runtime` / `refresh` / `render`
344
+
345
+ Returns
346
+ -------
347
+ dict
348
+ A dictionary of the form :
349
+ ```python
350
+ {
351
+ "user": user_data, # any passed to `current.card.refresh` function
352
+ "components": component_dict, # all rendered REALTIME_UPDATABLE components
353
+ "render_seq": seq,
354
+ # `render_seq` is a counter that is incremented every time `render_runtime` is called.
355
+ # If a metaflow card has a RELOAD_POLICY_ALWAYS set then the reload token will be set to this value
356
+ # so that the card reload on the UI everytime `render_runtime` is called.
357
+ "component_update_ts": self.components.layout_last_changed_on,
358
+ # `component_update_ts` is the timestamp of the last time the component array was modified.
359
+ # `component_update_ts` can get used by the `reload_content_token` to make decisions on weather to
360
+ # reload the card on the UI when component array has changed.
361
+ "mode": mode,
362
+ }
363
+ ```
364
+ """
365
+ seq = "final" if final else self._render_seq
366
+ # Extract all the runtime-updatable components as a dictionary
367
+ component_dict = {}
368
+ for component in self._components._realtime_updateable_components():
369
+ rendered_comp = _render_card_component(component)
370
+ if rendered_comp is not None:
371
+ component_dict.update({component.component_id: rendered_comp})
372
+
373
+ # Verify _latest_user_data is json serializable
374
+ user_data = {}
375
+ if self._latest_user_data is not None and not _object_is_json_serializable(
376
+ self._latest_user_data
377
+ ):
378
+ self._warning(
379
+ "Data provided to `refresh` is not JSON serializable. It will be ignored."
380
+ )
381
+ else:
382
+ user_data = self._latest_user_data
383
+
384
+ return {
385
+ "user": user_data,
386
+ "components": component_dict,
387
+ "render_seq": seq,
388
+ "component_update_ts": self.components.layout_last_changed_on,
389
+ "mode": mode,
390
+ }
391
+
392
+ def __iter__(self):
393
+ return iter(self._components)
394
+
395
+
24
396
  class CardComponentCollector:
25
397
  """
26
398
  This class helps collect `MetaflowCardComponent`s during runtime execution
@@ -42,25 +414,34 @@ class CardComponentCollector:
42
414
  - [x] by looking it up by its type, e.g. `current.card.get(type='pytorch')`.
43
415
  """
44
416
 
45
- def __init__(self, logger=None):
417
+ def __init__(self, logger=None, card_creator=None):
46
418
  from metaflow.metaflow_config import CARD_NO_WARNING
47
419
 
48
- self._cards_components = (
420
+ self._card_component_store = (
421
+ # Each key in the dictionary is the UUID of an individual card.
422
+ # value is of type `CardComponentManager`, holding a list of MetaflowCardComponents for that particular card
49
423
  {}
50
- ) # a dict with key as uuid and value as a list of MetaflowCardComponent.
424
+ )
51
425
  self._cards_meta = (
52
426
  {}
53
427
  ) # a `dict` of (card_uuid, `dict)` holding all metadata about all @card decorators on the `current` @step.
54
428
  self._card_id_map = {} # card_id to uuid map for all cards with ids
55
429
  self._logger = logger
430
+ self._card_creator = card_creator
56
431
  # `self._default_editable_card` holds the uuid of the card that is default editable. This card has access to `append`/`extend` methods of `self`
57
432
  self._default_editable_card = None
58
- self._warned_once = {"__getitem__": {}, "append": False, "extend": False}
433
+ self._warned_once = {
434
+ "__getitem__": {},
435
+ "append": False,
436
+ "extend": False,
437
+ "update": False,
438
+ "update_no_id": False,
439
+ }
59
440
  self._no_warnings = True if CARD_NO_WARNING else False
60
441
 
61
442
  @staticmethod
62
443
  def create_uuid():
63
- return str(uuid.uuid4())
444
+ return str(uuid.uuid4()).replace("-", "")
64
445
 
65
446
  def _log(self, *args, **kwargs):
66
447
  if self._logger:
@@ -70,9 +451,13 @@ class CardComponentCollector:
70
451
  self,
71
452
  card_type,
72
453
  card_id,
454
+ decorator_attributes,
455
+ card_options,
73
456
  editable=False,
74
457
  customize=False,
75
458
  suppress_warnings=False,
459
+ runtime_card=False,
460
+ refresh_interval=5,
76
461
  ):
77
462
  """
78
463
  This function helps collect cards from all the card decorators.
@@ -95,9 +480,24 @@ class CardComponentCollector:
95
480
  editable=editable,
96
481
  customize=customize,
97
482
  suppress_warnings=suppress_warnings,
483
+ runtime_card=runtime_card,
484
+ decorator_attributes=decorator_attributes,
485
+ card_options=card_options,
486
+ refresh_interval=refresh_interval,
98
487
  )
99
488
  self._cards_meta[card_uuid] = card_metadata
100
- self._cards_components[card_uuid] = []
489
+ self._card_component_store[card_uuid] = CardComponentManager(
490
+ card_uuid,
491
+ decorator_attributes,
492
+ self._card_creator,
493
+ components=None,
494
+ logger=self._logger,
495
+ no_warnings=self._no_warnings,
496
+ user_set_card_id=card_id,
497
+ runtime_card=runtime_card,
498
+ card_options=card_options,
499
+ refresh_interval=refresh_interval,
500
+ )
101
501
  return card_metadata
102
502
 
103
503
  def _warning(self, message):
@@ -107,9 +507,9 @@ class CardComponentCollector:
107
507
  def _add_warning_to_cards(self, warn_msg):
108
508
  if self._no_warnings:
109
509
  return
110
- for card_id in self._cards_components:
510
+ for card_id in self._card_component_store:
111
511
  if not self._cards_meta[card_id]["suppress_warnings"]:
112
- self._cards_components[card_id].append(WarningComponent(warn_msg))
512
+ self._card_component_store[card_id].append(WarningComponent(warn_msg))
113
513
 
114
514
  def get(self, type=None):
115
515
  """`get`
@@ -128,7 +528,7 @@ class CardComponentCollector:
128
528
  for card_meta in self._cards_meta.values()
129
529
  if card_meta["type"] == card_type
130
530
  ]
131
- return [self._cards_components[uuid] for uuid in card_uuids]
531
+ return [self._card_component_store[uuid] for uuid in card_uuids]
132
532
 
133
533
  def _finalize(self):
134
534
  """
@@ -229,13 +629,13 @@ class CardComponentCollector:
229
629
 
230
630
  Returns
231
631
  -------
232
- CardComponentCollector
632
+ CardComponentManager
233
633
  An object with `append` and `extend` calls which allow you to
234
634
  add components to the chosen card.
235
635
  """
236
636
  if key in self._card_id_map:
237
637
  card_uuid = self._card_id_map[key]
238
- return self._cards_components[card_uuid]
638
+ return self._card_component_store[card_uuid]
239
639
  if key not in self._warned_once["__getitem__"]:
240
640
  _warn_msg = [
241
641
  "`current.card['%s']` is not present. Please set the `id` argument in @card to '%s' to access `current.card['%s']`."
@@ -246,6 +646,7 @@ class CardComponentCollector:
246
646
  self._warning(" ".join(_warn_msg))
247
647
  self._add_warning_to_cards("\n".join(_warn_msg))
248
648
  self._warned_once["__getitem__"][key] = True
649
+
249
650
  return []
250
651
 
251
652
  def __setitem__(self, key, value):
@@ -263,7 +664,7 @@ class CardComponentCollector:
263
664
  key: str
264
665
  Card ID.
265
666
 
266
- value: List[CardComponent]
667
+ value: List[MetaflowCardComponent]
267
668
  List of card components to assign to this card.
268
669
  """
269
670
  if key in self._card_id_map:
@@ -275,7 +676,18 @@ class CardComponentCollector:
275
676
  )
276
677
  self._warning(_warning_msg)
277
678
  return
278
- self._cards_components[card_uuid] = value
679
+ self._card_component_store[card_uuid] = CardComponentManager(
680
+ card_uuid,
681
+ self._cards_meta[card_uuid]["decorator_attributes"],
682
+ self._card_creator,
683
+ components=value,
684
+ logger=self._logger,
685
+ no_warnings=self._no_warnings,
686
+ user_set_card_id=key,
687
+ card_options=self._cards_meta[card_uuid]["card_options"],
688
+ runtime_card=self._cards_meta[card_uuid]["runtime_card"],
689
+ refresh_interval=self._cards_meta[card_uuid]["refresh_interval"],
690
+ )
279
691
  return
280
692
 
281
693
  self._warning(
@@ -283,18 +695,18 @@ class CardComponentCollector:
283
695
  % (key, key, key)
284
696
  )
285
697
 
286
- def append(self, component):
698
+ def append(self, component, id=None):
287
699
  """
288
700
  Appends a component to the current card.
289
701
 
290
702
  Parameters
291
703
  ----------
292
- component : CardComponent
704
+ component : MetaflowCardComponent
293
705
  Card component to add to this card.
294
706
  """
295
707
  if self._default_editable_card is None:
296
708
  if (
297
- len(self._cards_components) == 1
709
+ len(self._card_component_store) == 1
298
710
  ): # if there is one card which is not the _default_editable_card then the card is not editable
299
711
  card_type = list(self._cards_meta.values())[0]["type"]
300
712
  if list(self._cards_meta.values())[0]["exists"]:
@@ -324,7 +736,7 @@ class CardComponentCollector:
324
736
  self._warned_once["append"] = True
325
737
 
326
738
  return
327
- self._cards_components[self._default_editable_card].append(component)
739
+ self._card_component_store[self._default_editable_card].append(component, id=id)
328
740
 
329
741
  def extend(self, components):
330
742
  """
@@ -332,12 +744,12 @@ class CardComponentCollector:
332
744
 
333
745
  Parameters
334
746
  ----------
335
- component : Iterator[CardComponent]
747
+ component : Iterator[MetaflowCardComponent]
336
748
  Card components to add to this card.
337
749
  """
338
750
  if self._default_editable_card is None:
339
751
  # if there is one card which is not the _default_editable_card then the card is not editable
340
- if len(self._cards_components) == 1:
752
+ if len(self._card_component_store) == 1:
341
753
  card_type = list(self._cards_meta.values())[0]["type"]
342
754
  _warning_msg = [
343
755
  "Card of type `%s` is not an editable card." % card_type,
@@ -357,7 +769,52 @@ class CardComponentCollector:
357
769
 
358
770
  return
359
771
 
360
- self._cards_components[self._default_editable_card].extend(components)
772
+ self._card_component_store[self._default_editable_card].extend(components)
773
+
774
+ @property
775
+ def components(self):
776
+ # FIXME: document
777
+ if self._default_editable_card is None:
778
+ if len(self._card_component_store) == 1:
779
+ card_type = list(self._cards_meta.values())[0]["type"]
780
+ _warning_msg = [
781
+ "Card of type `%s` is not an editable card." % card_type,
782
+ "Components list will not be updated and `current.card.components` will not work for any call during this runtime execution.",
783
+ "Please use an editable card", # todo : link to documentation
784
+ ]
785
+ else:
786
+ _warning_msg = [
787
+ "`current.card.components` cannot disambiguate between multiple @card decorators.",
788
+ "Components list will not be accessible and `current.card.components` will not work for any call during this runtime execution.",
789
+ "To fix this set the `id` argument in all @card when using multiple @card decorators over a single @step and reference `current.card[ID].components`", # todo : Add Link to documentation
790
+ "to update/access the appropriate card component.",
791
+ ]
792
+ if not self._warned_once["components"]:
793
+ self._warning(" ".join(_warning_msg))
794
+ self._warned_once["components"] = True
795
+ return
796
+
797
+ return self._card_component_store[self._default_editable_card].components
798
+
799
+ def clear(self):
800
+ # FIXME: document
801
+ if self._default_editable_card is not None:
802
+ self._card_component_store[self._default_editable_card].clear()
803
+
804
+ def refresh(self, *args, **kwargs):
805
+ # FIXME: document
806
+ if self._default_editable_card is not None:
807
+ self._card_component_store[self._default_editable_card].refresh(
808
+ *args, **kwargs
809
+ )
810
+
811
+ def _get_latest_data(self, card_uuid, final=False, mode=None):
812
+ """
813
+ Returns latest data so it can be used in the final render() call
814
+ """
815
+ return self._card_component_store[card_uuid]._get_latest_data(
816
+ final=final, mode=mode
817
+ )
361
818
 
362
819
  def _serialize_components(self, card_uuid):
363
820
  """
@@ -366,36 +823,43 @@ class CardComponentCollector:
366
823
  don't render safely then we don't add them to the final list of serialized functions
367
824
  """
368
825
  serialized_components = []
369
- if card_uuid not in self._cards_components:
826
+ if card_uuid not in self._card_component_store:
370
827
  return []
371
828
  has_user_components = any(
372
829
  [
373
830
  issubclass(type(component), UserComponent)
374
- for component in self._cards_components[card_uuid]
831
+ for component in self._card_component_store[card_uuid]
375
832
  ]
376
833
  )
377
- for component in self._cards_components[card_uuid]:
378
- if not issubclass(type(component), MetaflowCardComponent):
379
- continue
380
- try:
381
- rendered_obj = component.render()
382
- except:
834
+ for component in self._card_component_store[card_uuid]:
835
+ rendered_obj = _render_card_component(component)
836
+ if rendered_obj is None:
383
837
  continue
384
- else:
385
- if not (type(rendered_obj) == str or type(rendered_obj) == dict):
386
- continue
387
- else:
388
- # Since `UserComponent`s are safely_rendered using render_tools.py
389
- # we don't need to check JSON serialization as @render_tools.render_safely
390
- # decorator ensures this check so there is no need to re-serialize
391
- if not issubclass(type(component), UserComponent):
392
- try: # check if rendered object is json serializable.
393
- json.dumps(rendered_obj)
394
- except (TypeError, OverflowError) as e:
395
- continue
396
- serialized_components.append(rendered_obj)
838
+ serialized_components.append(rendered_obj)
397
839
  if has_user_components and len(serialized_components) > 0:
398
840
  serialized_components = [
399
841
  SectionComponent(contents=serialized_components).render()
400
842
  ]
401
843
  return serialized_components
844
+
845
+
846
+ def _render_card_component(component):
847
+ if not _component_is_valid(component):
848
+ return None
849
+ try:
850
+ rendered_obj = component.render()
851
+ except:
852
+ return None
853
+ else:
854
+ if not (type(rendered_obj) == str or type(rendered_obj) == dict):
855
+ return None
856
+ else:
857
+ # Since `UserComponent`s are safely_rendered using render_tools.py
858
+ # we don't need to check JSON serialization as @render_tools.render_safely
859
+ # decorator ensures this check so there is no need to re-serialize
860
+ if not issubclass(type(component), UserComponent):
861
+ try: # check if rendered object is json serializable.
862
+ json.dumps(rendered_obj)
863
+ except (TypeError, OverflowError) as e:
864
+ return None
865
+ return rendered_obj