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
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
from metaflow.client import Task
|
|
2
2
|
from metaflow import JSONType, namespace
|
|
3
|
-
from metaflow.
|
|
3
|
+
from metaflow.util import resolve_identity
|
|
4
|
+
from metaflow.exception import (
|
|
5
|
+
CommandException,
|
|
6
|
+
MetaflowNotFound,
|
|
7
|
+
MetaflowNamespaceMismatch,
|
|
8
|
+
)
|
|
4
9
|
import webbrowser
|
|
5
10
|
import re
|
|
6
11
|
from metaflow._vendor import click
|
|
7
12
|
import os
|
|
8
13
|
import json
|
|
14
|
+
import uuid
|
|
9
15
|
import signal
|
|
10
16
|
import random
|
|
11
17
|
from contextlib import contextmanager
|
|
@@ -19,11 +25,18 @@ from .exception import (
|
|
|
19
25
|
CardNotPresentException,
|
|
20
26
|
TaskNotFoundException,
|
|
21
27
|
)
|
|
28
|
+
import traceback
|
|
29
|
+
from collections import namedtuple
|
|
22
30
|
|
|
23
31
|
from .card_resolver import resolve_paths_from_task, resumed_info
|
|
24
32
|
|
|
25
33
|
id_func = id
|
|
26
34
|
|
|
35
|
+
CardRenderInfo = namedtuple(
|
|
36
|
+
"CardRenderInfo",
|
|
37
|
+
["mode", "is_implemented", "data", "timed_out", "timeout_stack_trace"],
|
|
38
|
+
)
|
|
39
|
+
|
|
27
40
|
|
|
28
41
|
def open_in_browser(card_path):
|
|
29
42
|
url = "file://" + os.path.abspath(card_path)
|
|
@@ -151,8 +164,6 @@ def timeout(time):
|
|
|
151
164
|
|
|
152
165
|
try:
|
|
153
166
|
yield
|
|
154
|
-
except TimeoutError:
|
|
155
|
-
pass
|
|
156
167
|
finally:
|
|
157
168
|
# Unregister the signal so that it won't be triggered
|
|
158
169
|
# if the timeout is not reached.
|
|
@@ -375,14 +386,144 @@ def card_read_options_and_arguments(func):
|
|
|
375
386
|
return wrapper
|
|
376
387
|
|
|
377
388
|
|
|
378
|
-
def
|
|
379
|
-
|
|
389
|
+
def update_card(mf_card, mode, task, data, timeout_value=None):
|
|
390
|
+
"""
|
|
391
|
+
This method will be responsible for creating a card/data-update based on the `mode`.
|
|
392
|
+
There are three possible modes taken by this function.
|
|
393
|
+
- render :
|
|
394
|
+
- This will render the "final" card.
|
|
395
|
+
- This mode is passed at task completion.
|
|
396
|
+
- Setting this mode will call the `render` method of a MetaflowCard.
|
|
397
|
+
- It will result in the creation of an HTML page.
|
|
398
|
+
- render_runtime:
|
|
399
|
+
- Setting this mode will render a card during task "runtime".
|
|
400
|
+
- Setting this mode will call the `render_runtime` method of a MetaflowCard.
|
|
401
|
+
- It will result in the creation of an HTML page.
|
|
402
|
+
- refresh:
|
|
403
|
+
- Setting this mode will refresh the data update for a card.
|
|
404
|
+
- We support this mode because rendering a full card can be an expensive operation, but shipping tiny data updates can be cheap.
|
|
405
|
+
- Setting this mode will call the `refresh` method of a MetaflowCard.
|
|
406
|
+
- It will result in the creation of a JSON object.
|
|
407
|
+
|
|
408
|
+
Parameters
|
|
409
|
+
----------
|
|
410
|
+
mf_card : MetaflowCard
|
|
411
|
+
MetaflowCard object which will be used to render the card.
|
|
412
|
+
mode : str
|
|
413
|
+
Mode of rendering the card.
|
|
414
|
+
task : Task
|
|
415
|
+
Task object which will be passed to render the card.
|
|
416
|
+
data : dict
|
|
417
|
+
object created and passed down from `current.card._get_latest_data` method.
|
|
418
|
+
For more information on this object's schema have a look at `current.card._get_latest_data` method.
|
|
419
|
+
timeout_value : int
|
|
420
|
+
Timeout value for rendering the card.
|
|
421
|
+
|
|
422
|
+
Returns
|
|
423
|
+
-------
|
|
424
|
+
CardRenderInfo
|
|
425
|
+
- NamedTuple which will contain:
|
|
426
|
+
- `mode`: The mode of rendering the card.
|
|
427
|
+
- `is_implemented`: whether the function was implemented or not.
|
|
428
|
+
- `data` : output from rendering the card (Can be string/dict)
|
|
429
|
+
- `timed_out` : whether the function timed out or not.
|
|
430
|
+
- `timeout_stack_trace` : stack trace of the function if it timed out.
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
def _reload_token():
|
|
434
|
+
if "render_seq" not in data:
|
|
435
|
+
return "never"
|
|
436
|
+
|
|
437
|
+
if data["render_seq"] == "final":
|
|
438
|
+
# final data update should always trigger a card reload to show
|
|
439
|
+
# the final card, hence a different token for the final update
|
|
440
|
+
return "final"
|
|
441
|
+
elif mf_card.RELOAD_POLICY == mf_card.RELOAD_POLICY_ALWAYS:
|
|
442
|
+
return "render-seq-%s" % data["render_seq"]
|
|
443
|
+
elif mf_card.RELOAD_POLICY == mf_card.RELOAD_POLICY_NEVER:
|
|
444
|
+
return "never"
|
|
445
|
+
elif mf_card.RELOAD_POLICY == mf_card.RELOAD_POLICY_ONCHANGE:
|
|
446
|
+
return mf_card.reload_content_token(task, data)
|
|
447
|
+
|
|
448
|
+
def _add_token_html(html):
|
|
449
|
+
if html is None:
|
|
450
|
+
return None
|
|
451
|
+
return html.replace(mf_card.RELOAD_POLICY_TOKEN, _reload_token())
|
|
452
|
+
|
|
453
|
+
def _add_token_json(json_msg):
|
|
454
|
+
if json_msg is None:
|
|
455
|
+
return None
|
|
456
|
+
return {"reload_token": _reload_token(), "data": json_msg}
|
|
457
|
+
|
|
458
|
+
def _safe_call_function(func, *args, **kwargs):
|
|
459
|
+
"""
|
|
460
|
+
returns (data, is_implemented)
|
|
461
|
+
"""
|
|
462
|
+
try:
|
|
463
|
+
return func(*args, **kwargs), True
|
|
464
|
+
except NotImplementedError as e:
|
|
465
|
+
return None, False
|
|
466
|
+
|
|
467
|
+
def _call():
|
|
468
|
+
if mode == "render":
|
|
469
|
+
setattr(
|
|
470
|
+
mf_card.__class__,
|
|
471
|
+
"runtime_data",
|
|
472
|
+
property(fget=lambda _, data=data: data),
|
|
473
|
+
)
|
|
474
|
+
output = _add_token_html(mf_card.render(task))
|
|
475
|
+
return CardRenderInfo(
|
|
476
|
+
mode=mode,
|
|
477
|
+
is_implemented=True,
|
|
478
|
+
data=output,
|
|
479
|
+
timed_out=False,
|
|
480
|
+
timeout_stack_trace=None,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
elif mode == "render_runtime":
|
|
484
|
+
# Since many cards created by metaflow users may not have implemented a
|
|
485
|
+
# `render_time` / `refresh` methods, it can result in an exception and thereby
|
|
486
|
+
# creation of error cards (especially for the `render_runtime` method). So instead
|
|
487
|
+
# we will catch the NotImplementedError and return None if users have not implemented it.
|
|
488
|
+
# If there any any other exception from the user code, it should be bubbled to the top level.
|
|
489
|
+
output, is_implemented = _safe_call_function(
|
|
490
|
+
mf_card.render_runtime, task, data
|
|
491
|
+
)
|
|
492
|
+
return CardRenderInfo(
|
|
493
|
+
mode=mode,
|
|
494
|
+
is_implemented=is_implemented,
|
|
495
|
+
data=_add_token_html(output),
|
|
496
|
+
timed_out=False,
|
|
497
|
+
timeout_stack_trace=None,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
elif mode == "refresh":
|
|
501
|
+
output, is_implemented = _safe_call_function(mf_card.refresh, task, data)
|
|
502
|
+
return CardRenderInfo(
|
|
503
|
+
mode=mode,
|
|
504
|
+
is_implemented=is_implemented,
|
|
505
|
+
data=_add_token_json(output),
|
|
506
|
+
timed_out=False,
|
|
507
|
+
timeout_stack_trace=None,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
render_info = None
|
|
380
511
|
if timeout_value is None or timeout_value < 0:
|
|
381
|
-
|
|
512
|
+
return _call()
|
|
382
513
|
else:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
514
|
+
try:
|
|
515
|
+
with timeout(timeout_value):
|
|
516
|
+
render_info = _call()
|
|
517
|
+
except TimeoutError:
|
|
518
|
+
stack_trace = traceback.format_exc()
|
|
519
|
+
return CardRenderInfo(
|
|
520
|
+
mode=mode,
|
|
521
|
+
is_implemented=True,
|
|
522
|
+
data=None,
|
|
523
|
+
timed_out=True,
|
|
524
|
+
timeout_stack_trace=stack_trace,
|
|
525
|
+
)
|
|
526
|
+
return render_info
|
|
386
527
|
|
|
387
528
|
|
|
388
529
|
@card.command(help="create a HTML card")
|
|
@@ -414,29 +555,64 @@ def render_card(mf_card, task, timeout_value=None):
|
|
|
414
555
|
is_flag=True,
|
|
415
556
|
help="Upon failing to render a card, render a card holding the stack trace",
|
|
416
557
|
)
|
|
558
|
+
@click.option(
|
|
559
|
+
"--id",
|
|
560
|
+
default=None,
|
|
561
|
+
show_default=True,
|
|
562
|
+
type=str,
|
|
563
|
+
help="ID of the card",
|
|
564
|
+
)
|
|
417
565
|
@click.option(
|
|
418
566
|
"--component-file",
|
|
419
567
|
default=None,
|
|
420
568
|
show_default=True,
|
|
421
569
|
type=str,
|
|
422
|
-
help="JSON File with
|
|
570
|
+
help="JSON File with pre-rendered components. (internal)",
|
|
423
571
|
)
|
|
424
572
|
@click.option(
|
|
425
|
-
"--
|
|
573
|
+
"--mode",
|
|
574
|
+
default="render",
|
|
575
|
+
show_default=True,
|
|
576
|
+
type=click.Choice(["render", "render_runtime", "refresh"]),
|
|
577
|
+
help="Rendering mode. (internal)",
|
|
578
|
+
)
|
|
579
|
+
@click.option(
|
|
580
|
+
"--data-file",
|
|
426
581
|
default=None,
|
|
427
582
|
show_default=True,
|
|
428
583
|
type=str,
|
|
429
|
-
|
|
584
|
+
hidden=True,
|
|
585
|
+
help="JSON file containing data to be updated. (internal)",
|
|
586
|
+
)
|
|
587
|
+
@click.option(
|
|
588
|
+
"--card-uuid",
|
|
589
|
+
default=None,
|
|
590
|
+
show_default=True,
|
|
591
|
+
type=str,
|
|
592
|
+
hidden=True,
|
|
593
|
+
help="Card UUID. (internal)",
|
|
594
|
+
)
|
|
595
|
+
@click.option(
|
|
596
|
+
"--delete-input-files",
|
|
597
|
+
default=False,
|
|
598
|
+
is_flag=True,
|
|
599
|
+
show_default=True,
|
|
600
|
+
hidden=True,
|
|
601
|
+
help="Delete data-file and component-file after reading. (internal)",
|
|
430
602
|
)
|
|
431
603
|
@click.pass_context
|
|
432
604
|
def create(
|
|
433
605
|
ctx,
|
|
434
606
|
pathspec,
|
|
607
|
+
mode=None,
|
|
435
608
|
type=None,
|
|
436
609
|
options=None,
|
|
437
610
|
timeout=None,
|
|
438
611
|
component_file=None,
|
|
612
|
+
data_file=None,
|
|
439
613
|
render_error_card=False,
|
|
614
|
+
card_uuid=None,
|
|
615
|
+
delete_input_files=None,
|
|
440
616
|
id=None,
|
|
441
617
|
):
|
|
442
618
|
card_id = id
|
|
@@ -452,11 +628,26 @@ def create(
|
|
|
452
628
|
|
|
453
629
|
graph_dict, _ = ctx.obj.graph.output_steps()
|
|
454
630
|
|
|
631
|
+
if card_uuid is None:
|
|
632
|
+
card_uuid = str(uuid.uuid4()).replace("-", "")
|
|
633
|
+
|
|
455
634
|
# Components are rendered in a Step and added via `current.card.append` are added here.
|
|
456
635
|
component_arr = []
|
|
457
636
|
if component_file is not None:
|
|
458
637
|
with open(component_file, "r") as f:
|
|
459
638
|
component_arr = json.load(f)
|
|
639
|
+
# Component data used in card runtime is passed in as temporary files which can be deleted after use
|
|
640
|
+
if delete_input_files:
|
|
641
|
+
os.remove(component_file)
|
|
642
|
+
|
|
643
|
+
# Load data to be refreshed for runtime cards
|
|
644
|
+
data = {}
|
|
645
|
+
if data_file is not None:
|
|
646
|
+
with open(data_file, "r") as f:
|
|
647
|
+
data = json.load(f)
|
|
648
|
+
# data is passed in as temporary files which can be deleted after use
|
|
649
|
+
if delete_input_files:
|
|
650
|
+
os.remove(data_file)
|
|
460
651
|
|
|
461
652
|
task = Task(full_pathspec)
|
|
462
653
|
from metaflow.plugins import CARDS
|
|
@@ -498,23 +689,88 @@ def create(
|
|
|
498
689
|
else:
|
|
499
690
|
raise IncorrectCardArgsException(type, options)
|
|
500
691
|
|
|
692
|
+
rendered_content = None
|
|
501
693
|
if mf_card:
|
|
502
694
|
try:
|
|
503
|
-
rendered_info =
|
|
695
|
+
rendered_info = update_card(
|
|
696
|
+
mf_card, mode, task, data, timeout_value=timeout
|
|
697
|
+
)
|
|
698
|
+
rendered_content = rendered_info.data
|
|
504
699
|
except:
|
|
700
|
+
rendered_info = CardRenderInfo(
|
|
701
|
+
mode=mode,
|
|
702
|
+
is_implemented=True,
|
|
703
|
+
data=None,
|
|
704
|
+
timed_out=False,
|
|
705
|
+
timeout_stack_trace=None,
|
|
706
|
+
)
|
|
505
707
|
if render_error_card:
|
|
506
708
|
error_stack_trace = str(UnrenderableCardException(type, options))
|
|
507
709
|
else:
|
|
508
710
|
raise UnrenderableCardException(type, options)
|
|
509
|
-
#
|
|
510
|
-
|
|
511
|
-
if error_stack_trace is not None:
|
|
512
|
-
rendered_info = error_card().render(task, stack_trace=error_stack_trace)
|
|
513
711
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
712
|
+
# In the entire card rendering process, there are a few cases we want to handle:
|
|
713
|
+
# - [mode == "render"]
|
|
714
|
+
# 1. Card is rendered successfully (We store it in the datastore as a HTML file)
|
|
715
|
+
# 2. Card is not rendered successfully and we have --save-error-card flag set to True
|
|
716
|
+
# (We store it in the datastore as a HTML file with stack trace)
|
|
717
|
+
# 3. Card render timed-out and we have --save-error-card flag set to True
|
|
718
|
+
# (We store it in the datastore as a HTML file with stack trace)
|
|
719
|
+
# 4. `render` returns nothing and we have --save-error-card flag set to True.
|
|
720
|
+
# (We store it in the datastore as a HTML file with some message saying you returned nothing)
|
|
721
|
+
# - [mode == "render_runtime"]
|
|
722
|
+
# 1. Card is rendered successfully (We store it in the datastore as a HTML file)
|
|
723
|
+
# 2. `render_runtime` is not implemented but gets called and we have --save-error-card flag set to True.
|
|
724
|
+
# (We store it in the datastore as a HTML file with some message saying the card should not be a runtime card if this method is not Implemented)
|
|
725
|
+
# 3. `render_runtime` is implemented and raises an exception and we have --save-error-card flag set to True.
|
|
726
|
+
# (We store it in the datastore as a HTML file with stack trace)
|
|
727
|
+
# 4. `render_runtime` is implemented but returns nothing and we have --save-error-card flag set to True.
|
|
728
|
+
# (We store it in the datastore as a HTML file with some message saying you returned nothing)
|
|
729
|
+
# 5. `render_runtime` is implemented but times out and we have --save-error-card flag set to True.
|
|
730
|
+
# (We store it in the datastore as a HTML file with stack trace)
|
|
731
|
+
# - [mode == "refresh"]
|
|
732
|
+
# 1. Data update is created successfully (We store it in the datastore as a JSON file)
|
|
733
|
+
# 2. `refresh` is not implemented. (We do nothing. Don't store anything.)
|
|
734
|
+
# 3. `refresh` is implemented but it raises an exception. (We do nothing. Don't store anything.)
|
|
735
|
+
# 4. `refresh` is implemented but it times out. (We do nothing. Don't store anything.)
|
|
736
|
+
|
|
737
|
+
if error_stack_trace is not None and mode != "refresh":
|
|
738
|
+
rendered_content = error_card().render(task, stack_trace=error_stack_trace)
|
|
739
|
+
|
|
740
|
+
if (
|
|
741
|
+
rendered_info.is_implemented
|
|
742
|
+
and rendered_info.timed_out
|
|
743
|
+
and mode != "refresh"
|
|
744
|
+
and render_error_card
|
|
745
|
+
):
|
|
746
|
+
timeout_stack_trace = (
|
|
747
|
+
"\nCard rendering timed out after %s seconds. "
|
|
748
|
+
"To increase the timeout duration for card rendering, please set the `timeout` parameter in the @card decorator. "
|
|
749
|
+
"\nStack Trace : \n%s"
|
|
750
|
+
) % (timeout, rendered_info.timeout_stack_trace)
|
|
751
|
+
rendered_content = error_card().render(
|
|
752
|
+
task,
|
|
753
|
+
stack_trace=timeout_stack_trace,
|
|
754
|
+
)
|
|
755
|
+
elif (
|
|
756
|
+
rendered_info.is_implemented
|
|
757
|
+
and rendered_info.data is None
|
|
758
|
+
and render_error_card
|
|
759
|
+
and mode != "refresh"
|
|
760
|
+
):
|
|
761
|
+
rendered_content = error_card().render(
|
|
762
|
+
task, stack_trace="No information rendered from card of type %s" % type
|
|
517
763
|
)
|
|
764
|
+
elif (
|
|
765
|
+
not rendered_info.is_implemented
|
|
766
|
+
and render_error_card
|
|
767
|
+
and mode == "render_runtime"
|
|
768
|
+
):
|
|
769
|
+
message = (
|
|
770
|
+
"Card of type %s is a runtime time card with no `render_runtime` implemented. "
|
|
771
|
+
"Please implement `render_runtime` method to allow rendering this card at runtime."
|
|
772
|
+
) % type
|
|
773
|
+
rendered_content = error_card().render(task, stack_trace=message)
|
|
518
774
|
|
|
519
775
|
# todo : should we save native type for error card or error type ?
|
|
520
776
|
if type is not None and re.match(CARD_ID_PATTERN, type) is not None:
|
|
@@ -531,13 +787,21 @@ def create(
|
|
|
531
787
|
)
|
|
532
788
|
card_id = None
|
|
533
789
|
|
|
534
|
-
if
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
fg="green"
|
|
540
|
-
|
|
790
|
+
if rendered_content is not None:
|
|
791
|
+
if mode == "refresh":
|
|
792
|
+
card_datastore.save_data(
|
|
793
|
+
card_uuid, save_type, rendered_content, card_id=card_id
|
|
794
|
+
)
|
|
795
|
+
ctx.obj.echo("Data updated", fg="green")
|
|
796
|
+
else:
|
|
797
|
+
card_info = card_datastore.save_card(
|
|
798
|
+
card_uuid, save_type, rendered_content, card_id=card_id
|
|
799
|
+
)
|
|
800
|
+
ctx.obj.echo(
|
|
801
|
+
"Card created with type: %s and hash: %s"
|
|
802
|
+
% (card_info.type, card_info.hash[:NUM_SHORT_HASH_CHARS]),
|
|
803
|
+
fg="green",
|
|
804
|
+
)
|
|
541
805
|
|
|
542
806
|
|
|
543
807
|
@card.command()
|
|
@@ -655,7 +919,6 @@ def list(
|
|
|
655
919
|
as_json=False,
|
|
656
920
|
file=None,
|
|
657
921
|
):
|
|
658
|
-
|
|
659
922
|
card_id = id
|
|
660
923
|
if pathspec is None:
|
|
661
924
|
list_many_cards(
|
|
@@ -687,3 +950,125 @@ def list(
|
|
|
687
950
|
show_list_as_json=as_json,
|
|
688
951
|
file=file,
|
|
689
952
|
)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
@card.command(help="Run local card viewer server")
|
|
956
|
+
@click.option(
|
|
957
|
+
"--run-id",
|
|
958
|
+
default=None,
|
|
959
|
+
show_default=True,
|
|
960
|
+
type=str,
|
|
961
|
+
help="Run ID of the flow",
|
|
962
|
+
)
|
|
963
|
+
@click.option(
|
|
964
|
+
"--port",
|
|
965
|
+
default=8324,
|
|
966
|
+
show_default=True,
|
|
967
|
+
type=int,
|
|
968
|
+
help="Port on which Metaflow card viewer server will run",
|
|
969
|
+
)
|
|
970
|
+
@click.option(
|
|
971
|
+
"--namespace",
|
|
972
|
+
"user_namespace",
|
|
973
|
+
default=None,
|
|
974
|
+
show_default=True,
|
|
975
|
+
type=str,
|
|
976
|
+
help="Namespace of the flow",
|
|
977
|
+
)
|
|
978
|
+
@click.option(
|
|
979
|
+
"--poll-interval",
|
|
980
|
+
default=5,
|
|
981
|
+
show_default=True,
|
|
982
|
+
type=int,
|
|
983
|
+
help="Polling interval of the card viewer server.",
|
|
984
|
+
)
|
|
985
|
+
@click.option(
|
|
986
|
+
"--max-cards",
|
|
987
|
+
default=30,
|
|
988
|
+
show_default=True,
|
|
989
|
+
type=int,
|
|
990
|
+
help="Maximum number of cards to be shown at any time by the card viewer server",
|
|
991
|
+
)
|
|
992
|
+
@click.pass_context
|
|
993
|
+
def server(ctx, run_id, port, user_namespace, poll_interval, max_cards):
|
|
994
|
+
from .card_server import create_card_server, CardServerOptions
|
|
995
|
+
|
|
996
|
+
user_namespace = resolve_identity() if user_namespace is None else user_namespace
|
|
997
|
+
run, follow_new_runs, _status_message = _get_run_object(
|
|
998
|
+
ctx.obj, run_id, user_namespace
|
|
999
|
+
)
|
|
1000
|
+
if _status_message is not None:
|
|
1001
|
+
ctx.obj.echo(_status_message, fg="red")
|
|
1002
|
+
options = CardServerOptions(
|
|
1003
|
+
flow_name=ctx.obj.flow.name,
|
|
1004
|
+
run_object=run,
|
|
1005
|
+
only_running=False,
|
|
1006
|
+
follow_resumed=False,
|
|
1007
|
+
flow_datastore=ctx.obj.flow_datastore,
|
|
1008
|
+
max_cards=max_cards,
|
|
1009
|
+
follow_new_runs=follow_new_runs,
|
|
1010
|
+
poll_interval=poll_interval,
|
|
1011
|
+
)
|
|
1012
|
+
create_card_server(options, port, ctx.obj)
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def _get_run_from_cli_set_runid(obj, run_id):
|
|
1016
|
+
# This run-id will be set from the command line args.
|
|
1017
|
+
# So if we hit a MetaflowNotFound exception / Namespace mismatch then
|
|
1018
|
+
# we should raise an exception
|
|
1019
|
+
from metaflow import Run
|
|
1020
|
+
|
|
1021
|
+
flow_name = obj.flow.name
|
|
1022
|
+
if len(run_id.split("/")) > 1:
|
|
1023
|
+
raise CommandException(
|
|
1024
|
+
"run_id should NOT be of the form: `<flowname>/<runid>`. Please provide only run-id"
|
|
1025
|
+
)
|
|
1026
|
+
try:
|
|
1027
|
+
pathspec = "%s/%s" % (flow_name, run_id)
|
|
1028
|
+
# Since we are looking at all namespaces,
|
|
1029
|
+
# we will not
|
|
1030
|
+
namespace(None)
|
|
1031
|
+
return Run(pathspec)
|
|
1032
|
+
except MetaflowNotFound:
|
|
1033
|
+
raise CommandException("No run (%s) found for *%s*." % (run_id, flow_name))
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def _get_run_object(obj, run_id, user_namespace):
|
|
1037
|
+
from metaflow import Flow
|
|
1038
|
+
|
|
1039
|
+
follow_new_runs = True
|
|
1040
|
+
flow_name = obj.flow.name
|
|
1041
|
+
|
|
1042
|
+
if run_id is not None:
|
|
1043
|
+
follow_new_runs = False
|
|
1044
|
+
run = _get_run_from_cli_set_runid(obj, run_id)
|
|
1045
|
+
obj.echo("Using run-id %s" % run.pathspec, fg="blue", bold=False)
|
|
1046
|
+
return run, follow_new_runs, None
|
|
1047
|
+
|
|
1048
|
+
_msg = "Searching for runs in namespace: %s" % user_namespace
|
|
1049
|
+
obj.echo(_msg, fg="blue", bold=False)
|
|
1050
|
+
|
|
1051
|
+
try:
|
|
1052
|
+
namespace(user_namespace)
|
|
1053
|
+
flow = Flow(pathspec=flow_name)
|
|
1054
|
+
run = flow.latest_run
|
|
1055
|
+
except MetaflowNotFound:
|
|
1056
|
+
# When we have no runs found for the Flow, we need to ensure that
|
|
1057
|
+
# if the `follow_new_runs` is set to True; If `follow_new_runs` is set to True then
|
|
1058
|
+
# we don't raise the Exception and instead we return None and let the
|
|
1059
|
+
# background Thread wait on the Retrieving the run object.
|
|
1060
|
+
_status_msg = "No run found for *%s*." % flow_name
|
|
1061
|
+
return None, follow_new_runs, _status_msg
|
|
1062
|
+
|
|
1063
|
+
except MetaflowNamespaceMismatch:
|
|
1064
|
+
_status_msg = (
|
|
1065
|
+
"No run found for *%s* in namespace *%s*. You can switch the namespace using --namespace"
|
|
1066
|
+
% (
|
|
1067
|
+
flow_name,
|
|
1068
|
+
user_namespace,
|
|
1069
|
+
)
|
|
1070
|
+
)
|
|
1071
|
+
return None, follow_new_runs, _status_msg
|
|
1072
|
+
|
|
1073
|
+
obj.echo("Using run-id %s" % run.pathspec, fg="blue", bold=False)
|
|
1074
|
+
return run, follow_new_runs, None
|
|
@@ -2,10 +2,10 @@ from typing import Optional, Union, TYPE_CHECKING
|
|
|
2
2
|
from metaflow.datastore import FlowDataStore
|
|
3
3
|
from metaflow.metaflow_config import CARD_SUFFIX
|
|
4
4
|
from .card_resolver import resolve_paths_from_task, resumed_info
|
|
5
|
-
from .card_datastore import CardDatastore
|
|
5
|
+
from .card_datastore import CardDatastore, CardNameSuffix
|
|
6
6
|
from .exception import (
|
|
7
7
|
UnresolvableDatastoreException,
|
|
8
|
-
|
|
8
|
+
IncorrectArgumentException,
|
|
9
9
|
IncorrectPathspecException,
|
|
10
10
|
)
|
|
11
11
|
import os
|
|
@@ -57,6 +57,15 @@ class Card:
|
|
|
57
57
|
# Tempfile to open stuff in browser
|
|
58
58
|
self._temp_file = None
|
|
59
59
|
|
|
60
|
+
def get_data(self) -> Optional[dict]:
|
|
61
|
+
# currently an internal method to retrieve a card's data.
|
|
62
|
+
data_paths = self._card_ds.extract_data_paths(
|
|
63
|
+
card_type=self.type, card_hash=self.hash, card_id=self._card_id
|
|
64
|
+
)
|
|
65
|
+
if len(data_paths) == 0:
|
|
66
|
+
return None
|
|
67
|
+
return self._card_ds.get_card_data(data_paths[0])
|
|
68
|
+
|
|
60
69
|
def get(self) -> str:
|
|
61
70
|
"""
|
|
62
71
|
Retrieves the HTML contents of the card from the
|
|
@@ -150,12 +159,12 @@ class CardContainer:
|
|
|
150
159
|
```
|
|
151
160
|
"""
|
|
152
161
|
|
|
153
|
-
def __init__(self, card_paths, card_ds,
|
|
162
|
+
def __init__(self, card_paths, card_ds, origin_pathspec=None):
|
|
154
163
|
self._card_paths = card_paths
|
|
155
164
|
self._card_ds = card_ds
|
|
156
165
|
self._current = 0
|
|
157
166
|
self._high = len(card_paths)
|
|
158
|
-
self.from_resumed =
|
|
167
|
+
self.from_resumed = origin_pathspec is not None
|
|
159
168
|
self.origin_pathspec = origin_pathspec
|
|
160
169
|
|
|
161
170
|
def __len__(self):
|
|
@@ -172,7 +181,7 @@ class CardContainer:
|
|
|
172
181
|
if index >= self._high:
|
|
173
182
|
raise IndexError
|
|
174
183
|
path = self._card_paths[index]
|
|
175
|
-
card_info = self._card_ds.
|
|
184
|
+
card_info = self._card_ds.info_from_path(path, suffix=CardNameSuffix.CARD)
|
|
176
185
|
# todo : find card creation date and put it in client.
|
|
177
186
|
return Card(
|
|
178
187
|
self._card_ds,
|
|
@@ -250,8 +259,9 @@ def get_cards(
|
|
|
250
259
|
task = Task(task_str)
|
|
251
260
|
elif not isinstance(task, Task):
|
|
252
261
|
# Exception that the task argument should be of form `Task` or `str`
|
|
253
|
-
raise
|
|
262
|
+
raise IncorrectArgumentException(_TYPE(task))
|
|
254
263
|
|
|
264
|
+
origin_taskpathspec = None
|
|
255
265
|
if follow_resumed:
|
|
256
266
|
origin_taskpathspec = resumed_info(task)
|
|
257
267
|
if origin_taskpathspec:
|
|
@@ -263,7 +273,6 @@ def get_cards(
|
|
|
263
273
|
return CardContainer(
|
|
264
274
|
card_paths,
|
|
265
275
|
card_ds,
|
|
266
|
-
from_resumed=origin_taskpathspec is not None,
|
|
267
276
|
origin_pathspec=origin_taskpathspec,
|
|
268
277
|
)
|
|
269
278
|
|