Visitar URL original
feat: Add progress bar to CLI from feast apply by franciscojavierarceo · Pull Request #5867 · feast-dev/feast · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
format nicer
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
  • Loading branch information
franciscojavierarceo committed Jan 16, 2026
commit f5ab7749c3f4c46e950ecea8b7067b12527d7f84
11 changes: 4 additions & 7 deletions sdk/python/feast/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -292,20 +291,18 @@ 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(
repo_config,
repo,
skip_source_validation,
skip_feature_view_validation,
tqdm_builder=tqdm_builder,
)
except FeastProviderLoginError as e:
print(str(e))
Expand Down
99 changes: 73 additions & 26 deletions sdk/python/feast/diff/apply_progress.py
Original file line number Diff line number Diff line change
@@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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):
Expand Down
7 changes: 0 additions & 7 deletions sdk/python/feast/diff/infra_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 23 additions & 18 deletions sdk/python/feast/feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
42 changes: 12 additions & 30 deletions sdk/python/feast/repo_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -360,54 +358,40 @@ 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(
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)

# Legacy apply path - no progress bars for legacy path
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()
# Cleanup is handled in the new _apply_diffs method
pass


def log_infra_changes(
Expand Down Expand Up @@ -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)
Expand All @@ -472,7 +455,6 @@ def apply_total(
repo,
skip_source_validation,
skip_feature_view_validation,
tqdm_builder=tqdm_builder,
)


Expand Down
Loading