ob-metaflow 2.15.13.1__py2.py3-none-any.whl → 2.19.7.1rc0__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.
- metaflow/__init__.py +10 -3
- metaflow/_vendor/imghdr/__init__.py +186 -0
- metaflow/_vendor/yaml/__init__.py +427 -0
- metaflow/_vendor/yaml/composer.py +139 -0
- metaflow/_vendor/yaml/constructor.py +748 -0
- metaflow/_vendor/yaml/cyaml.py +101 -0
- metaflow/_vendor/yaml/dumper.py +62 -0
- metaflow/_vendor/yaml/emitter.py +1137 -0
- metaflow/_vendor/yaml/error.py +75 -0
- metaflow/_vendor/yaml/events.py +86 -0
- metaflow/_vendor/yaml/loader.py +63 -0
- metaflow/_vendor/yaml/nodes.py +49 -0
- metaflow/_vendor/yaml/parser.py +589 -0
- metaflow/_vendor/yaml/reader.py +185 -0
- metaflow/_vendor/yaml/representer.py +389 -0
- metaflow/_vendor/yaml/resolver.py +227 -0
- metaflow/_vendor/yaml/scanner.py +1435 -0
- metaflow/_vendor/yaml/serializer.py +111 -0
- metaflow/_vendor/yaml/tokens.py +104 -0
- metaflow/cards.py +4 -0
- metaflow/cli.py +125 -21
- metaflow/cli_components/init_cmd.py +1 -0
- metaflow/cli_components/run_cmds.py +204 -40
- metaflow/cli_components/step_cmd.py +160 -4
- metaflow/client/__init__.py +1 -0
- metaflow/client/core.py +198 -130
- metaflow/client/filecache.py +59 -32
- metaflow/cmd/code/__init__.py +2 -1
- metaflow/cmd/develop/stub_generator.py +49 -18
- metaflow/cmd/develop/stubs.py +9 -27
- metaflow/cmd/make_wrapper.py +30 -0
- metaflow/datastore/__init__.py +1 -0
- metaflow/datastore/content_addressed_store.py +40 -9
- metaflow/datastore/datastore_set.py +10 -1
- metaflow/datastore/flow_datastore.py +124 -4
- metaflow/datastore/spin_datastore.py +91 -0
- metaflow/datastore/task_datastore.py +92 -6
- metaflow/debug.py +5 -0
- metaflow/decorators.py +331 -82
- metaflow/extension_support/__init__.py +414 -356
- metaflow/extension_support/_empty_file.py +2 -2
- metaflow/flowspec.py +322 -82
- metaflow/graph.py +178 -15
- metaflow/includefile.py +25 -3
- metaflow/lint.py +94 -3
- metaflow/meta_files.py +13 -0
- metaflow/metadata_provider/metadata.py +13 -2
- metaflow/metaflow_config.py +66 -4
- metaflow/metaflow_environment.py +91 -25
- metaflow/metaflow_profile.py +18 -0
- metaflow/metaflow_version.py +16 -1
- metaflow/package/__init__.py +673 -0
- metaflow/packaging_sys/__init__.py +880 -0
- metaflow/packaging_sys/backend.py +128 -0
- metaflow/packaging_sys/distribution_support.py +153 -0
- metaflow/packaging_sys/tar_backend.py +99 -0
- metaflow/packaging_sys/utils.py +54 -0
- metaflow/packaging_sys/v1.py +527 -0
- metaflow/parameters.py +6 -2
- metaflow/plugins/__init__.py +6 -0
- metaflow/plugins/airflow/airflow.py +11 -1
- metaflow/plugins/airflow/airflow_cli.py +16 -5
- metaflow/plugins/argo/argo_client.py +42 -20
- metaflow/plugins/argo/argo_events.py +6 -6
- metaflow/plugins/argo/argo_workflows.py +1023 -344
- metaflow/plugins/argo/argo_workflows_cli.py +396 -94
- metaflow/plugins/argo/argo_workflows_decorator.py +9 -0
- metaflow/plugins/argo/argo_workflows_deployer_objects.py +75 -49
- metaflow/plugins/argo/capture_error.py +5 -2
- metaflow/plugins/argo/conditional_input_paths.py +35 -0
- metaflow/plugins/argo/exit_hooks.py +209 -0
- metaflow/plugins/argo/param_val.py +19 -0
- metaflow/plugins/aws/aws_client.py +6 -0
- metaflow/plugins/aws/aws_utils.py +33 -1
- metaflow/plugins/aws/batch/batch.py +72 -5
- metaflow/plugins/aws/batch/batch_cli.py +24 -3
- metaflow/plugins/aws/batch/batch_decorator.py +57 -6
- metaflow/plugins/aws/step_functions/step_functions.py +28 -3
- metaflow/plugins/aws/step_functions/step_functions_cli.py +49 -4
- metaflow/plugins/aws/step_functions/step_functions_deployer.py +3 -0
- metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +30 -0
- metaflow/plugins/cards/card_cli.py +20 -1
- metaflow/plugins/cards/card_creator.py +24 -1
- metaflow/plugins/cards/card_datastore.py +21 -49
- metaflow/plugins/cards/card_decorator.py +58 -6
- metaflow/plugins/cards/card_modules/basic.py +38 -9
- metaflow/plugins/cards/card_modules/bundle.css +1 -1
- metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
- metaflow/plugins/cards/card_modules/components.py +592 -3
- metaflow/plugins/cards/card_modules/convert_to_native_type.py +34 -5
- metaflow/plugins/cards/card_modules/json_viewer.py +232 -0
- metaflow/plugins/cards/card_modules/main.css +1 -0
- metaflow/plugins/cards/card_modules/main.js +56 -41
- metaflow/plugins/cards/card_modules/test_cards.py +22 -6
- metaflow/plugins/cards/component_serializer.py +1 -8
- metaflow/plugins/cards/metadata.py +22 -0
- metaflow/plugins/catch_decorator.py +9 -0
- metaflow/plugins/datastores/local_storage.py +12 -6
- metaflow/plugins/datastores/spin_storage.py +12 -0
- metaflow/plugins/datatools/s3/s3.py +49 -17
- metaflow/plugins/datatools/s3/s3op.py +113 -66
- metaflow/plugins/env_escape/client_modules.py +102 -72
- metaflow/plugins/events_decorator.py +127 -121
- metaflow/plugins/exit_hook/__init__.py +0 -0
- metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
- metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
- metaflow/plugins/kubernetes/kubernetes.py +12 -1
- metaflow/plugins/kubernetes/kubernetes_cli.py +11 -0
- metaflow/plugins/kubernetes/kubernetes_decorator.py +25 -6
- metaflow/plugins/kubernetes/kubernetes_job.py +12 -4
- metaflow/plugins/kubernetes/kubernetes_jobsets.py +31 -30
- metaflow/plugins/metadata_providers/local.py +76 -82
- metaflow/plugins/metadata_providers/service.py +13 -9
- metaflow/plugins/metadata_providers/spin.py +16 -0
- metaflow/plugins/package_cli.py +36 -24
- metaflow/plugins/parallel_decorator.py +11 -2
- metaflow/plugins/parsers.py +16 -0
- metaflow/plugins/pypi/bootstrap.py +7 -1
- metaflow/plugins/pypi/conda_decorator.py +41 -82
- metaflow/plugins/pypi/conda_environment.py +14 -6
- metaflow/plugins/pypi/micromamba.py +9 -1
- metaflow/plugins/pypi/pip.py +41 -5
- metaflow/plugins/pypi/pypi_decorator.py +4 -4
- metaflow/plugins/pypi/utils.py +22 -0
- metaflow/plugins/secrets/__init__.py +3 -0
- metaflow/plugins/secrets/secrets_decorator.py +14 -178
- metaflow/plugins/secrets/secrets_func.py +49 -0
- metaflow/plugins/secrets/secrets_spec.py +101 -0
- metaflow/plugins/secrets/utils.py +74 -0
- metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
- metaflow/plugins/timeout_decorator.py +0 -1
- metaflow/plugins/uv/bootstrap.py +29 -1
- metaflow/plugins/uv/uv_environment.py +5 -3
- metaflow/pylint_wrapper.py +5 -1
- metaflow/runner/click_api.py +79 -26
- metaflow/runner/deployer.py +208 -6
- metaflow/runner/deployer_impl.py +32 -12
- metaflow/runner/metaflow_runner.py +266 -33
- metaflow/runner/subprocess_manager.py +21 -1
- metaflow/runner/utils.py +27 -16
- metaflow/runtime.py +660 -66
- metaflow/task.py +255 -26
- metaflow/user_configs/config_options.py +33 -21
- metaflow/user_configs/config_parameters.py +220 -58
- metaflow/user_decorators/__init__.py +0 -0
- metaflow/user_decorators/common.py +144 -0
- metaflow/user_decorators/mutable_flow.py +512 -0
- metaflow/user_decorators/mutable_step.py +424 -0
- metaflow/user_decorators/user_flow_decorator.py +264 -0
- metaflow/user_decorators/user_step_decorator.py +749 -0
- metaflow/util.py +197 -7
- metaflow/vendor.py +23 -7
- metaflow/version.py +1 -1
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Makefile +13 -2
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Tiltfile +107 -7
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/pick_services.sh +1 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/METADATA +2 -3
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/RECORD +162 -121
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
- metaflow/_vendor/v3_5/__init__.py +0 -1
- metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
- metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
- metaflow/_vendor/v3_5/zipp.py +0 -329
- metaflow/info_file.py +0 -25
- metaflow/package.py +0 -203
- metaflow/user_configs/config_decorators.py +0 -568
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +0 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/licenses/LICENSE +0 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/top_level.txt +0 -0
|
@@ -12,8 +12,10 @@ from .basic import (
|
|
|
12
12
|
from .card import MetaflowCardComponent, with_default_component_id
|
|
13
13
|
from .convert_to_native_type import TaskToDict, _full_classname
|
|
14
14
|
from .renderer_tools import render_safely
|
|
15
|
+
from .json_viewer import JSONViewer as _JSONViewer, YAMLViewer as _YAMLViewer
|
|
15
16
|
import uuid
|
|
16
17
|
import inspect
|
|
18
|
+
import textwrap
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def _warning_with_component(component, msg):
|
|
@@ -655,19 +657,38 @@ class Markdown(UserComponent):
|
|
|
655
657
|
)
|
|
656
658
|
```
|
|
657
659
|
|
|
660
|
+
Multi-line strings with indentation are automatically dedented:
|
|
661
|
+
```
|
|
662
|
+
current.card.append(
|
|
663
|
+
Markdown(f'''
|
|
664
|
+
# Header
|
|
665
|
+
- Item 1
|
|
666
|
+
- Item 2
|
|
667
|
+
''')
|
|
668
|
+
)
|
|
669
|
+
```
|
|
670
|
+
|
|
658
671
|
Parameters
|
|
659
672
|
----------
|
|
660
673
|
text : str
|
|
661
|
-
Text formatted in Markdown.
|
|
674
|
+
Text formatted in Markdown. Leading whitespace common to all lines
|
|
675
|
+
is automatically removed to support indented multi-line strings.
|
|
662
676
|
"""
|
|
663
677
|
|
|
664
678
|
REALTIME_UPDATABLE = True
|
|
665
679
|
|
|
680
|
+
@staticmethod
|
|
681
|
+
def _dedent_text(text):
|
|
682
|
+
"""Remove common leading whitespace from all lines."""
|
|
683
|
+
if text is None:
|
|
684
|
+
return None
|
|
685
|
+
return textwrap.dedent(text)
|
|
686
|
+
|
|
666
687
|
def update(self, text=None):
|
|
667
|
-
self._text = text
|
|
688
|
+
self._text = self._dedent_text(text)
|
|
668
689
|
|
|
669
690
|
def __init__(self, text=None):
|
|
670
|
-
self._text = text
|
|
691
|
+
self._text = self._dedent_text(text)
|
|
671
692
|
|
|
672
693
|
@with_default_component_id
|
|
673
694
|
@render_safely
|
|
@@ -753,6 +774,234 @@ class ProgressBar(UserComponent):
|
|
|
753
774
|
return data
|
|
754
775
|
|
|
755
776
|
|
|
777
|
+
class ValueBox(UserComponent):
|
|
778
|
+
"""
|
|
779
|
+
A Value Box component for displaying key metrics with styling and change indicators.
|
|
780
|
+
|
|
781
|
+
Inspired by Shiny's value box component, this displays a primary value with optional
|
|
782
|
+
title, subtitle, theme, and change indicators.
|
|
783
|
+
|
|
784
|
+
Example:
|
|
785
|
+
```
|
|
786
|
+
# Basic value box
|
|
787
|
+
value_box = ValueBox(
|
|
788
|
+
title="Revenue",
|
|
789
|
+
value="$1.2M",
|
|
790
|
+
subtitle="Monthly Revenue",
|
|
791
|
+
change_indicator="Up 15% from last month"
|
|
792
|
+
)
|
|
793
|
+
current.card.append(value_box)
|
|
794
|
+
|
|
795
|
+
# Themed value box
|
|
796
|
+
value_box = ValueBox(
|
|
797
|
+
title="Total Savings",
|
|
798
|
+
value=50000,
|
|
799
|
+
theme="success",
|
|
800
|
+
change_indicator="Up 30% from last month"
|
|
801
|
+
)
|
|
802
|
+
current.card.append(value_box)
|
|
803
|
+
|
|
804
|
+
# Updatable value box for real-time metrics
|
|
805
|
+
metrics_box = ValueBox(
|
|
806
|
+
title="Processing Progress",
|
|
807
|
+
value=0,
|
|
808
|
+
subtitle="Items processed"
|
|
809
|
+
)
|
|
810
|
+
current.card.append(metrics_box)
|
|
811
|
+
|
|
812
|
+
for i in range(1000):
|
|
813
|
+
metrics_box.update(value=i, change_indicator=f"Rate: {i*2}/sec")
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Parameters
|
|
817
|
+
----------
|
|
818
|
+
title : str, optional
|
|
819
|
+
The title/label for the value box (usually displayed above the value).
|
|
820
|
+
Must be 200 characters or less.
|
|
821
|
+
value : Union[str, int, float]
|
|
822
|
+
The main value to display prominently. Required parameter.
|
|
823
|
+
subtitle : str, optional
|
|
824
|
+
Additional descriptive text displayed below the title.
|
|
825
|
+
Must be 300 characters or less.
|
|
826
|
+
theme : str, optional
|
|
827
|
+
CSS class name for styling the value box. Supported themes: 'default', 'success',
|
|
828
|
+
'warning', 'danger', 'bg-gradient-indigo-purple'. Custom themes must be valid CSS class names.
|
|
829
|
+
change_indicator : str, optional
|
|
830
|
+
Text indicating change or additional context (e.g., "Up 30% VS PREVIOUS 30 DAYS").
|
|
831
|
+
Must be 200 characters or less.
|
|
832
|
+
"""
|
|
833
|
+
|
|
834
|
+
type = "valueBox"
|
|
835
|
+
|
|
836
|
+
REALTIME_UPDATABLE = True
|
|
837
|
+
|
|
838
|
+
# Valid built-in themes
|
|
839
|
+
VALID_THEMES = {
|
|
840
|
+
"default",
|
|
841
|
+
"success",
|
|
842
|
+
"warning",
|
|
843
|
+
"danger",
|
|
844
|
+
"bg-gradient-indigo-purple",
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
def __init__(
|
|
848
|
+
self,
|
|
849
|
+
title: Optional[str] = None,
|
|
850
|
+
value: Union[str, int, float] = "",
|
|
851
|
+
subtitle: Optional[str] = None,
|
|
852
|
+
theme: Optional[str] = None,
|
|
853
|
+
change_indicator: Optional[str] = None,
|
|
854
|
+
):
|
|
855
|
+
# Validate inputs
|
|
856
|
+
self._validate_title(title)
|
|
857
|
+
self._validate_value(value)
|
|
858
|
+
self._validate_subtitle(subtitle)
|
|
859
|
+
self._validate_theme(theme)
|
|
860
|
+
self._validate_change_indicator(change_indicator)
|
|
861
|
+
|
|
862
|
+
self._title = title
|
|
863
|
+
self._value = value
|
|
864
|
+
self._subtitle = subtitle
|
|
865
|
+
self._theme = theme
|
|
866
|
+
self._change_indicator = change_indicator
|
|
867
|
+
|
|
868
|
+
def update(
|
|
869
|
+
self,
|
|
870
|
+
title: Optional[str] = None,
|
|
871
|
+
value: Optional[Union[str, int, float]] = None,
|
|
872
|
+
subtitle: Optional[str] = None,
|
|
873
|
+
theme: Optional[str] = None,
|
|
874
|
+
change_indicator: Optional[str] = None,
|
|
875
|
+
):
|
|
876
|
+
"""
|
|
877
|
+
Update the value box with new data.
|
|
878
|
+
|
|
879
|
+
Parameters
|
|
880
|
+
----------
|
|
881
|
+
title : str, optional
|
|
882
|
+
New title for the value box.
|
|
883
|
+
value : Union[str, int, float], optional
|
|
884
|
+
New value to display.
|
|
885
|
+
subtitle : str, optional
|
|
886
|
+
New subtitle text.
|
|
887
|
+
theme : str, optional
|
|
888
|
+
New theme/styling class.
|
|
889
|
+
change_indicator : str, optional
|
|
890
|
+
New change indicator text.
|
|
891
|
+
"""
|
|
892
|
+
if title is not None:
|
|
893
|
+
self._validate_title(title)
|
|
894
|
+
self._title = title
|
|
895
|
+
if value is not None:
|
|
896
|
+
self._validate_value(value)
|
|
897
|
+
self._value = value
|
|
898
|
+
if subtitle is not None:
|
|
899
|
+
self._validate_subtitle(subtitle)
|
|
900
|
+
self._subtitle = subtitle
|
|
901
|
+
if theme is not None:
|
|
902
|
+
self._validate_theme(theme)
|
|
903
|
+
self._theme = theme
|
|
904
|
+
if change_indicator is not None:
|
|
905
|
+
self._validate_change_indicator(change_indicator)
|
|
906
|
+
self._change_indicator = change_indicator
|
|
907
|
+
|
|
908
|
+
def _validate_title(self, title: Optional[str]) -> None:
|
|
909
|
+
"""Validate title parameter."""
|
|
910
|
+
if title is not None:
|
|
911
|
+
if not isinstance(title, str):
|
|
912
|
+
raise TypeError(f"Title must be a string, got {type(title).__name__}")
|
|
913
|
+
if len(title) > 200:
|
|
914
|
+
raise ValueError(
|
|
915
|
+
f"Title must be 200 characters or less, got {len(title)} characters"
|
|
916
|
+
)
|
|
917
|
+
if not title.strip():
|
|
918
|
+
raise ValueError("Title cannot be empty or whitespace only")
|
|
919
|
+
|
|
920
|
+
def _validate_value(self, value: Union[str, int, float]) -> None:
|
|
921
|
+
"""Validate value parameter."""
|
|
922
|
+
if value is None:
|
|
923
|
+
raise ValueError("Value is required and cannot be None")
|
|
924
|
+
if not isinstance(value, (str, int, float)):
|
|
925
|
+
raise TypeError(
|
|
926
|
+
f"Value must be str, int, or float, got {type(value).__name__}"
|
|
927
|
+
)
|
|
928
|
+
if isinstance(value, str):
|
|
929
|
+
if len(value) > 100:
|
|
930
|
+
raise ValueError(
|
|
931
|
+
f"String value must be 100 characters or less, got {len(value)} characters"
|
|
932
|
+
)
|
|
933
|
+
if not value.strip():
|
|
934
|
+
raise ValueError("String value cannot be empty or whitespace only")
|
|
935
|
+
if isinstance(value, (int, float)):
|
|
936
|
+
if not (-1e15 <= value <= 1e15):
|
|
937
|
+
raise ValueError(
|
|
938
|
+
f"Numeric value must be between -1e15 and 1e15, got {value}"
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
def _validate_subtitle(self, subtitle: Optional[str]) -> None:
|
|
942
|
+
"""Validate subtitle parameter."""
|
|
943
|
+
if subtitle is not None:
|
|
944
|
+
if not isinstance(subtitle, str):
|
|
945
|
+
raise TypeError(
|
|
946
|
+
f"Subtitle must be a string, got {type(subtitle).__name__}"
|
|
947
|
+
)
|
|
948
|
+
if len(subtitle) > 300:
|
|
949
|
+
raise ValueError(
|
|
950
|
+
f"Subtitle must be 300 characters or less, got {len(subtitle)} characters"
|
|
951
|
+
)
|
|
952
|
+
if not subtitle.strip():
|
|
953
|
+
raise ValueError("Subtitle cannot be empty or whitespace only")
|
|
954
|
+
|
|
955
|
+
def _validate_theme(self, theme: Optional[str]) -> None:
|
|
956
|
+
"""Validate theme parameter."""
|
|
957
|
+
if theme is not None:
|
|
958
|
+
if not isinstance(theme, str):
|
|
959
|
+
raise TypeError(f"Theme must be a string, got {type(theme).__name__}")
|
|
960
|
+
if not theme.strip():
|
|
961
|
+
raise ValueError("Theme cannot be empty or whitespace only")
|
|
962
|
+
# Allow custom themes but warn if not in valid set
|
|
963
|
+
if theme not in self.VALID_THEMES:
|
|
964
|
+
import re
|
|
965
|
+
|
|
966
|
+
# Basic CSS class name validation
|
|
967
|
+
if not re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", theme):
|
|
968
|
+
raise ValueError(
|
|
969
|
+
f"Theme must be a valid CSS class name, got '{theme}'"
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
def _validate_change_indicator(self, change_indicator: Optional[str]) -> None:
|
|
973
|
+
"""Validate change_indicator parameter."""
|
|
974
|
+
if change_indicator is not None:
|
|
975
|
+
if not isinstance(change_indicator, str):
|
|
976
|
+
raise TypeError(
|
|
977
|
+
f"Change indicator must be a string, got {type(change_indicator).__name__}"
|
|
978
|
+
)
|
|
979
|
+
if len(change_indicator) > 200:
|
|
980
|
+
raise ValueError(
|
|
981
|
+
f"Change indicator must be 200 characters or less, got {len(change_indicator)} characters"
|
|
982
|
+
)
|
|
983
|
+
if not change_indicator.strip():
|
|
984
|
+
raise ValueError("Change indicator cannot be empty or whitespace only")
|
|
985
|
+
|
|
986
|
+
@with_default_component_id
|
|
987
|
+
@render_safely
|
|
988
|
+
def render(self):
|
|
989
|
+
data = {
|
|
990
|
+
"type": self.type,
|
|
991
|
+
"id": self.component_id,
|
|
992
|
+
"value": self._value,
|
|
993
|
+
}
|
|
994
|
+
if self._title is not None:
|
|
995
|
+
data["title"] = self._title
|
|
996
|
+
if self._subtitle is not None:
|
|
997
|
+
data["subtitle"] = self._subtitle
|
|
998
|
+
if self._theme is not None:
|
|
999
|
+
data["theme"] = self._theme
|
|
1000
|
+
if self._change_indicator is not None:
|
|
1001
|
+
data["change_indicator"] = self._change_indicator
|
|
1002
|
+
return data
|
|
1003
|
+
|
|
1004
|
+
|
|
756
1005
|
class VegaChart(UserComponent):
|
|
757
1006
|
type = "vegaChart"
|
|
758
1007
|
|
|
@@ -871,3 +1120,343 @@ class PythonCode(UserComponent):
|
|
|
871
1120
|
_code_component = PythonCodeComponent(self._code_string)
|
|
872
1121
|
_code_component.component_id = self.component_id
|
|
873
1122
|
return _code_component.render()
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
class EventsTimeline(UserComponent):
|
|
1126
|
+
"""
|
|
1127
|
+
An events timeline component for displaying structured log messages in real-time.
|
|
1128
|
+
|
|
1129
|
+
This component displays events in a timeline format with the latest events at the top.
|
|
1130
|
+
Each event can contain structured data including other UserComponents for rich display.
|
|
1131
|
+
|
|
1132
|
+
Example: Basic usage
|
|
1133
|
+
```python
|
|
1134
|
+
@card
|
|
1135
|
+
@step
|
|
1136
|
+
def my_step(self):
|
|
1137
|
+
from metaflow.cards import EventsTimeline
|
|
1138
|
+
from metaflow import current
|
|
1139
|
+
|
|
1140
|
+
# Create an events component
|
|
1141
|
+
events = EventsTimeline(title="Processing Events")
|
|
1142
|
+
current.card.append(events)
|
|
1143
|
+
|
|
1144
|
+
# Add events during processing
|
|
1145
|
+
for i in range(10):
|
|
1146
|
+
events.update(
|
|
1147
|
+
event_data={
|
|
1148
|
+
"timestamp": datetime.now().isoformat(),
|
|
1149
|
+
"event_type": "processing",
|
|
1150
|
+
"item_id": i,
|
|
1151
|
+
"status": "completed",
|
|
1152
|
+
"duration_ms": random.randint(100, 500)
|
|
1153
|
+
}
|
|
1154
|
+
)
|
|
1155
|
+
time.sleep(1)
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
Example: With styling and rich components
|
|
1159
|
+
```python
|
|
1160
|
+
from metaflow.cards import EventsTimeline, Markdown, PythonCode
|
|
1161
|
+
|
|
1162
|
+
events = EventsTimeline(title="Agent Actions")
|
|
1163
|
+
current.card.append(events)
|
|
1164
|
+
|
|
1165
|
+
# Event with styling
|
|
1166
|
+
events.update(
|
|
1167
|
+
event_data={
|
|
1168
|
+
"action": "tool_call",
|
|
1169
|
+
"function": "get_weather",
|
|
1170
|
+
"result": "Success"
|
|
1171
|
+
},
|
|
1172
|
+
style_theme="success"
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
# Event with rich components
|
|
1176
|
+
events.update(
|
|
1177
|
+
event_data={
|
|
1178
|
+
"action": "code_execution",
|
|
1179
|
+
"status": "completed"
|
|
1180
|
+
},
|
|
1181
|
+
payloads={
|
|
1182
|
+
"code": PythonCode(code_string="print('Hello World')"),
|
|
1183
|
+
"notes": Markdown("**Important**: This ran successfully")
|
|
1184
|
+
},
|
|
1185
|
+
style_theme="info"
|
|
1186
|
+
)
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
Parameters
|
|
1190
|
+
----------
|
|
1191
|
+
title : str, optional
|
|
1192
|
+
Title for the events timeline.
|
|
1193
|
+
max_events : int, default 100
|
|
1194
|
+
Maximum number of events to display. Older events are removed from display
|
|
1195
|
+
but total count is still tracked. Stats and relative time display are always enabled.
|
|
1196
|
+
"""
|
|
1197
|
+
|
|
1198
|
+
type = "eventsTimeline"
|
|
1199
|
+
|
|
1200
|
+
REALTIME_UPDATABLE = True
|
|
1201
|
+
|
|
1202
|
+
# Valid style themes
|
|
1203
|
+
VALID_THEMES = {
|
|
1204
|
+
"default",
|
|
1205
|
+
"success",
|
|
1206
|
+
"warning",
|
|
1207
|
+
"error",
|
|
1208
|
+
"info",
|
|
1209
|
+
"primary",
|
|
1210
|
+
"secondary",
|
|
1211
|
+
"tool_call",
|
|
1212
|
+
"ai_response",
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
def __init__(
|
|
1216
|
+
self,
|
|
1217
|
+
title: Optional[str] = None,
|
|
1218
|
+
max_events: int = 100,
|
|
1219
|
+
):
|
|
1220
|
+
self._title = title
|
|
1221
|
+
self._max_events = max_events
|
|
1222
|
+
self._events = []
|
|
1223
|
+
|
|
1224
|
+
# Metadata tracking
|
|
1225
|
+
self._total_events_count = 0
|
|
1226
|
+
self._first_event_time = None
|
|
1227
|
+
self._last_update_time = None
|
|
1228
|
+
self._finished = False
|
|
1229
|
+
|
|
1230
|
+
def update(
|
|
1231
|
+
self,
|
|
1232
|
+
event_data: dict,
|
|
1233
|
+
style_theme: Optional[str] = None,
|
|
1234
|
+
priority: Optional[str] = None,
|
|
1235
|
+
payloads: Optional[dict] = None,
|
|
1236
|
+
finished: Optional[bool] = None,
|
|
1237
|
+
):
|
|
1238
|
+
"""
|
|
1239
|
+
Add a new event to the timeline.
|
|
1240
|
+
|
|
1241
|
+
Parameters
|
|
1242
|
+
----------
|
|
1243
|
+
event_data : dict
|
|
1244
|
+
Basic event metadata (strings, numbers, simple values only).
|
|
1245
|
+
This appears in the main event display area.
|
|
1246
|
+
style_theme : str, optional
|
|
1247
|
+
Visual theme for this event. Valid values: 'default', 'success', 'warning',
|
|
1248
|
+
'error', 'info', 'primary', 'secondary', 'tool_call', 'ai_response'.
|
|
1249
|
+
priority : str, optional
|
|
1250
|
+
Priority level for the event ('low', 'normal', 'high', 'critical').
|
|
1251
|
+
Affects visual prominence.
|
|
1252
|
+
payloads : dict, optional
|
|
1253
|
+
Rich payload components that will be displayed in collapsible sections.
|
|
1254
|
+
Values must be UserComponent instances: ValueBox, Image, Markdown,
|
|
1255
|
+
Artifact, JSONViewer, YAMLViewer. VegaChart is not supported inside EventsTimeline.
|
|
1256
|
+
finished : bool, optional
|
|
1257
|
+
Mark the timeline as finished. When True, the status indicator will show
|
|
1258
|
+
"Finished" in the header.
|
|
1259
|
+
"""
|
|
1260
|
+
import time
|
|
1261
|
+
|
|
1262
|
+
# Validate style_theme
|
|
1263
|
+
if style_theme is not None and style_theme not in self.VALID_THEMES:
|
|
1264
|
+
import re
|
|
1265
|
+
|
|
1266
|
+
if not re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", style_theme):
|
|
1267
|
+
raise ValueError(
|
|
1268
|
+
f"Invalid style_theme '{style_theme}'. Must be a valid CSS class name."
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
# Validate payloads contain only allowed UserComponents
|
|
1272
|
+
if payloads is not None:
|
|
1273
|
+
allowed_components = (
|
|
1274
|
+
ValueBox,
|
|
1275
|
+
Image,
|
|
1276
|
+
Markdown,
|
|
1277
|
+
Artifact,
|
|
1278
|
+
PythonCode,
|
|
1279
|
+
_JSONViewer,
|
|
1280
|
+
_YAMLViewer,
|
|
1281
|
+
)
|
|
1282
|
+
for key, payload in payloads.items():
|
|
1283
|
+
if not isinstance(payload, allowed_components):
|
|
1284
|
+
raise TypeError(
|
|
1285
|
+
f"Payload '{key}' must be one of: ValueBox, Image, Markdown, "
|
|
1286
|
+
f"Artifact, JSONViewer, YAMLViewer. Got {type(payload).__name__}"
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1289
|
+
# Add timestamp if not provided
|
|
1290
|
+
if "timestamp" not in event_data:
|
|
1291
|
+
event_data["timestamp"] = time.time()
|
|
1292
|
+
|
|
1293
|
+
# Create event object with metadata and payloads
|
|
1294
|
+
event = {
|
|
1295
|
+
"metadata": event_data,
|
|
1296
|
+
"payloads": payloads or {},
|
|
1297
|
+
"event_id": f"event_{self._total_events_count}",
|
|
1298
|
+
"received_at": time.time(),
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
# Add styling metadata if provided
|
|
1302
|
+
if style_theme is not None:
|
|
1303
|
+
event["style_theme"] = style_theme
|
|
1304
|
+
if priority is not None:
|
|
1305
|
+
event["priority"] = priority
|
|
1306
|
+
|
|
1307
|
+
# Update metadata
|
|
1308
|
+
self._total_events_count += 1
|
|
1309
|
+
self._last_update_time = time.time()
|
|
1310
|
+
if self._first_event_time is None:
|
|
1311
|
+
self._first_event_time = time.time()
|
|
1312
|
+
|
|
1313
|
+
# Update finished status if provided
|
|
1314
|
+
if finished is not None:
|
|
1315
|
+
self._finished = finished
|
|
1316
|
+
|
|
1317
|
+
# Add the event to the beginning of the list (latest first)
|
|
1318
|
+
self._events.insert(0, event)
|
|
1319
|
+
|
|
1320
|
+
# Trim displayed events if we exceed max_events
|
|
1321
|
+
if len(self._events) > self._max_events:
|
|
1322
|
+
self._events = self._events[: self._max_events]
|
|
1323
|
+
|
|
1324
|
+
def get_stats(self) -> dict:
|
|
1325
|
+
"""
|
|
1326
|
+
Get timeline statistics.
|
|
1327
|
+
|
|
1328
|
+
Returns
|
|
1329
|
+
-------
|
|
1330
|
+
dict
|
|
1331
|
+
Statistics including total events, display count, timing info, etc.
|
|
1332
|
+
"""
|
|
1333
|
+
import time
|
|
1334
|
+
|
|
1335
|
+
current_time = time.time()
|
|
1336
|
+
|
|
1337
|
+
stats = {
|
|
1338
|
+
"total_events": self._total_events_count,
|
|
1339
|
+
"displayed_events": len(self._events),
|
|
1340
|
+
"last_update": self._last_update_time,
|
|
1341
|
+
"first_event": self._first_event_time,
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
# seconds_since_last_update removed; UI derives recency from last event timestamp
|
|
1345
|
+
|
|
1346
|
+
# Add finished status
|
|
1347
|
+
stats["finished"] = self._finished
|
|
1348
|
+
|
|
1349
|
+
if self._first_event_time and self._total_events_count > 1:
|
|
1350
|
+
runtime = self._last_update_time - self._first_event_time
|
|
1351
|
+
if runtime > 0:
|
|
1352
|
+
stats["events_per_minute"] = round(
|
|
1353
|
+
(self._total_events_count / runtime) * 60, 1
|
|
1354
|
+
)
|
|
1355
|
+
stats["total_runtime_seconds"] = round(runtime, 1)
|
|
1356
|
+
|
|
1357
|
+
return stats
|
|
1358
|
+
|
|
1359
|
+
def _render_subcomponents(self):
|
|
1360
|
+
"""
|
|
1361
|
+
Render any UserComponents within event payloads.
|
|
1362
|
+
"""
|
|
1363
|
+
rendered_events = []
|
|
1364
|
+
|
|
1365
|
+
for event in self._events:
|
|
1366
|
+
rendered_event = dict(event) # Copy event metadata
|
|
1367
|
+
|
|
1368
|
+
# Event metadata should only contain simple values (no components)
|
|
1369
|
+
rendered_event["metadata"] = event["metadata"]
|
|
1370
|
+
|
|
1371
|
+
# Render payload components
|
|
1372
|
+
rendered_payloads = {}
|
|
1373
|
+
for key, payload in event["payloads"].items():
|
|
1374
|
+
if isinstance(payload, MetaflowCardComponent):
|
|
1375
|
+
# Render the component
|
|
1376
|
+
rendered_payloads[key] = payload.render()
|
|
1377
|
+
else:
|
|
1378
|
+
# This shouldn't happen due to validation, but handle gracefully
|
|
1379
|
+
rendered_payloads[key] = str(payload)
|
|
1380
|
+
|
|
1381
|
+
rendered_event["payloads"] = rendered_payloads
|
|
1382
|
+
rendered_events.append(rendered_event)
|
|
1383
|
+
|
|
1384
|
+
return rendered_events
|
|
1385
|
+
|
|
1386
|
+
@with_default_component_id
|
|
1387
|
+
@render_safely
|
|
1388
|
+
def render(self):
|
|
1389
|
+
data = {
|
|
1390
|
+
"type": self.type,
|
|
1391
|
+
"id": self.component_id,
|
|
1392
|
+
"events": self._render_subcomponents(),
|
|
1393
|
+
"config": {
|
|
1394
|
+
"show_stats": True,
|
|
1395
|
+
"show_relative_time": True,
|
|
1396
|
+
"max_events": self._max_events,
|
|
1397
|
+
},
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if self._title is not None:
|
|
1401
|
+
data["title"] = self._title
|
|
1402
|
+
|
|
1403
|
+
# Always include stats
|
|
1404
|
+
data["stats"] = self.get_stats()
|
|
1405
|
+
|
|
1406
|
+
return data
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
# Rich viewer components
|
|
1410
|
+
class JSONViewer(_JSONViewer, UserComponent):
|
|
1411
|
+
"""
|
|
1412
|
+
A component for displaying JSON data with syntax highlighting and collapsible sections.
|
|
1413
|
+
|
|
1414
|
+
This component provides a rich view of JSON data with proper formatting, syntax highlighting,
|
|
1415
|
+
and the ability to collapse/expand sections for better readability.
|
|
1416
|
+
|
|
1417
|
+
Example:
|
|
1418
|
+
```python
|
|
1419
|
+
from metaflow.cards import JSONViewer, EventsTimeline
|
|
1420
|
+
from metaflow import current
|
|
1421
|
+
|
|
1422
|
+
# Use in events timeline
|
|
1423
|
+
events = EventsTimeline(title="API Calls")
|
|
1424
|
+
events.update({
|
|
1425
|
+
"action": "api_request",
|
|
1426
|
+
"endpoint": "/users",
|
|
1427
|
+
"payload": JSONViewer({"user_id": 123, "fields": ["name", "email"]})
|
|
1428
|
+
})
|
|
1429
|
+
|
|
1430
|
+
# Use standalone
|
|
1431
|
+
data = {"config": {"debug": True, "timeout": 30}}
|
|
1432
|
+
current.card.append(JSONViewer(data, collapsible=True))
|
|
1433
|
+
```
|
|
1434
|
+
"""
|
|
1435
|
+
|
|
1436
|
+
pass
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
class YAMLViewer(_YAMLViewer, UserComponent):
|
|
1440
|
+
"""
|
|
1441
|
+
A component for displaying YAML data with syntax highlighting and collapsible sections.
|
|
1442
|
+
|
|
1443
|
+
This component provides a rich view of YAML data with proper formatting and syntax highlighting.
|
|
1444
|
+
|
|
1445
|
+
Example:
|
|
1446
|
+
```python
|
|
1447
|
+
from metaflow.cards import YAMLViewer, EventsTimeline
|
|
1448
|
+
from metaflow import current
|
|
1449
|
+
|
|
1450
|
+
# Use in events timeline
|
|
1451
|
+
events = EventsTimeline(title="Configuration Changes")
|
|
1452
|
+
events.update({
|
|
1453
|
+
"action": "config_update",
|
|
1454
|
+
"config": YAMLViewer({
|
|
1455
|
+
"database": {"host": "localhost", "port": 5432},
|
|
1456
|
+
"features": ["auth", "logging"]
|
|
1457
|
+
})
|
|
1458
|
+
})
|
|
1459
|
+
```
|
|
1460
|
+
"""
|
|
1461
|
+
|
|
1462
|
+
pass
|