From e44dcb034e670b8929c71d8ed52a5e22745014de Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Fri, 16 Jan 2026 10:35:38 -0500 Subject: [PATCH 1/5] feat: Add progress bar to CLI from feast apply Signed-off-by: Francisco Javier Arceo --- sdk/python/feast/cli/cli.py | 25 ++++++- sdk/python/feast/diff/apply_progress.py | 90 +++++++++++++++++++++++++ sdk/python/feast/diff/infra_diff.py | 23 ++++++- sdk/python/feast/feature_store.py | 35 +++++++++- sdk/python/feast/repo_operations.py | 68 ++++++++++++++----- 5 files changed, 219 insertions(+), 22 deletions(-) create mode 100644 sdk/python/feast/diff/apply_progress.py diff --git a/sdk/python/feast/cli/cli.py b/sdk/python/feast/cli/cli.py index ab756d47496..a0f810482eb 100644 --- a/sdk/python/feast/cli/cli.py +++ b/sdk/python/feast/cli/cli.py @@ -23,6 +23,7 @@ from colorama import Fore, Style from dateutil import parser from pygments import formatters, highlight, lexers +from tqdm import tqdm from feast import utils from feast.cli.data_sources import data_sources_cmd @@ -270,9 +271,17 @@ def plan_command( is_flag=True, help="Don't validate feature views. Use with caution as this skips important checks.", ) +@click.option( + "--no-progress", + is_flag=True, + help="Disable progress bars during apply operation.", +) @click.pass_context def apply_total_command( - ctx: click.Context, skip_source_validation: bool, skip_feature_view_validation: bool + ctx: click.Context, + skip_source_validation: bool, + skip_feature_view_validation: bool, + no_progress: bool, ): """ Create or update a feature store deployment @@ -282,9 +291,21 @@ def apply_total_command( cli_check_repo(repo, fs_yaml_file) repo_config = load_repo_config(repo, fs_yaml_file) + + # Create tqdm_builder for progress tracking + tqdm_builder = None + if not no_progress: + + def tqdm_builder(length): + return tqdm(total=length, ncols=100) + try: apply_total( - repo_config, repo, skip_source_validation, skip_feature_view_validation + repo_config, + repo, + skip_source_validation, + skip_feature_view_validation, + tqdm_builder=tqdm_builder, ) except FeastProviderLoginError as e: print(str(e)) diff --git a/sdk/python/feast/diff/apply_progress.py b/sdk/python/feast/diff/apply_progress.py new file mode 100644 index 00000000000..57999fcde05 --- /dev/null +++ b/sdk/python/feast/diff/apply_progress.py @@ -0,0 +1,90 @@ +""" +Progress tracking infrastructure for feast apply operations. + +This module provides the ApplyProgressContext class that manages progress bars +during apply operations, following the same tqdm_builder pattern used in +materialization operations. +""" + +from dataclasses import dataclass +from typing import Callable, Optional + +from tqdm import tqdm + + +@dataclass +class ApplyProgressContext: + """ + Context object for tracking progress during feast apply operations. + + This class manages progress bars for the main phases of apply: + 1. Planning changes (computing diffs) + 2. Updating infrastructure (table creation/deletion) + 3. Updating registry (metadata updates) + + Follows the same tqdm_builder pattern used throughout Feast for consistency. + """ + + tqdm_builder: Callable[[int], tqdm] + current_phase: str = "" + overall_progress: Optional[tqdm] = None + phase_progress: Optional[tqdm] = None + + # Phase tracking + total_phases: int = 3 + completed_phases: int = 0 + + # Infrastructure operation tracking + total_infra_operations: int = 0 + completed_infra_operations: int = 0 + + def start_overall_progress(self): + """Initialize the overall progress bar for apply phases.""" + if self.overall_progress is None: + self.overall_progress = self.tqdm_builder(self.total_phases) + self.overall_progress.set_description("Applying changes") + + def start_phase(self, phase_name: str, operations_count: int = 0): + """ + Start tracking a new phase. + + Args: + phase_name: Human-readable name of the phase + operations_count: Number of operations in this phase (0 for unknown) + """ + self.current_phase = phase_name + if operations_count > 0: + self.phase_progress = self.tqdm_builder(operations_count) + self.phase_progress.set_description(f"{phase_name}") + + def update_phase_progress(self, description: Optional[str] = None): + """ + Update progress within the current phase. + + Args: + description: Optional description of current operation + """ + if self.phase_progress: + self.phase_progress.update(1) + if description: + self.phase_progress.set_description( + f"{self.current_phase}: {description}" + ) + + def complete_phase(self): + """Mark current phase as complete and advance overall progress.""" + if self.phase_progress: + self.phase_progress.close() + self.phase_progress = None + if self.overall_progress: + self.overall_progress.update(1) + self.completed_phases += 1 + + def cleanup(self): + """Clean up all progress bars. Should be called in finally blocks.""" + if self.phase_progress: + self.phase_progress.close() + self.phase_progress = None + if self.overall_progress: + self.overall_progress.close() + self.overall_progress = None diff --git a/sdk/python/feast/diff/infra_diff.py b/sdk/python/feast/diff/infra_diff.py index b761470905a..0e5db5bce43 100644 --- a/sdk/python/feast/diff/infra_diff.py +++ b/sdk/python/feast/diff/infra_diff.py @@ -1,5 +1,8 @@ from dataclasses import dataclass -from typing import Generic, Iterable, List, Optional, Tuple, TypeVar +from typing import TYPE_CHECKING, Generic, Iterable, List, Optional, Tuple, TypeVar + +if TYPE_CHECKING: + from feast.diff.apply_progress import ApplyProgressContext from feast.diff.property_diff import PropertyDiff, TransitionType from feast.infra.infra_object import ( @@ -33,8 +36,9 @@ class InfraDiff: def __init__(self): self.infra_object_diffs = [] - def update(self): + def update(self, progress_ctx: Optional["ApplyProgressContext"] = None): """Apply the infrastructure changes specified in this object.""" + for infra_object_diff in self.infra_object_diffs: if infra_object_diff.transition_type in [ TransitionType.DELETE, @@ -43,6 +47,10 @@ def update(self): infra_object = InfraObject.from_proto( infra_object_diff.current_infra_object ) + if progress_ctx: + progress_ctx.update_phase_progress( + f"Tearing down {infra_object_diff.name}" + ) infra_object.teardown() elif infra_object_diff.transition_type in [ TransitionType.CREATE, @@ -51,8 +59,19 @@ def update(self): infra_object = InfraObject.from_proto( infra_object_diff.new_infra_object ) + if progress_ctx: + progress_ctx.update_phase_progress( + f"Creating/updating {infra_object_diff.name}" + ) infra_object.update() + # Update progress after each operation (except unchanged operations) + if ( + progress_ctx + and infra_object_diff.transition_type != TransitionType.UNCHANGED + ): + progress_ctx.update_phase_progress() + def to_string(self): from colorama import Fore, Style diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 0eff5034683..c1965543451 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -18,6 +18,7 @@ from datetime import datetime, timedelta from pathlib import Path from typing import ( + TYPE_CHECKING, Any, Callable, Dict, @@ -31,6 +32,9 @@ cast, ) +if TYPE_CHECKING: + from feast.diff.apply_progress import ApplyProgressContext + import pandas as pd import pyarrow as pa from colorama import Fore, Style @@ -726,6 +730,7 @@ def plan( self, desired_repo_contents: RepoContents, skip_feature_view_validation: bool = False, + progress_ctx: Optional["ApplyProgressContext"] = None, ) -> Tuple[RegistryDiff, InfraDiff, Infra]: """Dry-run registering objects to metadata store. @@ -793,6 +798,9 @@ def plan( self._registry, self.project, desired_repo_contents ) + if progress_ctx: + progress_ctx.update_phase_progress("Computing infrastructure diff") + # Compute the desired difference between the current infra, as stored in the registry, # and the desired infra. self._registry.refresh(project=self.project) @@ -807,7 +815,11 @@ def plan( return registry_diff, infra_diff, new_infra def _apply_diffs( - self, registry_diff: RegistryDiff, infra_diff: InfraDiff, new_infra: Infra + self, + registry_diff: RegistryDiff, + infra_diff: InfraDiff, + new_infra: Infra, + progress_ctx: Optional["ApplyProgressContext"] = None, ): """Applies the given diffs to the metadata store and infrastructure. @@ -815,14 +827,33 @@ def _apply_diffs( registry_diff: The diff between the current registry and the desired registry. infra_diff: The diff between the current infra and the desired infra. new_infra: The desired infra. + progress_ctx: Optional progress context for tracking apply progress. """ - infra_diff.update() + # Infrastructure phase + if progress_ctx: + infra_ops_count = len(infra_diff.infra_object_diffs) + progress_ctx.start_phase("Updating infrastructure", infra_ops_count) + + infra_diff.update(progress_ctx=progress_ctx) + + if progress_ctx: + progress_ctx.complete_phase() + progress_ctx.start_phase("Updating registry", 2) + + # Registry phase apply_diff_to_registry( self._registry, registry_diff, self.project, commit=False ) + if progress_ctx: + progress_ctx.update_phase_progress("Committing registry changes") + self._registry.update_infra(new_infra, self.project, commit=True) + if progress_ctx: + progress_ctx.update_phase_progress("Registry update complete") + progress_ctx.complete_phase() + def apply( self, objects: Union[ diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index b0809bdd399..9873fc8e48f 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -10,10 +10,11 @@ from importlib.abc import Loader from importlib.machinery import ModuleSpec from pathlib import Path -from typing import List, Optional, Set, Union +from typing import Callable, List, Optional, Set, Union import click from click.exceptions import BadParameter +from tqdm import tqdm from feast import PushSource from feast.batch_feature_view import BatchFeatureView @@ -342,6 +343,7 @@ def apply_total_with_repo_instance( repo: RepoContents, skip_source_validation: bool, skip_feature_view_validation: bool = False, + tqdm_builder: Optional[Callable[[int], tqdm]] = None, ): if not skip_source_validation: provider = store._get_provider() @@ -358,22 +360,54 @@ def apply_total_with_repo_instance( views_to_delete, ) = extract_objects_for_apply_delete(project_name, registry, repo) - if store._should_use_plan(): - registry_diff, infra_diff, new_infra = store.plan( - repo, skip_feature_view_validation=skip_feature_view_validation - ) - click.echo(registry_diff.to_string()) + # Create progress context if tqdm_builder is provided + progress_ctx = None + if tqdm_builder: + from feast.diff.apply_progress import ApplyProgressContext - store._apply_diffs(registry_diff, infra_diff, new_infra) - click.echo(infra_diff.to_string()) - else: - store.apply( - all_to_apply, - objects_to_delete=all_to_delete, - partial=False, - skip_feature_view_validation=skip_feature_view_validation, - ) - log_infra_changes(views_to_keep, views_to_delete) + progress_ctx = ApplyProgressContext(tqdm_builder=tqdm_builder) + progress_ctx.start_overall_progress() + + try: + if store._should_use_plan(): + # Planning phase + if progress_ctx: + progress_ctx.start_phase("Planning changes", 1) + + registry_diff, infra_diff, new_infra = store.plan( + repo, + skip_feature_view_validation=skip_feature_view_validation, + progress_ctx=progress_ctx, + ) + click.echo(registry_diff.to_string()) + + if progress_ctx: + progress_ctx.complete_phase() + + # Apply phase + store._apply_diffs( + registry_diff, infra_diff, new_infra, progress_ctx=progress_ctx + ) + click.echo(infra_diff.to_string()) + else: + # Legacy apply path - simple single phase tracking + if progress_ctx: + progress_ctx.start_phase("Applying changes", 1) + + store.apply( + all_to_apply, + objects_to_delete=all_to_delete, + partial=False, + skip_feature_view_validation=skip_feature_view_validation, + ) + log_infra_changes(views_to_keep, views_to_delete) + + if progress_ctx: + progress_ctx.update_phase_progress() + progress_ctx.complete_phase() + finally: + if progress_ctx: + progress_ctx.cleanup() def log_infra_changes( @@ -416,6 +450,7 @@ def apply_total( repo_path: Path, skip_source_validation: bool, skip_feature_view_validation: bool = False, + tqdm_builder: Optional[Callable[[int], tqdm]] = None, ): os.chdir(repo_path) repo = _get_repo_contents(repo_path, repo_config.project, repo_config) @@ -437,6 +472,7 @@ def apply_total( repo, skip_source_validation, skip_feature_view_validation, + tqdm_builder=tqdm_builder, ) From f5ab7749c3f4c46e950ecea8b7067b12527d7f84 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Fri, 16 Jan 2026 13:31:21 -0500 Subject: [PATCH 2/5] format nicer Signed-off-by: Francisco Javier Arceo --- sdk/python/feast/cli/cli.py | 11 +-- sdk/python/feast/diff/apply_progress.py | 99 ++++++++++++++++++------- sdk/python/feast/diff/infra_diff.py | 7 -- sdk/python/feast/feature_store.py | 41 +++++----- sdk/python/feast/repo_operations.py | 42 +++-------- 5 files changed, 112 insertions(+), 88 deletions(-) diff --git a/sdk/python/feast/cli/cli.py b/sdk/python/feast/cli/cli.py index a0f810482eb..6667a8464d9 100644 --- a/sdk/python/feast/cli/cli.py +++ b/sdk/python/feast/cli/cli.py @@ -23,7 +23,6 @@ from colorama import Fore, Style from dateutil import parser from pygments import formatters, highlight, lexers -from tqdm import tqdm from feast import utils from feast.cli.data_sources import data_sources_cmd @@ -292,12 +291,11 @@ def apply_total_command( repo_config = load_repo_config(repo, fs_yaml_file) - # Create tqdm_builder for progress tracking - tqdm_builder = None - if not no_progress: + # Set environment variable to disable progress if requested + if no_progress: + import os - def tqdm_builder(length): - return tqdm(total=length, ncols=100) + os.environ["FEAST_NO_PROGRESS"] = "1" try: apply_total( @@ -305,7 +303,6 @@ def tqdm_builder(length): repo, skip_source_validation, skip_feature_view_validation, - tqdm_builder=tqdm_builder, ) except FeastProviderLoginError as e: print(str(e)) diff --git a/sdk/python/feast/diff/apply_progress.py b/sdk/python/feast/diff/apply_progress.py index 57999fcde05..92f88885e8f 100644 --- a/sdk/python/feast/diff/apply_progress.py +++ b/sdk/python/feast/diff/apply_progress.py @@ -1,48 +1,69 @@ """ -Progress tracking infrastructure for feast apply operations. +Enhanced progress tracking infrastructure for feast apply operations. -This module provides the ApplyProgressContext class that manages progress bars -during apply operations, following the same tqdm_builder pattern used in -materialization operations. +This module provides the ApplyProgressContext class that manages positioned, +color-coded progress bars during apply operations with fixed-width formatting +for perfect alignment. """ from dataclasses import dataclass -from typing import Callable, Optional +from typing import Optional from tqdm import tqdm +from feast.diff.progress_utils import ( + create_positioned_tqdm, + get_color_for_phase, + is_tty_available, +) + @dataclass class ApplyProgressContext: """ - Context object for tracking progress during feast apply operations. + Enhanced context object for tracking progress during feast apply operations. - This class manages progress bars for the main phases of apply: - 1. Planning changes (computing diffs) - 2. Updating infrastructure (table creation/deletion) - 3. Updating registry (metadata updates) + This class manages multiple positioned progress bars with fixed-width formatting: + 1. Overall progress (position 0) - tracks main phases + 2. Phase progress (position 1) - tracks operations within current phase - Follows the same tqdm_builder pattern used throughout Feast for consistency. + Features: + - Fixed-width alignment for perfect visual consistency + - Color-coded progress bars by phase + - Position coordination to prevent overlap + - TTY detection for CI/CD compatibility """ - tqdm_builder: Callable[[int], tqdm] + # Core tracking state current_phase: str = "" overall_progress: Optional[tqdm] = None phase_progress: Optional[tqdm] = None - # Phase tracking + # Progress tracking total_phases: int = 3 completed_phases: int = 0 + tty_available: bool = True + + # Position allocation + OVERALL_POSITION = 0 + PHASE_POSITION = 1 - # Infrastructure operation tracking - total_infra_operations: int = 0 - completed_infra_operations: int = 0 + def __post_init__(self): + """Initialize TTY detection after dataclass creation.""" + self.tty_available = is_tty_available() def start_overall_progress(self): """Initialize the overall progress bar for apply phases.""" + if not self.tty_available: + return + if self.overall_progress is None: - self.overall_progress = self.tqdm_builder(self.total_phases) - self.overall_progress.set_description("Applying changes") + self.overall_progress = create_positioned_tqdm( + position=self.OVERALL_POSITION, + description="Applying changes", + total=self.total_phases, + color=get_color_for_phase("overall"), + ) def start_phase(self, phase_name: str, operations_count: int = 0): """ @@ -52,10 +73,24 @@ def start_phase(self, phase_name: str, operations_count: int = 0): phase_name: Human-readable name of the phase operations_count: Number of operations in this phase (0 for unknown) """ + if not self.tty_available: + return + self.current_phase = phase_name + + # Close previous phase progress if exists + if self.phase_progress: + self.phase_progress.close() + self.phase_progress = None + + # Create new phase progress bar if operations are known if operations_count > 0: - self.phase_progress = self.tqdm_builder(operations_count) - self.phase_progress.set_description(f"{phase_name}") + self.phase_progress = create_positioned_tqdm( + position=self.PHASE_POSITION, + description=phase_name, + total=operations_count, + color=get_color_for_phase(phase_name.lower()), + ) def update_phase_progress(self, description: Optional[str] = None): """ @@ -64,20 +99,32 @@ def update_phase_progress(self, description: Optional[str] = None): Args: description: Optional description of current operation """ - if self.phase_progress: - self.phase_progress.update(1) - if description: - self.phase_progress.set_description( - f"{self.current_phase}: {description}" - ) + if not self.tty_available or not self.phase_progress: + return + + if description: + # Update postfix with current operation + self.phase_progress.set_postfix_str(description) + + self.phase_progress.update(1) def complete_phase(self): """Mark current phase as complete and advance overall progress.""" + if not self.tty_available: + return + + # Close phase progress if self.phase_progress: self.phase_progress.close() self.phase_progress = None + + # Update overall progress if self.overall_progress: self.overall_progress.update(1) + # Update postfix with phase completion + phase_text = f"({self.completed_phases + 1}/{self.total_phases} phases)" + self.overall_progress.set_postfix_str(phase_text) + self.completed_phases += 1 def cleanup(self): diff --git a/sdk/python/feast/diff/infra_diff.py b/sdk/python/feast/diff/infra_diff.py index 0e5db5bce43..2fa9e8e882e 100644 --- a/sdk/python/feast/diff/infra_diff.py +++ b/sdk/python/feast/diff/infra_diff.py @@ -65,13 +65,6 @@ def update(self, progress_ctx: Optional["ApplyProgressContext"] = None): ) infra_object.update() - # Update progress after each operation (except unchanged operations) - if ( - progress_ctx - and infra_object_diff.transition_type != TransitionType.UNCHANGED - ): - progress_ctx.update_phase_progress() - def to_string(self): from colorama import Fore, Style diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index c1965543451..fc4517281d3 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -829,30 +829,35 @@ def _apply_diffs( new_infra: The desired infra. progress_ctx: Optional progress context for tracking apply progress. """ - # Infrastructure phase - if progress_ctx: - infra_ops_count = len(infra_diff.infra_object_diffs) - progress_ctx.start_phase("Updating infrastructure", infra_ops_count) + try: + # Infrastructure phase + if progress_ctx: + infra_ops_count = len(infra_diff.infra_object_diffs) + progress_ctx.start_phase("Updating infrastructure", infra_ops_count) - infra_diff.update(progress_ctx=progress_ctx) + infra_diff.update(progress_ctx=progress_ctx) - if progress_ctx: - progress_ctx.complete_phase() - progress_ctx.start_phase("Updating registry", 2) + if progress_ctx: + progress_ctx.complete_phase() + progress_ctx.start_phase("Updating registry", 2) - # Registry phase - apply_diff_to_registry( - self._registry, registry_diff, self.project, commit=False - ) + # Registry phase + apply_diff_to_registry( + self._registry, registry_diff, self.project, commit=False + ) - if progress_ctx: - progress_ctx.update_phase_progress("Committing registry changes") + if progress_ctx: + progress_ctx.update_phase_progress("Committing registry changes") - self._registry.update_infra(new_infra, self.project, commit=True) + self._registry.update_infra(new_infra, self.project, commit=True) - if progress_ctx: - progress_ctx.update_phase_progress("Registry update complete") - progress_ctx.complete_phase() + if progress_ctx: + progress_ctx.update_phase_progress("Registry update complete") + progress_ctx.complete_phase() + finally: + # Always cleanup progress bars + if progress_ctx: + progress_ctx.cleanup() def apply( self, diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index 9873fc8e48f..fa5d297752a 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -10,11 +10,10 @@ from importlib.abc import Loader from importlib.machinery import ModuleSpec from pathlib import Path -from typing import Callable, List, Optional, Set, Union +from typing import List, Optional, Set, Union import click from click.exceptions import BadParameter -from tqdm import tqdm from feast import PushSource from feast.batch_feature_view import BatchFeatureView @@ -343,7 +342,6 @@ def apply_total_with_repo_instance( repo: RepoContents, skip_source_validation: bool, skip_feature_view_validation: bool = False, - tqdm_builder: Optional[Callable[[int], tqdm]] = None, ): if not skip_source_validation: provider = store._get_provider() @@ -360,29 +358,22 @@ def apply_total_with_repo_instance( views_to_delete, ) = extract_objects_for_apply_delete(project_name, registry, repo) - # Create progress context if tqdm_builder is provided - progress_ctx = None - if tqdm_builder: - from feast.diff.apply_progress import ApplyProgressContext - - progress_ctx = ApplyProgressContext(tqdm_builder=tqdm_builder) - progress_ctx.start_overall_progress() - try: if store._should_use_plan(): - # Planning phase - if progress_ctx: - progress_ctx.start_phase("Planning changes", 1) - + # Planning phase - compute diffs first without progress bars registry_diff, infra_diff, new_infra = store.plan( repo, skip_feature_view_validation=skip_feature_view_validation, - progress_ctx=progress_ctx, ) click.echo(registry_diff.to_string()) - if progress_ctx: - progress_ctx.complete_phase() + # Only show progress bars if there are actual infrastructure changes + progress_ctx = None + if len(infra_diff.infra_object_diffs) > 0: + from feast.diff.apply_progress import ApplyProgressContext + + progress_ctx = ApplyProgressContext() + progress_ctx.start_overall_progress() # Apply phase store._apply_diffs( @@ -390,10 +381,7 @@ def apply_total_with_repo_instance( ) click.echo(infra_diff.to_string()) else: - # Legacy apply path - simple single phase tracking - if progress_ctx: - progress_ctx.start_phase("Applying changes", 1) - + # Legacy apply path - no progress bars for legacy path store.apply( all_to_apply, objects_to_delete=all_to_delete, @@ -401,13 +389,9 @@ def apply_total_with_repo_instance( skip_feature_view_validation=skip_feature_view_validation, ) log_infra_changes(views_to_keep, views_to_delete) - - if progress_ctx: - progress_ctx.update_phase_progress() - progress_ctx.complete_phase() finally: - if progress_ctx: - progress_ctx.cleanup() + # Cleanup is handled in the new _apply_diffs method + pass def log_infra_changes( @@ -450,7 +434,6 @@ def apply_total( repo_path: Path, skip_source_validation: bool, skip_feature_view_validation: bool = False, - tqdm_builder: Optional[Callable[[int], tqdm]] = None, ): os.chdir(repo_path) repo = _get_repo_contents(repo_path, repo_config.project, repo_config) @@ -472,7 +455,6 @@ def apply_total( repo, skip_source_validation, skip_feature_view_validation, - tqdm_builder=tqdm_builder, ) From d43c39a91a1d914019f4451fa649ff39f1599793 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Fri, 16 Jan 2026 14:03:43 -0500 Subject: [PATCH 3/5] fix Signed-off-by: Francisco Javier Arceo --- sdk/python/feast/diff/apply_progress.py | 101 +++++++++++++++++------- 1 file changed, 71 insertions(+), 30 deletions(-) diff --git a/sdk/python/feast/diff/apply_progress.py b/sdk/python/feast/diff/apply_progress.py index 92f88885e8f..d59ce49f01d 100644 --- a/sdk/python/feast/diff/apply_progress.py +++ b/sdk/python/feast/diff/apply_progress.py @@ -11,11 +11,25 @@ from tqdm import tqdm -from feast.diff.progress_utils import ( - create_positioned_tqdm, - get_color_for_phase, - is_tty_available, -) +try: + from feast.diff.progress_utils import ( + create_positioned_tqdm, + get_color_for_phase, + is_tty_available, + ) + _PROGRESS_UTILS_AVAILABLE = True +except ImportError: + # Graceful fallback when progress_utils is not available (e.g., in tests) + _PROGRESS_UTILS_AVAILABLE = False + + def create_positioned_tqdm(*args, **kwargs): + return None + + def get_color_for_phase(phase): + return "blue" + + def is_tty_available(): + return False @dataclass @@ -50,7 +64,7 @@ class ApplyProgressContext: def __post_init__(self): """Initialize TTY detection after dataclass creation.""" - self.tty_available = is_tty_available() + self.tty_available = _PROGRESS_UTILS_AVAILABLE and is_tty_available() def start_overall_progress(self): """Initialize the overall progress bar for apply phases.""" @@ -58,12 +72,16 @@ def start_overall_progress(self): return if self.overall_progress is None: - self.overall_progress = create_positioned_tqdm( - position=self.OVERALL_POSITION, - description="Applying changes", - total=self.total_phases, - color=get_color_for_phase("overall"), - ) + try: + self.overall_progress = create_positioned_tqdm( + position=self.OVERALL_POSITION, + description="Applying changes", + total=self.total_phases, + color=get_color_for_phase("overall"), + ) + except (TypeError, AttributeError): + # Handle case where fallback functions don't work as expected + self.overall_progress = None def start_phase(self, phase_name: str, operations_count: int = 0): """ @@ -80,17 +98,24 @@ def start_phase(self, phase_name: str, operations_count: int = 0): # Close previous phase progress if exists if self.phase_progress: - self.phase_progress.close() + try: + self.phase_progress.close() + except (AttributeError, TypeError): + pass self.phase_progress = None # Create new phase progress bar if operations are known if operations_count > 0: - self.phase_progress = create_positioned_tqdm( - position=self.PHASE_POSITION, - description=phase_name, - total=operations_count, - color=get_color_for_phase(phase_name.lower()), - ) + try: + self.phase_progress = create_positioned_tqdm( + position=self.PHASE_POSITION, + description=phase_name, + total=operations_count, + color=get_color_for_phase(phase_name.lower()), + ) + except (TypeError, AttributeError): + # Handle case where fallback functions don't work as expected + self.phase_progress = None def update_phase_progress(self, description: Optional[str] = None): """ @@ -102,11 +127,15 @@ def update_phase_progress(self, description: Optional[str] = None): if not self.tty_available or not self.phase_progress: return - if description: - # Update postfix with current operation - self.phase_progress.set_postfix_str(description) + try: + if description: + # Update postfix with current operation + self.phase_progress.set_postfix_str(description) - self.phase_progress.update(1) + self.phase_progress.update(1) + except (AttributeError, TypeError): + # Handle case where phase_progress is None or fallback function returned None + pass def complete_phase(self): """Mark current phase as complete and advance overall progress.""" @@ -115,23 +144,35 @@ def complete_phase(self): # Close phase progress if self.phase_progress: - self.phase_progress.close() + try: + self.phase_progress.close() + except (AttributeError, TypeError): + pass self.phase_progress = None # Update overall progress if self.overall_progress: - self.overall_progress.update(1) - # Update postfix with phase completion - phase_text = f"({self.completed_phases + 1}/{self.total_phases} phases)" - self.overall_progress.set_postfix_str(phase_text) + try: + self.overall_progress.update(1) + # Update postfix with phase completion + phase_text = f"({self.completed_phases + 1}/{self.total_phases} phases)" + self.overall_progress.set_postfix_str(phase_text) + except (AttributeError, TypeError): + pass self.completed_phases += 1 def cleanup(self): """Clean up all progress bars. Should be called in finally blocks.""" if self.phase_progress: - self.phase_progress.close() + try: + self.phase_progress.close() + except (AttributeError, TypeError): + pass self.phase_progress = None if self.overall_progress: - self.overall_progress.close() + try: + self.overall_progress.close() + except (AttributeError, TypeError): + pass self.overall_progress = None From 9ad2b874f0d0f5d23126661ca7a896518d96fe50 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Fri, 16 Jan 2026 14:36:08 -0500 Subject: [PATCH 4/5] fix Signed-off-by: Francisco Javier Arceo --- sdk/python/feast/diff/apply_progress.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/python/feast/diff/apply_progress.py b/sdk/python/feast/diff/apply_progress.py index d59ce49f01d..1a64574e785 100644 --- a/sdk/python/feast/diff/apply_progress.py +++ b/sdk/python/feast/diff/apply_progress.py @@ -17,6 +17,7 @@ get_color_for_phase, is_tty_available, ) + _PROGRESS_UTILS_AVAILABLE = True except ImportError: # Graceful fallback when progress_utils is not available (e.g., in tests) From 1c199ce2718104f81ffb9bc0100bcffa27f7fed7 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Fri, 16 Jan 2026 14:44:57 -0500 Subject: [PATCH 5/5] fix Signed-off-by: Francisco Javier Arceo --- sdk/python/feast/diff/apply_progress.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/diff/apply_progress.py b/sdk/python/feast/diff/apply_progress.py index 1a64574e785..7d12ecef018 100644 --- a/sdk/python/feast/diff/apply_progress.py +++ b/sdk/python/feast/diff/apply_progress.py @@ -23,13 +23,19 @@ # Graceful fallback when progress_utils is not available (e.g., in tests) _PROGRESS_UTILS_AVAILABLE = False - def create_positioned_tqdm(*args, **kwargs): + def create_positioned_tqdm( + position: int, + description: str, + total: int, + color: str = "blue", + postfix: Optional[str] = None, + ) -> Optional[tqdm]: return None - def get_color_for_phase(phase): + def get_color_for_phase(phase: str) -> str: return "blue" - def is_tty_available(): + def is_tty_available() -> bool: return False