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.
Files changed (169) hide show
  1. metaflow/__init__.py +10 -3
  2. metaflow/_vendor/imghdr/__init__.py +186 -0
  3. metaflow/_vendor/yaml/__init__.py +427 -0
  4. metaflow/_vendor/yaml/composer.py +139 -0
  5. metaflow/_vendor/yaml/constructor.py +748 -0
  6. metaflow/_vendor/yaml/cyaml.py +101 -0
  7. metaflow/_vendor/yaml/dumper.py +62 -0
  8. metaflow/_vendor/yaml/emitter.py +1137 -0
  9. metaflow/_vendor/yaml/error.py +75 -0
  10. metaflow/_vendor/yaml/events.py +86 -0
  11. metaflow/_vendor/yaml/loader.py +63 -0
  12. metaflow/_vendor/yaml/nodes.py +49 -0
  13. metaflow/_vendor/yaml/parser.py +589 -0
  14. metaflow/_vendor/yaml/reader.py +185 -0
  15. metaflow/_vendor/yaml/representer.py +389 -0
  16. metaflow/_vendor/yaml/resolver.py +227 -0
  17. metaflow/_vendor/yaml/scanner.py +1435 -0
  18. metaflow/_vendor/yaml/serializer.py +111 -0
  19. metaflow/_vendor/yaml/tokens.py +104 -0
  20. metaflow/cards.py +4 -0
  21. metaflow/cli.py +125 -21
  22. metaflow/cli_components/init_cmd.py +1 -0
  23. metaflow/cli_components/run_cmds.py +204 -40
  24. metaflow/cli_components/step_cmd.py +160 -4
  25. metaflow/client/__init__.py +1 -0
  26. metaflow/client/core.py +198 -130
  27. metaflow/client/filecache.py +59 -32
  28. metaflow/cmd/code/__init__.py +2 -1
  29. metaflow/cmd/develop/stub_generator.py +49 -18
  30. metaflow/cmd/develop/stubs.py +9 -27
  31. metaflow/cmd/make_wrapper.py +30 -0
  32. metaflow/datastore/__init__.py +1 -0
  33. metaflow/datastore/content_addressed_store.py +40 -9
  34. metaflow/datastore/datastore_set.py +10 -1
  35. metaflow/datastore/flow_datastore.py +124 -4
  36. metaflow/datastore/spin_datastore.py +91 -0
  37. metaflow/datastore/task_datastore.py +92 -6
  38. metaflow/debug.py +5 -0
  39. metaflow/decorators.py +331 -82
  40. metaflow/extension_support/__init__.py +414 -356
  41. metaflow/extension_support/_empty_file.py +2 -2
  42. metaflow/flowspec.py +322 -82
  43. metaflow/graph.py +178 -15
  44. metaflow/includefile.py +25 -3
  45. metaflow/lint.py +94 -3
  46. metaflow/meta_files.py +13 -0
  47. metaflow/metadata_provider/metadata.py +13 -2
  48. metaflow/metaflow_config.py +66 -4
  49. metaflow/metaflow_environment.py +91 -25
  50. metaflow/metaflow_profile.py +18 -0
  51. metaflow/metaflow_version.py +16 -1
  52. metaflow/package/__init__.py +673 -0
  53. metaflow/packaging_sys/__init__.py +880 -0
  54. metaflow/packaging_sys/backend.py +128 -0
  55. metaflow/packaging_sys/distribution_support.py +153 -0
  56. metaflow/packaging_sys/tar_backend.py +99 -0
  57. metaflow/packaging_sys/utils.py +54 -0
  58. metaflow/packaging_sys/v1.py +527 -0
  59. metaflow/parameters.py +6 -2
  60. metaflow/plugins/__init__.py +6 -0
  61. metaflow/plugins/airflow/airflow.py +11 -1
  62. metaflow/plugins/airflow/airflow_cli.py +16 -5
  63. metaflow/plugins/argo/argo_client.py +42 -20
  64. metaflow/plugins/argo/argo_events.py +6 -6
  65. metaflow/plugins/argo/argo_workflows.py +1023 -344
  66. metaflow/plugins/argo/argo_workflows_cli.py +396 -94
  67. metaflow/plugins/argo/argo_workflows_decorator.py +9 -0
  68. metaflow/plugins/argo/argo_workflows_deployer_objects.py +75 -49
  69. metaflow/plugins/argo/capture_error.py +5 -2
  70. metaflow/plugins/argo/conditional_input_paths.py +35 -0
  71. metaflow/plugins/argo/exit_hooks.py +209 -0
  72. metaflow/plugins/argo/param_val.py +19 -0
  73. metaflow/plugins/aws/aws_client.py +6 -0
  74. metaflow/plugins/aws/aws_utils.py +33 -1
  75. metaflow/plugins/aws/batch/batch.py +72 -5
  76. metaflow/plugins/aws/batch/batch_cli.py +24 -3
  77. metaflow/plugins/aws/batch/batch_decorator.py +57 -6
  78. metaflow/plugins/aws/step_functions/step_functions.py +28 -3
  79. metaflow/plugins/aws/step_functions/step_functions_cli.py +49 -4
  80. metaflow/plugins/aws/step_functions/step_functions_deployer.py +3 -0
  81. metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +30 -0
  82. metaflow/plugins/cards/card_cli.py +20 -1
  83. metaflow/plugins/cards/card_creator.py +24 -1
  84. metaflow/plugins/cards/card_datastore.py +21 -49
  85. metaflow/plugins/cards/card_decorator.py +58 -6
  86. metaflow/plugins/cards/card_modules/basic.py +38 -9
  87. metaflow/plugins/cards/card_modules/bundle.css +1 -1
  88. metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
  89. metaflow/plugins/cards/card_modules/components.py +592 -3
  90. metaflow/plugins/cards/card_modules/convert_to_native_type.py +34 -5
  91. metaflow/plugins/cards/card_modules/json_viewer.py +232 -0
  92. metaflow/plugins/cards/card_modules/main.css +1 -0
  93. metaflow/plugins/cards/card_modules/main.js +56 -41
  94. metaflow/plugins/cards/card_modules/test_cards.py +22 -6
  95. metaflow/plugins/cards/component_serializer.py +1 -8
  96. metaflow/plugins/cards/metadata.py +22 -0
  97. metaflow/plugins/catch_decorator.py +9 -0
  98. metaflow/plugins/datastores/local_storage.py +12 -6
  99. metaflow/plugins/datastores/spin_storage.py +12 -0
  100. metaflow/plugins/datatools/s3/s3.py +49 -17
  101. metaflow/plugins/datatools/s3/s3op.py +113 -66
  102. metaflow/plugins/env_escape/client_modules.py +102 -72
  103. metaflow/plugins/events_decorator.py +127 -121
  104. metaflow/plugins/exit_hook/__init__.py +0 -0
  105. metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
  106. metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
  107. metaflow/plugins/kubernetes/kubernetes.py +12 -1
  108. metaflow/plugins/kubernetes/kubernetes_cli.py +11 -0
  109. metaflow/plugins/kubernetes/kubernetes_decorator.py +25 -6
  110. metaflow/plugins/kubernetes/kubernetes_job.py +12 -4
  111. metaflow/plugins/kubernetes/kubernetes_jobsets.py +31 -30
  112. metaflow/plugins/metadata_providers/local.py +76 -82
  113. metaflow/plugins/metadata_providers/service.py +13 -9
  114. metaflow/plugins/metadata_providers/spin.py +16 -0
  115. metaflow/plugins/package_cli.py +36 -24
  116. metaflow/plugins/parallel_decorator.py +11 -2
  117. metaflow/plugins/parsers.py +16 -0
  118. metaflow/plugins/pypi/bootstrap.py +7 -1
  119. metaflow/plugins/pypi/conda_decorator.py +41 -82
  120. metaflow/plugins/pypi/conda_environment.py +14 -6
  121. metaflow/plugins/pypi/micromamba.py +9 -1
  122. metaflow/plugins/pypi/pip.py +41 -5
  123. metaflow/plugins/pypi/pypi_decorator.py +4 -4
  124. metaflow/plugins/pypi/utils.py +22 -0
  125. metaflow/plugins/secrets/__init__.py +3 -0
  126. metaflow/plugins/secrets/secrets_decorator.py +14 -178
  127. metaflow/plugins/secrets/secrets_func.py +49 -0
  128. metaflow/plugins/secrets/secrets_spec.py +101 -0
  129. metaflow/plugins/secrets/utils.py +74 -0
  130. metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
  131. metaflow/plugins/timeout_decorator.py +0 -1
  132. metaflow/plugins/uv/bootstrap.py +29 -1
  133. metaflow/plugins/uv/uv_environment.py +5 -3
  134. metaflow/pylint_wrapper.py +5 -1
  135. metaflow/runner/click_api.py +79 -26
  136. metaflow/runner/deployer.py +208 -6
  137. metaflow/runner/deployer_impl.py +32 -12
  138. metaflow/runner/metaflow_runner.py +266 -33
  139. metaflow/runner/subprocess_manager.py +21 -1
  140. metaflow/runner/utils.py +27 -16
  141. metaflow/runtime.py +660 -66
  142. metaflow/task.py +255 -26
  143. metaflow/user_configs/config_options.py +33 -21
  144. metaflow/user_configs/config_parameters.py +220 -58
  145. metaflow/user_decorators/__init__.py +0 -0
  146. metaflow/user_decorators/common.py +144 -0
  147. metaflow/user_decorators/mutable_flow.py +512 -0
  148. metaflow/user_decorators/mutable_step.py +424 -0
  149. metaflow/user_decorators/user_flow_decorator.py +264 -0
  150. metaflow/user_decorators/user_step_decorator.py +749 -0
  151. metaflow/util.py +197 -7
  152. metaflow/vendor.py +23 -7
  153. metaflow/version.py +1 -1
  154. {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Makefile +13 -2
  155. {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Tiltfile +107 -7
  156. {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/pick_services.sh +1 -0
  157. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/METADATA +2 -3
  158. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/RECORD +162 -121
  159. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
  160. metaflow/_vendor/v3_5/__init__.py +0 -1
  161. metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
  162. metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
  163. metaflow/_vendor/v3_5/zipp.py +0 -329
  164. metaflow/info_file.py +0 -25
  165. metaflow/package.py +0 -203
  166. metaflow/user_configs/config_decorators.py +0 -568
  167. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +0 -0
  168. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/licenses/LICENSE +0 -0
  169. {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