Skip to content

Conversation

rhatdan
Copy link
Member

@rhatdan rhatdan commented Jul 6, 2025

Summary by Sourcery

Ensure all progress and error messages are written to stderr and centralize download logic with retries, checksum verification, and HTTP error handling in HttpClient

New Features:

  • Introduce download_file with retry logic and exponential backoff
  • Add download_and_verify wrapper to verify checksums and retry on mismatch

Enhancements:

  • Replace print calls with perror to redirect progress and error messages to stderr
  • Relocate download utilities from common to http_client and define HTTP_NOT_FOUND/HTTP_RANGE_NOT_SATISFIABLE constants
  • Automatically disable progress output when stdout is not a TTY
  • Fix spelling in test docstrings

Copy link
Contributor

sourcery-ai bot commented Jul 6, 2025

Reviewer's Guide

This PR redirects all user-facing progress and error outputs from stdout to stderr by replacing print calls with perror, consolidates and enhances download logic in HttpClient with retry/backoff and checksum verification, and removes duplicate constants and download functions.

Sequence diagram for download_file with retries and error handling

sequenceDiagram
    participant User
    participant download_file
    participant HttpClient
    participant console
    participant verify_checksum
    User->>download_file: call download_file(url, dest_path, ...)
    loop up to max_retries
        download_file->>HttpClient: init(url, headers, output_file, show_progress)
        alt HTTPError (404/416)
            download_file-->>User: raise error
        else URLError
            download_file->>console: error("Network Error: ...")
        else TimeoutError
            download_file->>console: warning("TimeoutError ...")
        else RuntimeError
            download_file->>console: warning("RuntimeError ...")
        else IOError
            download_file->>console: warning("I/O Error ...")
        else Exception
            download_file->>console: error("Unexpected error ...")
            download_file-->>User: raise error
        end
        download_file->>download_file: sleep (exponential backoff)
    end
    alt retries exceeded
        download_file-->>User: raise ConnectionError
    end
Loading

Class diagram for updated HttpClient and download logic

classDiagram
    class HttpClient {
        +__init__()
        +init(url, headers, output_file, show_progress)
        +perform_download(file, show_progress)
        +human_readable_time(seconds)
        +set_resume_point(output_file)
        +print_progress(progress_prefix, progress_bar, progress_suffix)
        +update_progress(chunk_size)
        +calculate_speed(now_downloaded, start_time)
    }
    class File
    class console
    class download_file {
        +download_file(url: str, dest_path: str, headers: dict[str, str] | None = None, show_progress: bool = True)
    }
    class download_and_verify {
        +download_and_verify(url: str, target_path: str, max_retries: int = 2)
    }
    HttpClient <.. download_file : uses
    download_file <.. download_and_verify : uses
    console <.. download_file : uses
    console <.. download_and_verify : uses
    File <.. HttpClient : uses
    verify_checksum <.. download_and_verify : uses
Loading

Class diagram for perror and error output redirection

classDiagram
    class console {
        +error(msg)
        +warning(msg)
    }
    class perror {
        +perror(msg, ...)
    }
    console <.. perror : uses
    note for perror "perror replaces print for error/progress messages, outputs to stderr"
Loading

File-Level Changes

Change Details Files
Redirect error and progress messages to stderr across modules
  • Replace print() with perror() for progress updates
  • Replace print() with perror() for error and status messages
  • Swap print-based debug outputs for logger.debug() in retry loops
ramalama/http_client.py
ramalama/common.py
ramalama/model.py
ramalama/oci.py
ramalama/chat.py
ramalama/hf_style_repo_base.py
ramalama/ollama.py
ramalama/rag.py
ramalama/cli.py
ramalama/engine.py
ramalama/huggingface.py
ramalama/ollama_repo_utils.py
Refactor and enhance HttpClient download routines
  • Introduce download_file() with retry loops, error handling and exponential backoff
  • Define HTTP status code constants (404, 416) and import sys for tty detection
  • Remove duplicate download functions and constants from common.py
  • Add download_and_verify() to wrap download_file() with checksum validation
ramalama/http_client.py
ramalama/common.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rhatdan - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `ramalama/http_client.py:154` </location>
<code_context>
+def download_file(url: str, dest_path: str, headers: dict[str, str] | None = None, show_progress: bool = True):
</code_context>

<issue_to_address>
The retry loop in download_file may not handle KeyboardInterrupt gracefully.

Explicitly handle KeyboardInterrupt to ensure a clean exit and avoid logging it as an 'Unexpected error.'

Suggested implementation:

```python
def download_file(url: str, dest_path: str, headers: dict[str, str] | None = None, show_progress: bool = True):
    """
    Downloads a file from a given URL to a specified destination path.

    Args:
        url (str): The URL to download from.
        dest_path (str): The path to save the downloaded file.
        headers (dict): Optional headers to include in the request.
        show_progress (bool): Whether to show a progress bar during download.

    Raises:
    KeyboardInterrupt: If the download is interrupted by the user.

```

```python
    Raises:
    """
    max_retries = 3
    for attempt in range(max_retries):
        try:
            # ... (existing download logic)
            break
        except KeyboardInterrupt:
            print("\nDownload interrupted by user. Exiting cleanly.")
            return
        except Exception as e:
            if attempt < max_retries - 1:
                print(f"Error occurred: {e}. Retrying ({attempt + 1}/{max_retries})...")
                continue
            else:
                print("Unexpected error:", e)
                raise

```

You will need to merge the new retry loop and exception handling with your existing download logic inside `download_file`. Make sure to place the `except KeyboardInterrupt` before the generic `except Exception` to ensure it is handled explicitly and not logged as an 'Unexpected error.'
</issue_to_address>

### Comment 2
<location> `ramalama/common.py:64` </location>
<code_context>
             return True
         if user_input in ["no", "n"]:
             return False
-        print("Invalid input. Please enter 'yes' or 'no'.")
+        perror("Invalid input. Please enter 'yes' or 'no'.")

</code_context>

<issue_to_address>
Switching to perror for user prompts may not be ideal for all user-facing messages.

Consider keeping this prompt on stdout to avoid confusing users with non-error messages on stderr.
</issue_to_address>

### Comment 3
<location> `ramalama/model.py:845` </location>
<code_context>
     def print_pull_message(self, model_name):
-        print(f"Downloading {model_name} ...")
-        print(f"Trying to pull {model_name} ...")
+        # Write messages to stderr
+        perror(f"Downloading {model_name} ...")
+        perror(f"Trying to pull {model_name} ...")


</code_context>

<issue_to_address>
Both pull messages are now sent to stderr, which may not be necessary.

Evaluate if informational messages like 'Downloading' and 'Trying to pull' should be sent to stderr, as this may impact script output parsing. Typically, only errors or warnings are sent to stderr.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +154 to +163
def download_file(url: str, dest_path: str, headers: dict[str, str] | None = None, show_progress: bool = True):
"""
Downloads a file from a given URL to a specified destination path.

Args:
url (str): The URL to download from.
dest_path (str): The path to save the downloaded file.
headers (dict): Optional headers to include in the request.
show_progress (bool): Whether to show a progress bar during download.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The retry loop in download_file may not handle KeyboardInterrupt gracefully.

Explicitly handle KeyboardInterrupt to ensure a clean exit and avoid logging it as an 'Unexpected error.'

Suggested implementation:

def download_file(url: str, dest_path: str, headers: dict[str, str] | None = None, show_progress: bool = True):
    """
    Downloads a file from a given URL to a specified destination path.

    Args:
        url (str): The URL to download from.
        dest_path (str): The path to save the downloaded file.
        headers (dict): Optional headers to include in the request.
        show_progress (bool): Whether to show a progress bar during download.

    Raises:
    KeyboardInterrupt: If the download is interrupted by the user.
    Raises:
    """
    max_retries = 3
    for attempt in range(max_retries):
        try:
            # ... (existing download logic)
            break
        except KeyboardInterrupt:
            print("\nDownload interrupted by user. Exiting cleanly.")
            return
        except Exception as e:
            if attempt < max_retries - 1:
                print(f"Error occurred: {e}. Retrying ({attempt + 1}/{max_retries})...")
                continue
            else:
                print("Unexpected error:", e)
                raise

You will need to merge the new retry loop and exception handling with your existing download logic inside download_file. Make sure to place the except KeyboardInterrupt before the generic except Exception to ensure it is handled explicitly and not logged as an 'Unexpected error.'

@@ -61,7 +54,7 @@ def confirm_no_gpu(name, provider) -> bool:
return True
if user_input in ["no", "n"]:
return False
print("Invalid input. Please enter 'yes' or 'no'.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Switching to perror for user prompts may not be ideal for all user-facing messages.

Consider keeping this prompt on stdout to avoid confusing users with non-error messages on stderr.

Comment on lines +845 to +847
# Write messages to stderr
perror(f"Downloading {model_name} ...")
perror(f"Trying to pull {model_name} ...")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Both pull messages are now sent to stderr, which may not be necessary.

Evaluate if informational messages like 'Downloading' and 'Trying to pull' should be sent to stderr, as this may impact script output parsing. Typically, only errors or warnings are sent to stderr.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @rhatdan, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request focuses on improving the clarity and correctness of output by redirecting error and progress messages to STDERR. This change ensures that important diagnostic information is separated from regular output, making it easier to debug and monitor the application. The introduction of the perror function promotes code reuse and consistency across the codebase.

Highlights

  • STDERR Usage: Ensured that all error messages and progress messages are now directed to STDERR instead of STDOUT.
  • Common Function: Introduced a common perror function to handle STDERR output, replacing direct print calls.
  • File Modifications: Modified multiple files including chat.py, cli.py, common.py, engine.py, hf_style_repo_base.py, http_client.py, huggingface.py, model.py, model_store/snapshot_file.py, oci.py, ollama.py, ollama_repo_utils.py, and rag.py to use perror for error and progress messages.
  • Code Cleanup: Removed unused imports and code blocks from common.py to improve code readability.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments to provide feedback.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request focuses on improving error handling and standardizing output streams by directing progress and error messages to stderr. It also centralizes download logic within the HttpClient class, enhancing code modularity and maintainability.

@ericcurtin ericcurtin merged commit 0947e11 into containers:main Jul 6, 2025
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants