relationalai 0.11.4__py3-none-any.whl → 0.12.1__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.
- relationalai/clients/config.py +7 -0
- relationalai/clients/direct_access_client.py +113 -0
- relationalai/clients/snowflake.py +263 -189
- relationalai/clients/types.py +4 -1
- relationalai/clients/use_index_poller.py +72 -48
- relationalai/clients/util.py +9 -0
- relationalai/dsl.py +1 -2
- relationalai/early_access/metamodel/rewrite/__init__.py +5 -3
- relationalai/early_access/rel/rewrite/__init__.py +1 -1
- relationalai/environments/snowbook.py +10 -1
- relationalai/errors.py +24 -3
- relationalai/semantics/internal/annotations.py +1 -0
- relationalai/semantics/internal/internal.py +22 -3
- relationalai/semantics/lqp/builtins.py +1 -0
- relationalai/semantics/lqp/executor.py +12 -4
- relationalai/semantics/lqp/model2lqp.py +1 -0
- relationalai/semantics/lqp/passes.py +3 -4
- relationalai/semantics/{rel → lqp}/rewrite/__init__.py +6 -0
- relationalai/semantics/metamodel/builtins.py +12 -1
- relationalai/semantics/metamodel/executor.py +2 -1
- relationalai/semantics/metamodel/rewrite/__init__.py +3 -9
- relationalai/semantics/metamodel/rewrite/flatten.py +8 -7
- relationalai/semantics/reasoners/graph/core.py +1356 -258
- relationalai/semantics/rel/builtins.py +5 -1
- relationalai/semantics/rel/compiler.py +3 -3
- relationalai/semantics/rel/executor.py +20 -11
- relationalai/semantics/sql/compiler.py +2 -3
- relationalai/semantics/sql/executor/duck_db.py +8 -4
- relationalai/semantics/sql/executor/snowflake.py +1 -1
- relationalai/tools/cli.py +17 -6
- relationalai/tools/cli_controls.py +334 -352
- relationalai/tools/constants.py +1 -0
- relationalai/tools/query_utils.py +27 -0
- relationalai/util/otel_configuration.py +1 -1
- {relationalai-0.11.4.dist-info → relationalai-0.12.1.dist-info}/METADATA +5 -4
- {relationalai-0.11.4.dist-info → relationalai-0.12.1.dist-info}/RECORD +45 -45
- relationalai/semantics/metamodel/rewrite/gc_nodes.py +0 -58
- relationalai/semantics/metamodel/rewrite/list_types.py +0 -109
- /relationalai/semantics/{rel → lqp}/rewrite/cdc.py +0 -0
- /relationalai/semantics/{rel → lqp}/rewrite/extract_common.py +0 -0
- /relationalai/semantics/{metamodel → lqp}/rewrite/extract_keys.py +0 -0
- /relationalai/semantics/{metamodel → lqp}/rewrite/fd_constraints.py +0 -0
- /relationalai/semantics/{rel → lqp}/rewrite/quantify_vars.py +0 -0
- /relationalai/semantics/{metamodel → lqp}/rewrite/splinter.py +0 -0
- {relationalai-0.11.4.dist-info → relationalai-0.12.1.dist-info}/WHEEL +0 -0
- {relationalai-0.11.4.dist-info → relationalai-0.12.1.dist-info}/entry_points.txt +0 -0
- {relationalai-0.11.4.dist-info → relationalai-0.12.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,6 +9,7 @@ import shutil
|
|
|
9
9
|
import sys
|
|
10
10
|
import threading
|
|
11
11
|
import time
|
|
12
|
+
import importlib
|
|
12
13
|
from dataclasses import dataclass
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any, Callable, Dict, List, Sequence, TextIO, cast
|
|
@@ -116,6 +117,19 @@ def rich_str(string:str, style:str|None = None) -> str:
|
|
|
116
117
|
console.print(string, style=style)
|
|
117
118
|
return output.getvalue()
|
|
118
119
|
|
|
120
|
+
def _load_ipython_display() -> tuple[Any, Callable[..., Any]]:
|
|
121
|
+
"""Load IPython display helpers, raising if unavailable."""
|
|
122
|
+
try:
|
|
123
|
+
module = importlib.import_module("IPython.display")
|
|
124
|
+
except ImportError as exc: # pragma: no cover - only triggered without IPython
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
"NotebookTaskProgress requires IPython when running in a notebook environment."
|
|
127
|
+
) from exc
|
|
128
|
+
|
|
129
|
+
html_factory = getattr(module, "HTML")
|
|
130
|
+
display_fn = getattr(module, "display")
|
|
131
|
+
return html_factory, cast(Callable[..., Any], display_fn)
|
|
132
|
+
|
|
119
133
|
def nat_path(path: Path, base: Path):
|
|
120
134
|
resolved_path = path.resolve()
|
|
121
135
|
resolved_base = base.resolve()
|
|
@@ -763,34 +777,22 @@ class _TimerManager:
|
|
|
763
777
|
|
|
764
778
|
def _process_operation(self, task_id: str, op_type: str):
|
|
765
779
|
"""Process a completed delayed operation."""
|
|
780
|
+
progress = self._progress
|
|
766
781
|
if op_type == "remove_highlighting":
|
|
767
|
-
if hasattr(
|
|
768
|
-
del
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
self._progress._invalidate_cache()
|
|
772
|
-
self._progress._update_display()
|
|
773
|
-
elif hasattr(self._progress, 'highlighted_tasks') and task_id in self._progress.highlighted_tasks:
|
|
774
|
-
del self._progress.highlighted_tasks[task_id]
|
|
775
|
-
# For NotebookTaskProgress, no special update needed
|
|
782
|
+
if hasattr(progress, "_highlighted_tasks") and task_id in progress._highlighted_tasks:
|
|
783
|
+
del progress._highlighted_tasks[task_id]
|
|
784
|
+
if hasattr(progress, "_after_task_update"):
|
|
785
|
+
progress._after_task_update()
|
|
776
786
|
elif op_type == "delayed_removal":
|
|
777
|
-
if hasattr(
|
|
778
|
-
del
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
self._progress._invalidate_cache()
|
|
782
|
-
self._progress._update_display()
|
|
783
|
-
elif hasattr(self._progress, 'sub_tasks') and task_id in self._progress.sub_tasks:
|
|
784
|
-
del self._progress.sub_tasks[task_id]
|
|
785
|
-
# For NotebookTaskProgress, no special update needed
|
|
787
|
+
if hasattr(progress, "_tasks") and task_id in progress._tasks:
|
|
788
|
+
del progress._tasks[task_id]
|
|
789
|
+
if hasattr(progress, "_after_task_update"):
|
|
790
|
+
progress._after_task_update()
|
|
786
791
|
elif op_type == "delayed_hiding":
|
|
787
|
-
if hasattr(
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if hasattr(self._progress, '_invalidate_cache'):
|
|
792
|
-
self._progress._invalidate_cache()
|
|
793
|
-
self._progress._update_display()
|
|
792
|
+
if hasattr(progress, "_tasks") and task_id in progress._tasks:
|
|
793
|
+
progress._tasks[task_id].hidden = True
|
|
794
|
+
if hasattr(progress, "_after_task_update"):
|
|
795
|
+
progress._after_task_update()
|
|
794
796
|
|
|
795
797
|
def stop(self):
|
|
796
798
|
"""Stop the timer manager."""
|
|
@@ -798,7 +800,176 @@ class _TimerManager:
|
|
|
798
800
|
self._operations.clear()
|
|
799
801
|
|
|
800
802
|
|
|
801
|
-
class
|
|
803
|
+
class _TaskStateMixin:
|
|
804
|
+
"""Shared task management helpers for notebook and terminal progress displays."""
|
|
805
|
+
|
|
806
|
+
enable_highlighting: bool = True
|
|
807
|
+
|
|
808
|
+
def _init_task_state(self, *, hide_on_completion: bool = False, show_duration_summary: bool = True) -> None:
|
|
809
|
+
self.hide_on_completion = hide_on_completion
|
|
810
|
+
self.show_duration_summary = show_duration_summary
|
|
811
|
+
self._tasks: dict[str, TaskInfo] = {}
|
|
812
|
+
self._next_task_id: int = 1
|
|
813
|
+
self._highlighted_tasks: dict[str, float] = {}
|
|
814
|
+
self._process_start_time: float | None = None
|
|
815
|
+
self._process_end_time: float | None = None
|
|
816
|
+
self.main_completed: bool = False
|
|
817
|
+
self.main_failed: bool = False
|
|
818
|
+
self._timer_manager = _TimerManager(self)
|
|
819
|
+
|
|
820
|
+
def _after_task_update(self) -> None:
|
|
821
|
+
"""Hook for subclasses to react when task state changes."""
|
|
822
|
+
# Implemented by subclasses when they need to update the display immediately.
|
|
823
|
+
return None
|
|
824
|
+
|
|
825
|
+
def _generate_task_id(self) -> str:
|
|
826
|
+
"""Generate a unique task ID."""
|
|
827
|
+
task_id = f"task_{self._next_task_id}"
|
|
828
|
+
self._next_task_id += 1
|
|
829
|
+
return task_id
|
|
830
|
+
|
|
831
|
+
def add_sub_task(self, description: str, task_id: str | None = None, category: str = "general") -> str:
|
|
832
|
+
"""Add a new sub-task and return its unique ID."""
|
|
833
|
+
if task_id is None:
|
|
834
|
+
task_id = self._generate_task_id()
|
|
835
|
+
|
|
836
|
+
if task_id not in self._tasks:
|
|
837
|
+
self._tasks[task_id] = TaskInfo(description=description, category=category)
|
|
838
|
+
self._after_task_update()
|
|
839
|
+
|
|
840
|
+
return task_id
|
|
841
|
+
|
|
842
|
+
def update_sub_task(self, task_id: str, description: str) -> None:
|
|
843
|
+
"""Update an existing sub-task description."""
|
|
844
|
+
if task_id in self._tasks:
|
|
845
|
+
task_info = self._tasks[task_id]
|
|
846
|
+
if self.enable_highlighting and task_info.description != description:
|
|
847
|
+
self._highlighted_tasks[task_id] = time.time() + HIGHLIGHT_DURATION
|
|
848
|
+
self._timer_manager.schedule_highlight_removal(task_id)
|
|
849
|
+
|
|
850
|
+
task_info.description = description
|
|
851
|
+
self._after_task_update()
|
|
852
|
+
|
|
853
|
+
def complete_sub_task(self, task_id: str, record_time: bool = True) -> None:
|
|
854
|
+
"""Complete a sub-task by marking it as done."""
|
|
855
|
+
if task_id in self._tasks:
|
|
856
|
+
if task_id in self._highlighted_tasks:
|
|
857
|
+
del self._highlighted_tasks[task_id]
|
|
858
|
+
|
|
859
|
+
if not self._tasks[task_id].completed and record_time:
|
|
860
|
+
self._tasks[task_id].completed_time = time.time()
|
|
861
|
+
self._tasks[task_id].completed = True
|
|
862
|
+
|
|
863
|
+
self._after_task_update()
|
|
864
|
+
self._timer_manager.schedule_task_hiding(task_id)
|
|
865
|
+
|
|
866
|
+
def remove_sub_task(self, task_id: str, animate: bool = True) -> None:
|
|
867
|
+
"""Remove a sub-task by ID with optional completion animation."""
|
|
868
|
+
if task_id in self._tasks:
|
|
869
|
+
if task_id in self._highlighted_tasks:
|
|
870
|
+
del self._highlighted_tasks[task_id]
|
|
871
|
+
|
|
872
|
+
if animate:
|
|
873
|
+
self.complete_sub_task(task_id)
|
|
874
|
+
else:
|
|
875
|
+
del self._tasks[task_id]
|
|
876
|
+
self._after_task_update()
|
|
877
|
+
|
|
878
|
+
def update_sub_status(self, sub_status: str) -> None:
|
|
879
|
+
"""Legacy method for backward compatibility - creates/updates a default sub-task."""
|
|
880
|
+
self.add_sub_task(sub_status, "default")
|
|
881
|
+
self.update_sub_task("default", sub_status)
|
|
882
|
+
|
|
883
|
+
def update_main_status(self, message: str) -> None:
|
|
884
|
+
"""Update the main status line with custom information."""
|
|
885
|
+
if getattr(self, "description", "") != message:
|
|
886
|
+
self.description = message
|
|
887
|
+
self._after_task_update()
|
|
888
|
+
|
|
889
|
+
def update_messages(self, updater: dict[str, str]) -> None:
|
|
890
|
+
"""Update both main message and sub-status if provided."""
|
|
891
|
+
if "message" in updater:
|
|
892
|
+
self.description = updater["message"]
|
|
893
|
+
self._after_task_update()
|
|
894
|
+
if "sub_status" in updater:
|
|
895
|
+
self.update_sub_status(updater["sub_status"])
|
|
896
|
+
if "success_message" in updater:
|
|
897
|
+
self.success_message = updater["success_message"]
|
|
898
|
+
if "failure_message" in updater:
|
|
899
|
+
self.failure_message = updater["failure_message"]
|
|
900
|
+
|
|
901
|
+
def get_sub_task_count(self) -> int:
|
|
902
|
+
"""Get the current number of active sub-tasks."""
|
|
903
|
+
return len(self._tasks)
|
|
904
|
+
|
|
905
|
+
def list_sub_tasks(self) -> list[str]:
|
|
906
|
+
"""Get a list of all active sub-task IDs."""
|
|
907
|
+
return list(self._tasks.keys())
|
|
908
|
+
|
|
909
|
+
def get_task_status(self) -> str:
|
|
910
|
+
"""Get a human-readable status of current task count vs limit."""
|
|
911
|
+
return f"› Active tasks: {len(self._tasks)}"
|
|
912
|
+
|
|
913
|
+
def get_task_duration(self, task_id: str) -> float:
|
|
914
|
+
"""Get the duration of a specific task in seconds."""
|
|
915
|
+
if task_id in self._tasks:
|
|
916
|
+
return self._tasks[task_id].get_duration()
|
|
917
|
+
return 0.0
|
|
918
|
+
|
|
919
|
+
def get_completed_tasks(self) -> dict[str, TaskInfo]:
|
|
920
|
+
"""Get all completed tasks with their timing information."""
|
|
921
|
+
return {task_id: task_info for task_id, task_info in self._tasks.items() if task_info.completed}
|
|
922
|
+
|
|
923
|
+
def get_tasks_by_category(self, category: str) -> dict[str, TaskInfo]:
|
|
924
|
+
"""Get all tasks (completed or active) for a specific category."""
|
|
925
|
+
return {
|
|
926
|
+
task_id: task_info
|
|
927
|
+
for task_id, task_info in self._tasks.items()
|
|
928
|
+
if task_info.category == category
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
def get_completed_tasks_by_category(self, category: str) -> dict[str, TaskInfo]:
|
|
932
|
+
"""Get all completed tasks for a specific category."""
|
|
933
|
+
return {
|
|
934
|
+
task_id: task_info
|
|
935
|
+
for task_id, task_info in self._tasks.items()
|
|
936
|
+
if task_info.category == category and task_info.completed
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
def _clear_all_tasks(self) -> None:
|
|
940
|
+
"""Clear all tasks and related data."""
|
|
941
|
+
self._tasks.clear()
|
|
942
|
+
self._highlighted_tasks.clear()
|
|
943
|
+
|
|
944
|
+
def set_process_start_time(self) -> None:
|
|
945
|
+
"""Set the overall process start time."""
|
|
946
|
+
self._process_start_time = time.time()
|
|
947
|
+
|
|
948
|
+
def set_process_end_time(self) -> None:
|
|
949
|
+
"""Set the overall process end time."""
|
|
950
|
+
self._process_end_time = time.time()
|
|
951
|
+
|
|
952
|
+
def get_total_duration(self) -> float:
|
|
953
|
+
"""Get the total duration from first task added to last task completed."""
|
|
954
|
+
if not self._tasks:
|
|
955
|
+
return 0.0
|
|
956
|
+
|
|
957
|
+
completed_tasks = self.get_completed_tasks()
|
|
958
|
+
if not completed_tasks:
|
|
959
|
+
return 0.0
|
|
960
|
+
|
|
961
|
+
start_times = [task.added_time for task in self._tasks.values()]
|
|
962
|
+
completion_times = [task.completed_time for task in completed_tasks.values() if task.completed_time > 0]
|
|
963
|
+
|
|
964
|
+
if not start_times or not completion_times:
|
|
965
|
+
return 0.0
|
|
966
|
+
|
|
967
|
+
earliest_start = min(start_times)
|
|
968
|
+
latest_completion = max(completion_times)
|
|
969
|
+
return latest_completion - earliest_start
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
class TaskProgress(_TaskStateMixin):
|
|
802
973
|
"""A progress component that uses Rich's Live system to provide proper two-line display.
|
|
803
974
|
|
|
804
975
|
This class provides:
|
|
@@ -828,40 +999,36 @@ class TaskProgress:
|
|
|
828
999
|
self.leading_newline = leading_newline
|
|
829
1000
|
self.trailing_newline = trailing_newline
|
|
830
1001
|
self.transient = transient
|
|
831
|
-
self.
|
|
832
|
-
|
|
1002
|
+
self._init_task_state(
|
|
1003
|
+
hide_on_completion=hide_on_completion,
|
|
1004
|
+
show_duration_summary=show_duration_summary,
|
|
1005
|
+
)
|
|
1006
|
+
self.enable_highlighting = True
|
|
833
1007
|
|
|
834
1008
|
# Detect CI environment to avoid cursor control issues
|
|
835
1009
|
from ..environments import CIEnvironment
|
|
836
1010
|
self.is_ci = isinstance(runtime_env, CIEnvironment)
|
|
1011
|
+
self.is_jupyter = isinstance(runtime_env, JupyterEnvironment)
|
|
837
1012
|
|
|
838
1013
|
# Core components
|
|
839
|
-
# In CI
|
|
840
|
-
self.
|
|
1014
|
+
# In CI or Jupyter, avoid forcing terminal rendering to prevent duplicate outputs
|
|
1015
|
+
force_terminal = not self.is_ci and not self.is_jupyter
|
|
1016
|
+
force_jupyter = True if self.is_jupyter else None
|
|
1017
|
+
self.console = Console(
|
|
1018
|
+
force_terminal=force_terminal,
|
|
1019
|
+
force_jupyter=force_jupyter,
|
|
1020
|
+
)
|
|
841
1021
|
self.live = None
|
|
842
1022
|
self.main_completed = False
|
|
843
1023
|
self.main_failed = False
|
|
844
1024
|
|
|
845
|
-
# Task management - unified data structure
|
|
846
|
-
self._tasks = {} # task_id -> TaskInfo
|
|
847
|
-
self._next_task_id = 1
|
|
848
|
-
|
|
849
|
-
# Overall process timing
|
|
850
|
-
self._process_start_time = None
|
|
851
|
-
self._process_end_time = None
|
|
852
|
-
|
|
853
1025
|
# Animation state
|
|
854
1026
|
self.spinner_index = 0
|
|
855
1027
|
|
|
856
|
-
# Highlighting system
|
|
857
|
-
self._highlighted_tasks = {} # task_id -> highlight_until_time
|
|
858
|
-
|
|
859
1028
|
# Performance optimizations
|
|
860
1029
|
self._render_cache = None
|
|
861
1030
|
self._last_state_hash = None
|
|
862
1031
|
|
|
863
|
-
# Threading
|
|
864
|
-
self._timer_manager = _TimerManager(self)
|
|
865
1032
|
self._spinner_thread = None
|
|
866
1033
|
|
|
867
1034
|
def _generate_task_id(self) -> str:
|
|
@@ -979,192 +1146,31 @@ class TaskProgress:
|
|
|
979
1146
|
if self.live:
|
|
980
1147
|
self.live.update(self._render_display())
|
|
981
1148
|
|
|
982
|
-
def
|
|
983
|
-
"""
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
description: Description of the subtask
|
|
987
|
-
task_id: Optional custom task ID, if not provided one will be generated
|
|
988
|
-
category: Category for this task (e.g., "indexing", "provisioning", "change_tracking")
|
|
989
|
-
|
|
990
|
-
Returns:
|
|
991
|
-
str: The task ID for this subtask
|
|
992
|
-
"""
|
|
993
|
-
if task_id is None:
|
|
994
|
-
task_id = self._generate_task_id()
|
|
995
|
-
|
|
996
|
-
if task_id not in self._tasks:
|
|
997
|
-
self._tasks[task_id] = TaskInfo(description=description, category=category)
|
|
998
|
-
self._invalidate_cache()
|
|
999
|
-
self._update_display()
|
|
1000
|
-
|
|
1001
|
-
return task_id
|
|
1002
|
-
|
|
1003
|
-
def update_sub_task(self, task_id: str, description: str) -> None:
|
|
1004
|
-
"""Update an existing sub-task description.
|
|
1005
|
-
|
|
1006
|
-
When the description text changes from the previous value, the subtask
|
|
1007
|
-
will be highlighted in yellow for 2 seconds to make the change visible.
|
|
1008
|
-
"""
|
|
1009
|
-
if task_id in self._tasks:
|
|
1010
|
-
task_info = self._tasks[task_id]
|
|
1011
|
-
|
|
1012
|
-
# Check if text has changed
|
|
1013
|
-
if task_info.description != description:
|
|
1014
|
-
# Text has changed - set up highlighting
|
|
1015
|
-
self._highlighted_tasks[task_id] = time.time() + HIGHLIGHT_DURATION
|
|
1016
|
-
self._timer_manager.schedule_highlight_removal(task_id)
|
|
1149
|
+
def _after_task_update(self) -> None:
|
|
1150
|
+
"""Refresh the live display when task state changes."""
|
|
1151
|
+
self._invalidate_cache()
|
|
1152
|
+
self._update_display()
|
|
1017
1153
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
def complete_sub_task(self, task_id: str, record_time: bool = True) -> None:
|
|
1023
|
-
"""Complete a sub-task by marking it as done."""
|
|
1024
|
-
if task_id in self._tasks:
|
|
1025
|
-
# Remove any highlighting when completing
|
|
1026
|
-
if task_id in self._highlighted_tasks:
|
|
1027
|
-
del self._highlighted_tasks[task_id]
|
|
1028
|
-
|
|
1029
|
-
# Record completion time (only if not already completed and record_time is True)
|
|
1030
|
-
if not self._tasks[task_id].completed and record_time:
|
|
1031
|
-
self._tasks[task_id].completed_time = time.time()
|
|
1032
|
-
self._tasks[task_id].completed = True
|
|
1033
|
-
|
|
1034
|
-
self._invalidate_cache()
|
|
1035
|
-
self._update_display()
|
|
1036
|
-
|
|
1037
|
-
# Schedule hiding the task from display after a short delay
|
|
1038
|
-
# but keep it in the data structure for summary generation
|
|
1039
|
-
self._timer_manager.schedule_task_hiding(task_id)
|
|
1040
|
-
|
|
1041
|
-
def remove_sub_task(self, task_id: str, animate: bool = True) -> None:
|
|
1042
|
-
"""Remove a sub-task by ID with optional completion animation."""
|
|
1043
|
-
if task_id in self._tasks:
|
|
1044
|
-
# Remove any highlighting when removing
|
|
1045
|
-
if task_id in self._highlighted_tasks:
|
|
1046
|
-
del self._highlighted_tasks[task_id]
|
|
1047
|
-
|
|
1048
|
-
if animate:
|
|
1049
|
-
self.complete_sub_task(task_id)
|
|
1050
|
-
else:
|
|
1051
|
-
del self._tasks[task_id]
|
|
1052
|
-
self._invalidate_cache()
|
|
1053
|
-
self._update_display()
|
|
1054
|
-
|
|
1055
|
-
def update_sub_status(self, sub_status: str):
|
|
1056
|
-
"""Legacy method for backward compatibility - creates/updates a default sub-task."""
|
|
1057
|
-
self.add_sub_task(sub_status, "default")
|
|
1058
|
-
self.update_sub_task("default", sub_status)
|
|
1059
|
-
|
|
1060
|
-
def update_main_status(self, message: str):
|
|
1061
|
-
"""Update the main status line with custom information."""
|
|
1062
|
-
if self.description != message: # Only update if changed
|
|
1063
|
-
self.description = message
|
|
1064
|
-
self._invalidate_cache()
|
|
1065
|
-
self._update_display()
|
|
1066
|
-
|
|
1067
|
-
def update_messages(self, updater: dict[str, str]):
|
|
1068
|
-
"""Update both main message and sub-status if provided."""
|
|
1069
|
-
if "message" in updater:
|
|
1070
|
-
self.description = updater["message"]
|
|
1071
|
-
self._invalidate_cache()
|
|
1072
|
-
self._update_display()
|
|
1073
|
-
if "sub_status" in updater:
|
|
1074
|
-
self.update_sub_status(updater["sub_status"])
|
|
1075
|
-
if "success_message" in updater:
|
|
1076
|
-
self.success_message = updater["success_message"]
|
|
1077
|
-
if "failure_message" in updater:
|
|
1078
|
-
self.failure_message = updater["failure_message"]
|
|
1079
|
-
|
|
1080
|
-
def get_sub_task_count(self) -> int:
|
|
1081
|
-
"""Get the current number of active sub-tasks."""
|
|
1082
|
-
return len(self._tasks)
|
|
1083
|
-
|
|
1084
|
-
def list_sub_tasks(self) -> list[str]:
|
|
1085
|
-
"""Get a list of all active sub-task IDs."""
|
|
1086
|
-
return list(self._tasks.keys())
|
|
1087
|
-
|
|
1088
|
-
def get_task_status(self) -> str:
|
|
1089
|
-
"""Get a human-readable status of current task count vs limit."""
|
|
1090
|
-
current_count = len(self._tasks)
|
|
1091
|
-
return f"› Active tasks: {current_count}"
|
|
1092
|
-
|
|
1093
|
-
def get_task_duration(self, task_id: str) -> float:
|
|
1094
|
-
"""Get the duration of a specific task in seconds."""
|
|
1095
|
-
if task_id in self._tasks:
|
|
1096
|
-
return self._tasks[task_id].get_duration()
|
|
1097
|
-
return 0.0
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
def get_completed_tasks(self) -> dict[str, TaskInfo]:
|
|
1101
|
-
"""Get all completed tasks with their timing information."""
|
|
1102
|
-
return {task_id: task_info for task_id, task_info in self._tasks.items() if task_info.completed}
|
|
1103
|
-
|
|
1104
|
-
def get_tasks_by_category(self, category: str) -> dict[str, TaskInfo]:
|
|
1105
|
-
"""Get all tasks (completed and active) for a specific category."""
|
|
1106
|
-
return {task_id: task_info for task_id, task_info in self._tasks.items() if task_info.category == category}
|
|
1107
|
-
|
|
1108
|
-
def get_completed_tasks_by_category(self, category: str) -> dict[str, TaskInfo]:
|
|
1109
|
-
"""Get all completed tasks for a specific category."""
|
|
1110
|
-
return {task_id: task_info for task_id, task_info in self._tasks.items()
|
|
1111
|
-
if task_info.category == category and task_info.completed}
|
|
1112
|
-
|
|
1113
|
-
def set_process_start_time(self) -> None:
|
|
1114
|
-
"""Set the overall process start time."""
|
|
1115
|
-
self._process_start_time = time.time()
|
|
1116
|
-
|
|
1117
|
-
def set_process_end_time(self) -> None:
|
|
1118
|
-
"""Set the overall process end time."""
|
|
1119
|
-
self._process_end_time = time.time()
|
|
1120
|
-
|
|
1121
|
-
def get_total_duration(self) -> float:
|
|
1122
|
-
"""Get the total duration from first task added to last task completed."""
|
|
1123
|
-
if not self._tasks:
|
|
1124
|
-
return 0.0
|
|
1125
|
-
|
|
1126
|
-
completed_tasks = self.get_completed_tasks()
|
|
1127
|
-
if not completed_tasks:
|
|
1128
|
-
return 0.0
|
|
1129
|
-
|
|
1130
|
-
# Find earliest start time and latest completion time
|
|
1131
|
-
start_times = [task.added_time for task in self._tasks.values()]
|
|
1132
|
-
completion_times = [task.completed_time for task in completed_tasks.values() if task.completed_time > 0]
|
|
1133
|
-
|
|
1134
|
-
if not start_times or not completion_times:
|
|
1135
|
-
return 0.0
|
|
1136
|
-
|
|
1137
|
-
earliest_start = min(start_times)
|
|
1138
|
-
latest_completion = max(completion_times)
|
|
1139
|
-
|
|
1140
|
-
return latest_completion - earliest_start
|
|
1154
|
+
def _clear_all_tasks(self) -> None:
|
|
1155
|
+
"""Clear tasks and refresh display."""
|
|
1156
|
+
super()._clear_all_tasks()
|
|
1157
|
+
self._after_task_update()
|
|
1141
1158
|
|
|
1142
1159
|
def generate_summary(self, categories: dict[str, str] | None = None) -> str:
|
|
1143
|
-
"""Generate a summary of completed tasks by category.
|
|
1144
|
-
|
|
1145
|
-
Args:
|
|
1146
|
-
categories: Optional dict mapping category names to display names.
|
|
1147
|
-
Defaults to standard UseIndexPoller categories.
|
|
1148
|
-
|
|
1149
|
-
Returns:
|
|
1150
|
-
Formatted summary string, or empty string if no meaningful tasks.
|
|
1151
|
-
"""
|
|
1160
|
+
"""Generate a summary of completed tasks by category."""
|
|
1152
1161
|
if categories is None:
|
|
1153
1162
|
categories = DEFAULT_SUMMARY_CATEGORIES
|
|
1154
1163
|
|
|
1155
|
-
|
|
1156
|
-
category_durations = {}
|
|
1164
|
+
category_durations: dict[str, float] = {}
|
|
1157
1165
|
for category_name in categories:
|
|
1158
1166
|
tasks = self.get_completed_tasks_by_category(category_name)
|
|
1159
1167
|
category_durations[category_name] = _calculate_category_duration(category_name, tasks)
|
|
1160
1168
|
|
|
1161
|
-
# If there's nothing meaningful to show, return empty string
|
|
1162
1169
|
if not any(category_durations.values()):
|
|
1163
1170
|
return ""
|
|
1164
1171
|
|
|
1165
1172
|
total_duration = self.get_total_duration()
|
|
1166
1173
|
|
|
1167
|
-
# Build Rich table directly from data (not from formatted strings)
|
|
1168
1174
|
try:
|
|
1169
1175
|
from rich.console import Console
|
|
1170
1176
|
from rich.table import Table
|
|
@@ -1173,14 +1179,12 @@ class TaskProgress:
|
|
|
1173
1179
|
table.add_column("Operation", style="white")
|
|
1174
1180
|
table.add_column("Duration", style="green", justify="right")
|
|
1175
1181
|
|
|
1176
|
-
# Add total duration row
|
|
1177
1182
|
if total_duration > 0:
|
|
1178
1183
|
table.add_row(
|
|
1179
1184
|
INITIALIZATION_COMPLETED_TEXT,
|
|
1180
1185
|
format_duration(total_duration)
|
|
1181
1186
|
)
|
|
1182
1187
|
|
|
1183
|
-
# Add category rows
|
|
1184
1188
|
for category_name, display_name in categories.items():
|
|
1185
1189
|
duration = category_durations[category_name]
|
|
1186
1190
|
if duration > MIN_CATEGORY_DURATION_SECONDS:
|
|
@@ -1189,7 +1193,6 @@ class TaskProgress:
|
|
|
1189
1193
|
format_duration(duration)
|
|
1190
1194
|
)
|
|
1191
1195
|
|
|
1192
|
-
# Add blank row for spacing
|
|
1193
1196
|
table.add_row("", "")
|
|
1194
1197
|
|
|
1195
1198
|
console = Console()
|
|
@@ -1198,8 +1201,7 @@ class TaskProgress:
|
|
|
1198
1201
|
return capture.get()
|
|
1199
1202
|
|
|
1200
1203
|
except ImportError:
|
|
1201
|
-
|
|
1202
|
-
lines = []
|
|
1204
|
+
lines: list[str] = []
|
|
1203
1205
|
if total_duration > 0:
|
|
1204
1206
|
lines.append(f"{INITIALIZATION_COMPLETED_TEXT} {format_duration(total_duration)}")
|
|
1205
1207
|
|
|
@@ -1305,11 +1307,6 @@ class TaskProgress:
|
|
|
1305
1307
|
print()
|
|
1306
1308
|
self._cleanup()
|
|
1307
1309
|
|
|
1308
|
-
def _clear_all_tasks(self):
|
|
1309
|
-
"""Clear all tasks and related data."""
|
|
1310
|
-
self._tasks.clear()
|
|
1311
|
-
self._highlighted_tasks.clear()
|
|
1312
|
-
|
|
1313
1310
|
def _cleanup(self):
|
|
1314
1311
|
"""Clean up resources."""
|
|
1315
1312
|
if self.live:
|
|
@@ -1347,9 +1344,9 @@ leading_newline: bool = False, trailing_newline: bool = False, show_duration_sum
|
|
|
1347
1344
|
Automatically detects if we're in a notebook environment (Snowflake, Jupyter, etc.)
|
|
1348
1345
|
and returns the appropriate progress class.
|
|
1349
1346
|
"""
|
|
1350
|
-
from ..environments import runtime_env, SnowbookEnvironment,
|
|
1347
|
+
from ..environments import runtime_env, SnowbookEnvironment, NotebookRuntimeEnvironment
|
|
1351
1348
|
|
|
1352
|
-
if isinstance(runtime_env, (SnowbookEnvironment,
|
|
1349
|
+
if isinstance(runtime_env, (SnowbookEnvironment, NotebookRuntimeEnvironment)):
|
|
1353
1350
|
# Use NotebookTaskProgress for Snowflake and Jupyter notebooks
|
|
1354
1351
|
return NotebookTaskProgress(
|
|
1355
1352
|
description=description,
|
|
@@ -1393,7 +1390,7 @@ class SubTaskContext:
|
|
|
1393
1390
|
return False # Don't suppress exceptions
|
|
1394
1391
|
|
|
1395
1392
|
|
|
1396
|
-
class NotebookTaskProgress:
|
|
1393
|
+
class NotebookTaskProgress(_TaskStateMixin):
|
|
1397
1394
|
"""A progress component specifically designed for notebook environments like Snowflake.
|
|
1398
1395
|
|
|
1399
1396
|
This class copies the EXACT working Spinner code and adapts it for notebook use.
|
|
@@ -1413,15 +1410,8 @@ class NotebookTaskProgress:
|
|
|
1413
1410
|
self.failure_message = failure_message
|
|
1414
1411
|
self.leading_newline = leading_newline
|
|
1415
1412
|
self.trailing_newline = trailing_newline
|
|
1416
|
-
self.show_duration_summary
|
|
1417
|
-
|
|
1418
|
-
# Task management - unified data structure
|
|
1419
|
-
self._tasks = {} # task_id -> TaskInfo
|
|
1420
|
-
self._next_task_id = 1
|
|
1421
|
-
|
|
1422
|
-
# Overall process timing
|
|
1423
|
-
self._process_start_time = None
|
|
1424
|
-
self._process_end_time = None
|
|
1413
|
+
self._init_task_state(show_duration_summary=show_duration_summary)
|
|
1414
|
+
self.enable_highlighting = False
|
|
1425
1415
|
|
|
1426
1416
|
self.spinner_generator = itertools.cycle(SPINNER_FRAMES)
|
|
1427
1417
|
|
|
@@ -1438,15 +1428,10 @@ class NotebookTaskProgress:
|
|
|
1438
1428
|
self._update_lock = threading.Lock()
|
|
1439
1429
|
|
|
1440
1430
|
# Add sub-task support for TaskProgress compatibility
|
|
1441
|
-
# Note: _tasks and _next_task_id already initialized above (lines 1393-1394)
|
|
1442
|
-
self.main_completed = False
|
|
1443
1431
|
self.spinner_thread = None
|
|
1444
1432
|
self._current_subtask = ""
|
|
1445
1433
|
self.busy = False # Initialize busy state
|
|
1446
1434
|
|
|
1447
|
-
# Timer manager for delayed operations
|
|
1448
|
-
self._timer_manager = _TimerManager(self)
|
|
1449
|
-
|
|
1450
1435
|
|
|
1451
1436
|
def _generate_task_id(self) -> str:
|
|
1452
1437
|
"""Generate a unique task ID."""
|
|
@@ -1500,31 +1485,78 @@ class NotebookTaskProgress:
|
|
|
1500
1485
|
"""Update the display - notebook environments only."""
|
|
1501
1486
|
# Use lock to prevent race conditions between spinner thread and main thread
|
|
1502
1487
|
with self._update_lock:
|
|
1503
|
-
if message is None:
|
|
1504
|
-
message = self.get_message(starting=starting)
|
|
1505
1488
|
if self.is_jupyter:
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1489
|
+
_, display_fn = _load_ipython_display()
|
|
1490
|
+
|
|
1491
|
+
if message is None:
|
|
1492
|
+
lines = self._build_jupyter_lines(starting=starting)
|
|
1493
|
+
elif message == "":
|
|
1494
|
+
lines = []
|
|
1495
|
+
else:
|
|
1496
|
+
lines = [message]
|
|
1497
|
+
|
|
1498
|
+
rendered = "\n".join(lines)
|
|
1499
|
+
content = {"text/plain": rendered}
|
|
1509
1500
|
if self.display is not None:
|
|
1510
|
-
self.display.update(content)
|
|
1501
|
+
self.display.update(content, raw=True)
|
|
1511
1502
|
else:
|
|
1512
|
-
self.display =
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
return sum(wcwidth(c) for c in word)
|
|
1518
|
-
diff = width(self.last_message) - width(rich_string)
|
|
1503
|
+
self.display = display_fn(content, display_id=True, raw=True)
|
|
1504
|
+
return
|
|
1505
|
+
|
|
1506
|
+
if message is None:
|
|
1507
|
+
message = self.get_message(starting=starting)
|
|
1519
1508
|
|
|
1520
|
-
|
|
1521
|
-
sys.stdout.write(" " * DEFAULT_TERMINAL_WIDTH) # Clear with spaces
|
|
1522
|
-
sys.stdout.write("\r") # Move back to beginning
|
|
1509
|
+
rich_string = rich_str(message)
|
|
1523
1510
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1511
|
+
def width(word):
|
|
1512
|
+
return sum(wcwidth(c) for c in word)
|
|
1513
|
+
|
|
1514
|
+
diff = width(self.last_message) - width(rich_string)
|
|
1515
|
+
|
|
1516
|
+
sys.stdout.write("\r") # Move to beginning
|
|
1517
|
+
sys.stdout.write(" " * DEFAULT_TERMINAL_WIDTH) # Clear with spaces
|
|
1518
|
+
sys.stdout.write("\r") # Move back to beginning
|
|
1519
|
+
|
|
1520
|
+
sys.stdout.write(message + (" " * diff)) # Write text directly
|
|
1521
|
+
if self.in_notebook:
|
|
1522
|
+
sys.stdout.flush() # Force output
|
|
1523
|
+
self.last_message = rich_string
|
|
1524
|
+
|
|
1525
|
+
def _build_jupyter_lines(self, starting: bool) -> list[str]:
|
|
1526
|
+
"""Compose the main status and subtasks for Jupyter display."""
|
|
1527
|
+
if self.busy or starting:
|
|
1528
|
+
spinner = SPINNER_FRAMES[0] if starting else next(self.spinner_generator)
|
|
1529
|
+
main_line = f"{spinner} {self.description}"
|
|
1530
|
+
else:
|
|
1531
|
+
main_text = self.success_message or self.description
|
|
1532
|
+
main_line = f"{SUCCESS_ICON} {main_text}"
|
|
1533
|
+
|
|
1534
|
+
visible_tasks = self._collect_visible_tasks()
|
|
1535
|
+
lines = [main_line]
|
|
1536
|
+
|
|
1537
|
+
for marker, tasks in (
|
|
1538
|
+
(ARROW, visible_tasks["incomplete"]),
|
|
1539
|
+
(CHECK_MARK, visible_tasks["completed"]),
|
|
1540
|
+
):
|
|
1541
|
+
for task_info in tasks:
|
|
1542
|
+
lines.append(f" {marker} {task_info.description}")
|
|
1543
|
+
|
|
1544
|
+
return lines
|
|
1545
|
+
|
|
1546
|
+
def _collect_visible_tasks(self) -> dict[str, list["TaskInfo"]]:
|
|
1547
|
+
"""Separate visible tasks into incomplete and completed lists."""
|
|
1548
|
+
incomplete: list["TaskInfo"] = []
|
|
1549
|
+
completed: list["TaskInfo"] = []
|
|
1550
|
+
|
|
1551
|
+
for task_info in self._tasks.values():
|
|
1552
|
+
if task_info.hidden:
|
|
1553
|
+
continue
|
|
1554
|
+
if task_info.completed:
|
|
1555
|
+
completed.append(task_info)
|
|
1556
|
+
else:
|
|
1557
|
+
incomplete.append(task_info)
|
|
1558
|
+
|
|
1559
|
+
return {"incomplete": incomplete, "completed": completed}
|
|
1528
1560
|
|
|
1529
1561
|
def reset_cursor(self):
|
|
1530
1562
|
"""Reset cursor to beginning of line - notebook environments only."""
|
|
@@ -1553,114 +1585,54 @@ class NotebookTaskProgress:
|
|
|
1553
1585
|
# The spinner will now show the subtask instead of main task
|
|
1554
1586
|
|
|
1555
1587
|
def add_sub_task(self, description: str, task_id: str | None = None, category: str = "general") -> str:
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
description
|
|
1560
|
-
task_id: Optional custom task ID, if not provided one will be generated
|
|
1561
|
-
category: Category for this task (e.g., "indexing", "provisioning", "change_tracking")
|
|
1562
|
-
|
|
1563
|
-
Returns:
|
|
1564
|
-
str: The task ID for this subtask
|
|
1565
|
-
"""
|
|
1566
|
-
if task_id is None:
|
|
1567
|
-
task_id = self._generate_task_id()
|
|
1568
|
-
|
|
1569
|
-
if task_id not in self._tasks:
|
|
1570
|
-
self._tasks[task_id] = TaskInfo(description=description, category=category)
|
|
1571
|
-
|
|
1572
|
-
# Show the subtask by updating the main task text
|
|
1573
|
-
self._update_subtask_display(description)
|
|
1574
|
-
|
|
1588
|
+
task_id = super().add_sub_task(description, task_id, category)
|
|
1589
|
+
# Update spinner display with the active subtask
|
|
1590
|
+
if task_id in self._tasks:
|
|
1591
|
+
self._update_subtask_display(self._tasks[task_id].description)
|
|
1575
1592
|
return task_id
|
|
1576
1593
|
|
|
1577
1594
|
def update_sub_task(self, task_id: str, description: str) -> None:
|
|
1578
|
-
|
|
1595
|
+
super().update_sub_task(task_id, description)
|
|
1579
1596
|
if task_id in self._tasks:
|
|
1580
|
-
self._tasks[task_id].description = description
|
|
1581
|
-
# Show the updated subtask by updating the main task text
|
|
1582
1597
|
self._update_subtask_display(description)
|
|
1583
1598
|
|
|
1584
1599
|
def complete_sub_task(self, task_id: str, record_time: bool = True) -> None:
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
self._tasks[task_id].completed_time = time.time()
|
|
1590
|
-
self._tasks[task_id].completed = True
|
|
1591
|
-
|
|
1592
|
-
# Clear the subtask display when completed
|
|
1593
|
-
self._current_subtask = ""
|
|
1594
|
-
self._current_display = ""
|
|
1595
|
-
# The spinner will now show the main task again
|
|
1596
|
-
|
|
1597
|
-
# Schedule hiding the task from display after a short delay
|
|
1598
|
-
# but keep it in the data structure for summary generation
|
|
1599
|
-
self._timer_manager.schedule_task_hiding(task_id)
|
|
1600
|
+
super().complete_sub_task(task_id, record_time=record_time)
|
|
1601
|
+
# Clear the subtask display when completed
|
|
1602
|
+
self._current_subtask = ""
|
|
1603
|
+
self._current_display = ""
|
|
1600
1604
|
|
|
1601
1605
|
def remove_sub_task(self, task_id: str, animate: bool = True) -> None:
|
|
1602
1606
|
"""Remove a sub-task by ID."""
|
|
1607
|
+
task_description: str | None = None
|
|
1603
1608
|
if task_id in self._tasks:
|
|
1604
|
-
# Store description before deletion
|
|
1605
1609
|
task_description = self._tasks[task_id].description
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1610
|
+
|
|
1611
|
+
# Notebook display should drop the subtask immediately to clear the UI.
|
|
1612
|
+
super().remove_sub_task(task_id, animate=False)
|
|
1613
|
+
|
|
1614
|
+
if task_description and getattr(self, "_current_subtask", "") == task_description:
|
|
1615
|
+
self._current_subtask = ""
|
|
1616
|
+
self._current_display = ""
|
|
1617
|
+
# The spinner will now show the main task again
|
|
1612
1618
|
|
|
1613
1619
|
def update_sub_status(self, sub_status: str):
|
|
1614
1620
|
"""Legacy method for backward compatibility - creates/updates a default sub-task."""
|
|
1615
|
-
|
|
1616
|
-
self.update_sub_task("default", sub_status)
|
|
1621
|
+
super().update_sub_status(sub_status)
|
|
1617
1622
|
|
|
1618
1623
|
def update_main_status(self, message: str):
|
|
1619
1624
|
"""Update the main status line with real-time updates."""
|
|
1620
|
-
|
|
1621
|
-
# Clear any existing subtask when main status changes
|
|
1625
|
+
super().update_main_status(message)
|
|
1622
1626
|
self._current_subtask = ""
|
|
1623
1627
|
self._current_display = ""
|
|
1624
1628
|
# The spinner will now show the updated main task
|
|
1625
1629
|
|
|
1626
1630
|
def update_messages(self, updater: dict[str, str]):
|
|
1627
1631
|
"""Update both main message and sub-status if provided."""
|
|
1628
|
-
|
|
1629
|
-
self.update_main_status(updater["message"])
|
|
1630
|
-
if "sub_status" in updater:
|
|
1631
|
-
self.update_sub_status(updater["sub_status"])
|
|
1632
|
-
if "success_message" in updater:
|
|
1633
|
-
self.success_message = updater["success_message"]
|
|
1634
|
-
if "failure_message" in updater:
|
|
1635
|
-
self.failure_message = updater["failure_message"]
|
|
1632
|
+
super().update_messages(updater)
|
|
1636
1633
|
|
|
1637
|
-
def
|
|
1638
|
-
|
|
1639
|
-
return len(self._tasks)
|
|
1640
|
-
|
|
1641
|
-
def list_sub_tasks(self) -> list[str]:
|
|
1642
|
-
"""Get a list of all active sub-task IDs."""
|
|
1643
|
-
return list(self._tasks.keys())
|
|
1644
|
-
|
|
1645
|
-
def get_task_status(self) -> str:
|
|
1646
|
-
"""Get a human-readable status of current task count vs limit."""
|
|
1647
|
-
current_count = len(self._tasks)
|
|
1648
|
-
return f"› Active tasks: {current_count}"
|
|
1649
|
-
|
|
1650
|
-
def _invalidate_cache(self):
|
|
1651
|
-
"""Invalidate the render cache to force re-rendering."""
|
|
1652
|
-
# NotebookTaskProgress doesn't use caching, but we need this for API compatibility
|
|
1653
|
-
pass
|
|
1654
|
-
|
|
1655
|
-
def _update_display(self):
|
|
1656
|
-
"""Update the display if live."""
|
|
1657
|
-
# NotebookTaskProgress updates display through the update() method
|
|
1658
|
-
# This is here for API compatibility with TaskProgress
|
|
1659
|
-
pass
|
|
1660
|
-
|
|
1661
|
-
def _clear_all_tasks(self):
|
|
1662
|
-
"""Clear all tasks and related data."""
|
|
1663
|
-
self._tasks.clear()
|
|
1634
|
+
def _clear_all_tasks(self) -> None:
|
|
1635
|
+
super()._clear_all_tasks()
|
|
1664
1636
|
self._current_subtask = ""
|
|
1665
1637
|
self._current_display = ""
|
|
1666
1638
|
|
|
@@ -1700,14 +1672,20 @@ class NotebookTaskProgress:
|
|
|
1700
1672
|
# Clear the spinner line completely
|
|
1701
1673
|
self._clear_spinner_line()
|
|
1702
1674
|
|
|
1703
|
-
|
|
1675
|
+
final_message: str | None = None
|
|
1676
|
+
if self.success_message:
|
|
1704
1677
|
final_message = f"{SUCCESS_ICON} {self.success_message}"
|
|
1705
|
-
|
|
1706
|
-
|
|
1678
|
+
elif summary:
|
|
1679
|
+
final_message = f"{SUCCESS_ICON} Done"
|
|
1680
|
+
|
|
1681
|
+
if final_message:
|
|
1707
1682
|
if self.is_jupyter:
|
|
1708
|
-
self.
|
|
1683
|
+
if self.display is not None:
|
|
1684
|
+
self.display.update({"text/plain": final_message}, raw=True)
|
|
1685
|
+
else:
|
|
1686
|
+
_, display_fn = _load_ipython_display()
|
|
1687
|
+
self.display = display_fn({"text/plain": final_message}, display_id=True, raw=True)
|
|
1709
1688
|
else:
|
|
1710
|
-
# Print the success message on a clean line
|
|
1711
1689
|
print(final_message)
|
|
1712
1690
|
elif self.success_message == "":
|
|
1713
1691
|
# When there's no success message, clear the display for notebooks
|
|
@@ -1718,9 +1696,13 @@ class NotebookTaskProgress:
|
|
|
1718
1696
|
|
|
1719
1697
|
# Print summary if there are completed tasks
|
|
1720
1698
|
if summary:
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1699
|
+
if self.is_jupyter:
|
|
1700
|
+
# Use IPython display to avoid blank stdout lines in notebooks
|
|
1701
|
+
_, display_fn = _load_ipython_display()
|
|
1702
|
+
display_fn({"text/plain": summary.strip()}, raw=True)
|
|
1703
|
+
else:
|
|
1704
|
+
print()
|
|
1705
|
+
print(summary.strip()) # Summary includes visual separator line
|
|
1724
1706
|
|
|
1725
1707
|
# Skip trailing newline for Jupyter - it interferes with IPython display
|
|
1726
1708
|
if self.trailing_newline and not self.is_jupyter:
|