ob-metaflow 2.13.2.1__py2.py3-none-any.whl → 2.13.6.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.
- metaflow/cards.py +1 -0
- metaflow/cli.py +5 -0
- metaflow/cli_components/run_cmds.py +2 -0
- metaflow/cli_components/step_cmd.py +0 -13
- metaflow/decorators.py +2 -2
- metaflow/parameters.py +3 -1
- metaflow/plugins/__init__.py +5 -0
- metaflow/plugins/argo/argo_workflows.py +24 -8
- metaflow/plugins/cards/card_cli.py +7 -2
- metaflow/plugins/cards/card_decorator.py +53 -20
- metaflow/plugins/cards/card_modules/basic.py +56 -5
- metaflow/plugins/cards/card_modules/card.py +16 -1
- metaflow/plugins/cards/card_modules/components.py +64 -16
- metaflow/plugins/cards/card_modules/main.js +27 -25
- metaflow/plugins/cards/card_modules/test_cards.py +4 -4
- metaflow/plugins/cards/component_serializer.py +1 -1
- metaflow/plugins/events_decorator.py +120 -149
- metaflow/plugins/kubernetes/kubernetes.py +0 -9
- metaflow/plugins/kubernetes/kubernetes_cli.py +1 -1
- metaflow/plugins/kubernetes/kubernetes_decorator.py +8 -0
- metaflow/plugins/kubernetes/spot_metadata_cli.py +69 -0
- metaflow/plugins/kubernetes/spot_monitor_sidecar.py +109 -0
- metaflow/plugins/project_decorator.py +33 -5
- metaflow/user_configs/config_parameters.py +23 -9
- metaflow/version.py +1 -1
- {ob_metaflow-2.13.2.1.dist-info → ob_metaflow-2.13.6.1.dist-info}/METADATA +11 -3
- {ob_metaflow-2.13.2.1.dist-info → ob_metaflow-2.13.6.1.dist-info}/RECORD +31 -29
- {ob_metaflow-2.13.2.1.dist-info → ob_metaflow-2.13.6.1.dist-info}/WHEEL +1 -1
- {ob_metaflow-2.13.2.1.dist-info → ob_metaflow-2.13.6.1.dist-info}/LICENSE +0 -0
- {ob_metaflow-2.13.2.1.dist-info → ob_metaflow-2.13.6.1.dist-info}/entry_points.txt +0 -0
- {ob_metaflow-2.13.2.1.dist-info → ob_metaflow-2.13.6.1.dist-info}/top_level.txt +0 -0
metaflow/cards.py
CHANGED
metaflow/cli.py
CHANGED
|
@@ -504,6 +504,11 @@ def start(
|
|
|
504
504
|
# *after* the run decospecs so that they don't take precedence. In other
|
|
505
505
|
# words, for the same decorator, we want `myflow.py run --with foo` to
|
|
506
506
|
# take precedence over any other `foo` decospec
|
|
507
|
+
|
|
508
|
+
# Note that top-level decospecs are used primarily with non run/resume
|
|
509
|
+
# options as well as with the airflow/argo/sfn integrations which pass
|
|
510
|
+
# all the decospecs (the ones from top-level but also the ones from the
|
|
511
|
+
# run/resume level) through the tl decospecs.
|
|
507
512
|
ctx.obj.tl_decospecs = list(decospecs or [])
|
|
508
513
|
|
|
509
514
|
# initialize current and parameter context for deploy-time parameters
|
|
@@ -39,6 +39,8 @@ def before_run(obj, tags, decospecs):
|
|
|
39
39
|
+ list(obj.environment.decospecs() or [])
|
|
40
40
|
)
|
|
41
41
|
if all_decospecs:
|
|
42
|
+
# These decospecs are the ones from run/resume PLUS the ones from the
|
|
43
|
+
# environment (for example the @conda)
|
|
42
44
|
decorators._attach_decorators(obj.flow, all_decospecs)
|
|
43
45
|
decorators._init(obj.flow)
|
|
44
46
|
# Regenerate graph if we attached more decorators
|
|
@@ -77,14 +77,6 @@ from ..util import decompress_list
|
|
|
77
77
|
default=None,
|
|
78
78
|
help="Run id of the origin flow, if this task is part of a flow being resumed.",
|
|
79
79
|
)
|
|
80
|
-
@click.option(
|
|
81
|
-
"--with",
|
|
82
|
-
"decospecs",
|
|
83
|
-
multiple=True,
|
|
84
|
-
help="Add a decorator to this task. You can specify this "
|
|
85
|
-
"option multiple times to attach multiple decorators "
|
|
86
|
-
"to this task.",
|
|
87
|
-
)
|
|
88
80
|
@click.option(
|
|
89
81
|
"--ubf-context",
|
|
90
82
|
default="none",
|
|
@@ -112,7 +104,6 @@ def step(
|
|
|
112
104
|
max_user_code_retries=None,
|
|
113
105
|
clone_only=None,
|
|
114
106
|
clone_run_id=None,
|
|
115
|
-
decospecs=None,
|
|
116
107
|
ubf_context="none",
|
|
117
108
|
num_parallel=None,
|
|
118
109
|
):
|
|
@@ -136,10 +127,6 @@ def step(
|
|
|
136
127
|
raise CommandException("Function *%s* is not a step." % step_name)
|
|
137
128
|
echo("Executing a step, *%s*" % step_name, fg="magenta", bold=False)
|
|
138
129
|
|
|
139
|
-
if decospecs:
|
|
140
|
-
decorators._attach_decorators_to_step(func, decospecs)
|
|
141
|
-
decorators._init(ctx.obj.flow)
|
|
142
|
-
|
|
143
130
|
step_kwargs = ctx.params
|
|
144
131
|
# Remove argument `step_name` from `step_kwargs`.
|
|
145
132
|
step_kwargs.pop("step_name", None)
|
metaflow/decorators.py
CHANGED
|
@@ -150,8 +150,8 @@ class Decorator(object):
|
|
|
150
150
|
return
|
|
151
151
|
|
|
152
152
|
# Note that by design, later values override previous ones.
|
|
153
|
-
self.attributes = unpack_delayed_evaluator(self.attributes)
|
|
154
|
-
self._user_defined_attributes.update(
|
|
153
|
+
self.attributes, new_user_attributes = unpack_delayed_evaluator(self.attributes)
|
|
154
|
+
self._user_defined_attributes.update(new_user_attributes)
|
|
155
155
|
self.attributes = resolve_delayed_evaluator(self.attributes)
|
|
156
156
|
|
|
157
157
|
self._ran_init = True
|
metaflow/parameters.py
CHANGED
|
@@ -367,7 +367,9 @@ class Parameter(object):
|
|
|
367
367
|
)
|
|
368
368
|
|
|
369
369
|
# Resolve any value from configurations
|
|
370
|
-
self.kwargs = unpack_delayed_evaluator(
|
|
370
|
+
self.kwargs, _ = unpack_delayed_evaluator(
|
|
371
|
+
self.kwargs, ignore_errors=ignore_errors
|
|
372
|
+
)
|
|
371
373
|
# Do it one item at a time so errors are ignored at that level (as opposed to
|
|
372
374
|
# at the entire kwargs level)
|
|
373
375
|
self.kwargs = {
|
metaflow/plugins/__init__.py
CHANGED
|
@@ -16,6 +16,7 @@ CLIS_DESC = [
|
|
|
16
16
|
("argo-workflows", ".argo.argo_workflows_cli.cli"),
|
|
17
17
|
("card", ".cards.card_cli.cli"),
|
|
18
18
|
("tag", ".tag_cli.cli"),
|
|
19
|
+
("spot-metadata", ".kubernetes.spot_metadata_cli.cli"),
|
|
19
20
|
("logs", ".logs_cli.cli"),
|
|
20
21
|
]
|
|
21
22
|
|
|
@@ -104,6 +105,10 @@ SIDECARS_DESC = [
|
|
|
104
105
|
"save_logs_periodically",
|
|
105
106
|
"..mflog.save_logs_periodically.SaveLogsPeriodicallySidecar",
|
|
106
107
|
),
|
|
108
|
+
(
|
|
109
|
+
"spot_termination_monitor",
|
|
110
|
+
".kubernetes.spot_monitor_sidecar.SpotTerminationMonitorSidecar",
|
|
111
|
+
),
|
|
107
112
|
("heartbeat", "metaflow.metadata_provider.heartbeat.MetadataHeartBeat"),
|
|
108
113
|
]
|
|
109
114
|
|
|
@@ -625,6 +625,16 @@ class ArgoWorkflows(object):
|
|
|
625
625
|
for event in trigger_on_finish_deco.triggers:
|
|
626
626
|
# Actual filters are deduced here since we don't have access to
|
|
627
627
|
# the current object in the @trigger_on_finish decorator.
|
|
628
|
+
project_name = event.get("project") or current.get("project_name")
|
|
629
|
+
branch_name = event.get("branch") or current.get("branch_name")
|
|
630
|
+
# validate that we have complete project info for an event name
|
|
631
|
+
if project_name or branch_name:
|
|
632
|
+
if not (project_name and branch_name):
|
|
633
|
+
# if one of the two is missing, we would end up listening to an event that will never be broadcast.
|
|
634
|
+
raise ArgoWorkflowsException(
|
|
635
|
+
"Incomplete project info. Please specify both 'project' and 'project_branch' or use the @project decorator"
|
|
636
|
+
)
|
|
637
|
+
|
|
628
638
|
triggers.append(
|
|
629
639
|
{
|
|
630
640
|
# Make sure this remains consistent with the event name format
|
|
@@ -633,18 +643,16 @@ class ArgoWorkflows(object):
|
|
|
633
643
|
% ".".join(
|
|
634
644
|
v
|
|
635
645
|
for v in [
|
|
636
|
-
|
|
637
|
-
|
|
646
|
+
project_name,
|
|
647
|
+
branch_name,
|
|
638
648
|
event["flow"],
|
|
639
649
|
]
|
|
640
650
|
if v
|
|
641
651
|
),
|
|
642
652
|
"filters": {
|
|
643
653
|
"auto-generated-by-metaflow": True,
|
|
644
|
-
"project_name":
|
|
645
|
-
|
|
646
|
-
"branch_name": event.get("branch")
|
|
647
|
-
or current.get("branch_name"),
|
|
654
|
+
"project_name": project_name,
|
|
655
|
+
"branch_name": branch_name,
|
|
648
656
|
# TODO: Add a time filters to guard against cached events
|
|
649
657
|
},
|
|
650
658
|
"type": "run",
|
|
@@ -842,8 +850,15 @@ class ArgoWorkflows(object):
|
|
|
842
850
|
Metadata()
|
|
843
851
|
.labels(self._base_labels)
|
|
844
852
|
.label("app.kubernetes.io/name", "metaflow-task")
|
|
845
|
-
.annotations(
|
|
846
|
-
|
|
853
|
+
.annotations(
|
|
854
|
+
{
|
|
855
|
+
**annotations,
|
|
856
|
+
**self._base_annotations,
|
|
857
|
+
**{
|
|
858
|
+
"metaflow/run_id": "argo-{{workflow.name}}"
|
|
859
|
+
}, # we want pods of the workflow to have the run_id as an annotation as well
|
|
860
|
+
}
|
|
861
|
+
)
|
|
847
862
|
)
|
|
848
863
|
# Set the entrypoint to flow name
|
|
849
864
|
.entrypoint(self.flow.name)
|
|
@@ -1706,6 +1721,7 @@ class ArgoWorkflows(object):
|
|
|
1706
1721
|
},
|
|
1707
1722
|
**{
|
|
1708
1723
|
# Some optional values for bookkeeping
|
|
1724
|
+
"METAFLOW_FLOW_FILENAME": os.path.basename(sys.argv[0]),
|
|
1709
1725
|
"METAFLOW_FLOW_NAME": self.flow.name,
|
|
1710
1726
|
"METAFLOW_STEP_NAME": node.name,
|
|
1711
1727
|
"METAFLOW_RUN_ID": run_id,
|
|
@@ -691,10 +691,15 @@ def create(
|
|
|
691
691
|
try:
|
|
692
692
|
if options is not None:
|
|
693
693
|
mf_card = filtered_card(
|
|
694
|
-
options=options,
|
|
694
|
+
options=options,
|
|
695
|
+
components=component_arr,
|
|
696
|
+
graph=graph_dict,
|
|
697
|
+
flow=ctx.obj.flow,
|
|
695
698
|
)
|
|
696
699
|
else:
|
|
697
|
-
mf_card = filtered_card(
|
|
700
|
+
mf_card = filtered_card(
|
|
701
|
+
components=component_arr, graph=graph_dict, flow=ctx.obj.flow
|
|
702
|
+
)
|
|
698
703
|
except TypeError as e:
|
|
699
704
|
if render_error_card:
|
|
700
705
|
mf_card = None
|
|
@@ -76,6 +76,12 @@ class CardDecorator(StepDecorator):
|
|
|
76
76
|
|
|
77
77
|
card_creator = None
|
|
78
78
|
|
|
79
|
+
_config_values = None
|
|
80
|
+
|
|
81
|
+
_config_file_name = None
|
|
82
|
+
|
|
83
|
+
task_finished_decos = 0
|
|
84
|
+
|
|
79
85
|
def __init__(self, *args, **kwargs):
|
|
80
86
|
super(CardDecorator, self).__init__(*args, **kwargs)
|
|
81
87
|
self._task_datastore = None
|
|
@@ -106,6 +112,25 @@ class CardDecorator(StepDecorator):
|
|
|
106
112
|
def _increment_step_counter(cls):
|
|
107
113
|
cls.step_counter += 1
|
|
108
114
|
|
|
115
|
+
@classmethod
|
|
116
|
+
def _increment_completed_counter(cls):
|
|
117
|
+
cls.task_finished_decos += 1
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def _set_config_values(cls, config_values):
|
|
121
|
+
cls._config_values = config_values
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def _set_config_file_name(cls, flow):
|
|
125
|
+
# Only create a config file from the very first card decorator.
|
|
126
|
+
if cls._config_values and not cls._config_file_name:
|
|
127
|
+
with tempfile.NamedTemporaryFile(
|
|
128
|
+
mode="w", encoding="utf-8", delete=False
|
|
129
|
+
) as config_file:
|
|
130
|
+
config_value = dump_config_values(flow)
|
|
131
|
+
json.dump(config_value, config_file)
|
|
132
|
+
cls._config_file_name = config_file.name
|
|
133
|
+
|
|
109
134
|
def step_init(
|
|
110
135
|
self, flow, graph, step_name, decorators, environment, flow_datastore, logger
|
|
111
136
|
):
|
|
@@ -116,11 +141,13 @@ class CardDecorator(StepDecorator):
|
|
|
116
141
|
|
|
117
142
|
# We check for configuration options. We do this here before they are
|
|
118
143
|
# converted to properties.
|
|
119
|
-
self.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
self._set_config_values(
|
|
145
|
+
[
|
|
146
|
+
(config.name, ConfigInput.make_key_name(config.name))
|
|
147
|
+
for _, config in flow._get_parameters()
|
|
148
|
+
if config.IS_CONFIG_PARAMETER
|
|
149
|
+
]
|
|
150
|
+
)
|
|
124
151
|
|
|
125
152
|
self.card_options = self.attributes["options"]
|
|
126
153
|
|
|
@@ -159,15 +186,11 @@ class CardDecorator(StepDecorator):
|
|
|
159
186
|
|
|
160
187
|
# If we have configs, we need to dump them to a file so we can re-use them
|
|
161
188
|
# when calling the card creation subprocess.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
json.dump(config_value, config_file)
|
|
168
|
-
self._config_file_name = config_file.name
|
|
169
|
-
else:
|
|
170
|
-
self._config_file_name = None
|
|
189
|
+
# Since a step can contain multiple card decorators, and all the card creation processes
|
|
190
|
+
# will reference the same config file (because of how the CardCreator is created (only single class instance)),
|
|
191
|
+
# we need to ensure that a single config file is being referenced for all card create commands.
|
|
192
|
+
# This config file will be removed when the last card decorator has finished creating its card.
|
|
193
|
+
self._set_config_file_name(flow)
|
|
171
194
|
|
|
172
195
|
card_type = self.attributes["type"]
|
|
173
196
|
card_class = get_card_class(card_type)
|
|
@@ -246,12 +269,7 @@ class CardDecorator(StepDecorator):
|
|
|
246
269
|
self.card_creator.create(mode="render", final=True, **create_options)
|
|
247
270
|
self.card_creator.create(mode="refresh", final=True, **create_options)
|
|
248
271
|
|
|
249
|
-
|
|
250
|
-
if self._config_file_name:
|
|
251
|
-
try:
|
|
252
|
-
os.unlink(self._config_file_name)
|
|
253
|
-
except Exception as e:
|
|
254
|
-
pass
|
|
272
|
+
self._cleanup(step_name)
|
|
255
273
|
|
|
256
274
|
@staticmethod
|
|
257
275
|
def _options(mapping):
|
|
@@ -286,3 +304,18 @@ class CardDecorator(StepDecorator):
|
|
|
286
304
|
top_level_options["local-config-file"] = self._config_file_name
|
|
287
305
|
|
|
288
306
|
return list(self._options(top_level_options))
|
|
307
|
+
|
|
308
|
+
def task_exception(
|
|
309
|
+
self, exception, step_name, flow, graph, retry_count, max_user_code_retries
|
|
310
|
+
):
|
|
311
|
+
self._cleanup(step_name)
|
|
312
|
+
|
|
313
|
+
def _cleanup(self, step_name):
|
|
314
|
+
self._increment_completed_counter()
|
|
315
|
+
if self.task_finished_decos == self.total_decos_on_step[step_name]:
|
|
316
|
+
# Unlink the config file if it exists
|
|
317
|
+
if self._config_file_name:
|
|
318
|
+
try:
|
|
319
|
+
os.unlink(self._config_file_name)
|
|
320
|
+
except Exception as e:
|
|
321
|
+
pass
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
-
from .card import MetaflowCard, MetaflowCardComponent
|
|
4
|
+
from .card import MetaflowCard, MetaflowCardComponent, with_default_component_id
|
|
5
5
|
from .convert_to_native_type import TaskToDict
|
|
6
6
|
import uuid
|
|
7
|
+
import inspect
|
|
7
8
|
|
|
8
9
|
ABS_DIR_PATH = os.path.dirname(os.path.abspath(__file__))
|
|
9
10
|
RENDER_TEMPLATE_PATH = os.path.join(ABS_DIR_PATH, "base.html")
|
|
@@ -236,9 +237,28 @@ class LogComponent(DefaultComponent):
|
|
|
236
237
|
super().__init__(title=None, subtitle=None)
|
|
237
238
|
self._data = data
|
|
238
239
|
|
|
240
|
+
@with_default_component_id
|
|
239
241
|
def render(self):
|
|
240
242
|
datadict = super().render()
|
|
241
243
|
datadict["data"] = self._data
|
|
244
|
+
if self.component_id is not None:
|
|
245
|
+
datadict["id"] = self.component_id
|
|
246
|
+
return datadict
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class PythonCodeComponent(DefaultComponent):
|
|
250
|
+
|
|
251
|
+
type = "pythonCode"
|
|
252
|
+
|
|
253
|
+
def __init__(self, data=None):
|
|
254
|
+
super().__init__(title=None, subtitle=None)
|
|
255
|
+
self._data = data
|
|
256
|
+
|
|
257
|
+
def render(self):
|
|
258
|
+
datadict = super().render()
|
|
259
|
+
datadict["data"] = self._data
|
|
260
|
+
if self.component_id is not None:
|
|
261
|
+
datadict["id"] = self.component_id
|
|
242
262
|
return datadict
|
|
243
263
|
|
|
244
264
|
|
|
@@ -343,6 +363,7 @@ class TaskInfoComponent(MetaflowCardComponent):
|
|
|
343
363
|
graph=None,
|
|
344
364
|
components=[],
|
|
345
365
|
runtime=False,
|
|
366
|
+
flow=None,
|
|
346
367
|
):
|
|
347
368
|
self._task = task
|
|
348
369
|
self._only_repr = only_repr
|
|
@@ -352,6 +373,7 @@ class TaskInfoComponent(MetaflowCardComponent):
|
|
|
352
373
|
self.final_component = None
|
|
353
374
|
self.page_component = None
|
|
354
375
|
self.runtime = runtime
|
|
376
|
+
self.flow = flow
|
|
355
377
|
|
|
356
378
|
def render(self):
|
|
357
379
|
"""
|
|
@@ -475,6 +497,16 @@ class TaskInfoComponent(MetaflowCardComponent):
|
|
|
475
497
|
contents=[param_component],
|
|
476
498
|
).render()
|
|
477
499
|
|
|
500
|
+
step_func = getattr(self.flow, self._task.parent.id)
|
|
501
|
+
code_table = SectionComponent(
|
|
502
|
+
title="Task Code",
|
|
503
|
+
contents=[
|
|
504
|
+
TableComponent(
|
|
505
|
+
data=[[PythonCodeComponent(inspect.getsource(step_func)).render()]]
|
|
506
|
+
)
|
|
507
|
+
],
|
|
508
|
+
).render()
|
|
509
|
+
|
|
478
510
|
# Don't include parameter ids + "name" in the task artifacts
|
|
479
511
|
artifactlist = [
|
|
480
512
|
task_data_dict["data"][k]
|
|
@@ -500,6 +532,7 @@ class TaskInfoComponent(MetaflowCardComponent):
|
|
|
500
532
|
page_contents.extend(
|
|
501
533
|
[
|
|
502
534
|
metadata_table,
|
|
535
|
+
code_table,
|
|
503
536
|
parameter_table,
|
|
504
537
|
artifact_section,
|
|
505
538
|
]
|
|
@@ -546,7 +579,7 @@ class ErrorCard(MetaflowCard):
|
|
|
546
579
|
|
|
547
580
|
RELOAD_POLICY = MetaflowCard.RELOAD_POLICY_ONCHANGE
|
|
548
581
|
|
|
549
|
-
def __init__(self, options={}, components=[], graph=None):
|
|
582
|
+
def __init__(self, options={}, components=[], graph=None, **kwargs):
|
|
550
583
|
self._only_repr = True
|
|
551
584
|
self._graph = None if graph is None else transform_flow_graph(graph)
|
|
552
585
|
self._components = components
|
|
@@ -602,9 +635,17 @@ class DefaultCardJSON(MetaflowCard):
|
|
|
602
635
|
|
|
603
636
|
type = "default_json"
|
|
604
637
|
|
|
605
|
-
def __init__(
|
|
638
|
+
def __init__(
|
|
639
|
+
self,
|
|
640
|
+
options=dict(only_repr=True),
|
|
641
|
+
components=[],
|
|
642
|
+
graph=None,
|
|
643
|
+
flow=None,
|
|
644
|
+
**kwargs
|
|
645
|
+
):
|
|
606
646
|
self._only_repr = True
|
|
607
647
|
self._graph = None if graph is None else transform_flow_graph(graph)
|
|
648
|
+
self._flow = flow
|
|
608
649
|
if "only_repr" in options:
|
|
609
650
|
self._only_repr = options["only_repr"]
|
|
610
651
|
self._components = components
|
|
@@ -615,6 +656,7 @@ class DefaultCardJSON(MetaflowCard):
|
|
|
615
656
|
only_repr=self._only_repr,
|
|
616
657
|
graph=self._graph,
|
|
617
658
|
components=self._components,
|
|
659
|
+
flow=self._flow,
|
|
618
660
|
).render()
|
|
619
661
|
return json.dumps(final_component_dict)
|
|
620
662
|
|
|
@@ -629,9 +671,17 @@ class DefaultCard(MetaflowCard):
|
|
|
629
671
|
|
|
630
672
|
type = "default"
|
|
631
673
|
|
|
632
|
-
def __init__(
|
|
674
|
+
def __init__(
|
|
675
|
+
self,
|
|
676
|
+
options=dict(only_repr=True),
|
|
677
|
+
components=[],
|
|
678
|
+
graph=None,
|
|
679
|
+
flow=None,
|
|
680
|
+
**kwargs
|
|
681
|
+
):
|
|
633
682
|
self._only_repr = True
|
|
634
683
|
self._graph = None if graph is None else transform_flow_graph(graph)
|
|
684
|
+
self._flow = flow
|
|
635
685
|
if "only_repr" in options:
|
|
636
686
|
self._only_repr = options["only_repr"]
|
|
637
687
|
self._components = components
|
|
@@ -646,6 +696,7 @@ class DefaultCard(MetaflowCard):
|
|
|
646
696
|
graph=self._graph,
|
|
647
697
|
components=self._components,
|
|
648
698
|
runtime=runtime,
|
|
699
|
+
flow=self._flow,
|
|
649
700
|
).render()
|
|
650
701
|
pt = self._get_mustache()
|
|
651
702
|
data_dict = dict(
|
|
@@ -688,7 +739,7 @@ class BlankCard(MetaflowCard):
|
|
|
688
739
|
|
|
689
740
|
type = "blank"
|
|
690
741
|
|
|
691
|
-
def __init__(self, options=dict(title=""), components=[], graph=None):
|
|
742
|
+
def __init__(self, options=dict(title=""), components=[], graph=None, **kwargs):
|
|
692
743
|
self._graph = None if graph is None else transform_flow_graph(graph)
|
|
693
744
|
self._title = ""
|
|
694
745
|
if "title" in options:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
|
+
import uuid
|
|
2
3
|
|
|
3
4
|
if TYPE_CHECKING:
|
|
4
5
|
import metaflow
|
|
@@ -66,7 +67,7 @@ class MetaflowCard(object):
|
|
|
66
67
|
# FIXME document runtime_data
|
|
67
68
|
runtime_data = None
|
|
68
69
|
|
|
69
|
-
def __init__(self, options={}, components=[], graph=None):
|
|
70
|
+
def __init__(self, options={}, components=[], graph=None, flow=None):
|
|
70
71
|
pass
|
|
71
72
|
|
|
72
73
|
def _get_mustache(self):
|
|
@@ -140,3 +141,17 @@ class MetaflowCardComponent(object):
|
|
|
140
141
|
`render` returns a string or dictionary. This class can be called on the client side to dynamically add components to the `MetaflowCard`
|
|
141
142
|
"""
|
|
142
143
|
raise NotImplementedError()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def create_component_id(component):
|
|
147
|
+
uuid_bit = "".join(uuid.uuid4().hex.split("-"))[:6]
|
|
148
|
+
return type(component).__name__.lower() + "_" + uuid_bit
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def with_default_component_id(func):
|
|
152
|
+
def ret_func(self, *args, **kwargs):
|
|
153
|
+
if self.component_id is None:
|
|
154
|
+
self.component_id = create_component_id(self)
|
|
155
|
+
return func(self, *args, **kwargs)
|
|
156
|
+
|
|
157
|
+
return ret_func
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, List, Optional, Union
|
|
1
|
+
from typing import Any, List, Optional, Union, Callable
|
|
2
2
|
from .basic import (
|
|
3
3
|
LogComponent,
|
|
4
4
|
ErrorComponent,
|
|
@@ -7,25 +7,13 @@ from .basic import (
|
|
|
7
7
|
ImageComponent,
|
|
8
8
|
SectionComponent,
|
|
9
9
|
MarkdownComponent,
|
|
10
|
+
PythonCodeComponent,
|
|
10
11
|
)
|
|
11
|
-
from .card import MetaflowCardComponent
|
|
12
|
+
from .card import MetaflowCardComponent, with_default_component_id
|
|
12
13
|
from .convert_to_native_type import TaskToDict, _full_classname
|
|
13
14
|
from .renderer_tools import render_safely
|
|
14
15
|
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
|
|
16
|
+
import inspect
|
|
29
17
|
|
|
30
18
|
|
|
31
19
|
def _warning_with_component(component, msg):
|
|
@@ -823,3 +811,63 @@ class VegaChart(UserComponent):
|
|
|
823
811
|
if self._chart_inside_table and "autosize" not in self._spec:
|
|
824
812
|
data["spec"]["autosize"] = "fit-x"
|
|
825
813
|
return data
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
class PythonCode(UserComponent):
|
|
817
|
+
"""
|
|
818
|
+
A component to display Python code with syntax highlighting.
|
|
819
|
+
|
|
820
|
+
Example:
|
|
821
|
+
```python
|
|
822
|
+
@card
|
|
823
|
+
@step
|
|
824
|
+
def my_step(self):
|
|
825
|
+
# Using code_func
|
|
826
|
+
def my_function():
|
|
827
|
+
x = 1
|
|
828
|
+
y = 2
|
|
829
|
+
return x + y
|
|
830
|
+
current.card.append(
|
|
831
|
+
PythonCode(my_function)
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# Using code_string
|
|
835
|
+
code = '''
|
|
836
|
+
def another_function():
|
|
837
|
+
return "Hello World"
|
|
838
|
+
'''
|
|
839
|
+
current.card.append(
|
|
840
|
+
PythonCode(code_string=code)
|
|
841
|
+
)
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
Parameters
|
|
845
|
+
----------
|
|
846
|
+
code_func : Callable[..., Any], optional, default None
|
|
847
|
+
The function whose source code should be displayed.
|
|
848
|
+
code_string : str, optional, default None
|
|
849
|
+
A string containing Python code to display.
|
|
850
|
+
Either code_func or code_string must be provided.
|
|
851
|
+
"""
|
|
852
|
+
|
|
853
|
+
def __init__(
|
|
854
|
+
self,
|
|
855
|
+
code_func: Optional[Callable[..., Any]] = None,
|
|
856
|
+
code_string: Optional[str] = None,
|
|
857
|
+
):
|
|
858
|
+
if code_func is not None:
|
|
859
|
+
self._code_string = inspect.getsource(code_func)
|
|
860
|
+
else:
|
|
861
|
+
self._code_string = code_string
|
|
862
|
+
|
|
863
|
+
@with_default_component_id
|
|
864
|
+
@render_safely
|
|
865
|
+
def render(self):
|
|
866
|
+
if self._code_string is None:
|
|
867
|
+
return ErrorComponent(
|
|
868
|
+
"`PythonCode` component requires a `code_func` or `code_string` argument. ",
|
|
869
|
+
"None provided for both",
|
|
870
|
+
).render()
|
|
871
|
+
_code_component = PythonCodeComponent(self._code_string)
|
|
872
|
+
_code_component.component_id = self.component_id
|
|
873
|
+
return _code_component.render()
|