Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

common: general improvements #713

Merged
merged 2 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ on:
- main

jobs:
lint:
name: Lint Code
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y bash codespell python3-argcomplete pipx
make install-requirements
- name: Run lint
shell: bash
run: make lint

bats:
runs-on: ubuntu-24.04
steps:
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ setup_ramalama() {
"common.py" "__init__.py" "quadlet.py" "kube.py" \
"oci.py" "version.py" "shortnames.py" "toml_parser.py" \
"file.py" "http_client.py" "url.py" "annotations.py" \
"gpu_detector.py" )
"gpu_detector.py", "console.py" )
for i in "${python_files[@]}"; do
if $local_install; then
url="ramalama/${i}"
Expand Down
70 changes: 57 additions & 13 deletions ramalama/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@
import hashlib
import os
import random
import logging
import shutil
import string
import subprocess
import time
import sys
import urllib.request
import urllib.error
import ramalama.console as console

from ramalama.http_client import HttpClient


logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(levelname)s - %(message)s")

MNT_DIR = "/mnt/models"
MNT_FILE = f"{MNT_DIR}/model.file"
HTTP_RANGE_NOT_SATISFIABLE = 416


def container_manager():
Expand Down Expand Up @@ -165,25 +173,61 @@ def download_file(url, dest_path, headers=None, show_progress=True):
headers (dict): Optional headers to include in the request.
show_progress (bool): Whether to show a progress bar during download.

Returns:
None
Raises:
RuntimeError: If the download fails after multiple attempts.
"""
http_client = HttpClient()

headers = headers or {}

# if we are not a tty, don't show progress, can pollute CI output and such
# If not running in a TTY, disable progress to prevent CI pollution
if not sys.stdout.isatty():
show_progress = False

try:
http_client.init(url=url, headers=headers, output_file=dest_path, progress=show_progress)
except urllib.error.HTTPError as e:
if e.code == 416: # Range not satisfiable
if show_progress:
print(f"File {url} already fully downloaded.")
else:
raise e
http_client = HttpClient()
max_retries = 5 # Stop after 5 failures
retries = 0

while retries < max_retries:
try:
# Initialize HTTP client for the request
http_client.init(url=url, headers=headers, output_file=dest_path, progress=show_progress)
return # Exit function if successful

except urllib.error.HTTPError as e:
if e.code == HTTP_RANGE_NOT_SATISFIABLE: # "Range Not Satisfiable" error (file already downloaded)
return # No need to retry

except urllib.error.URLError as e:
console.error(f"Network Error: {e.reason}")
retries += 1

except TimeoutError:
retries += 1
console.warning(f"TimeoutError: The server took too long to respond. Retrying {retries}/{max_retries}...")

except RuntimeError as e: # Catch network-related errors from HttpClient
retries += 1
console.warning(f"{e}. Retrying {retries}/{max_retries}...")

except IOError as e:
retries += 1
console.warning(f"I/O Error: {e}. Retrying {retries}/{max_retries}...")

except Exception as e:
console.error(f"Unexpected error: {str(e)}")
raise

if retries >= max_retries:
error_message = (
"\nDownload failed after multiple attempts.\n"
"Possible causes:\n"
"- Internet connection issue\n"
"- Server is down or unresponsive\n"
"- Firewall or proxy blocking the request\n"
)
console.error(error_message)
sys.exit(1)

time.sleep(2**retries * 0.1) # Exponential backoff (0.1s, 0.2s, 0.4s...)


def engine_version(engine):
Expand Down
39 changes: 39 additions & 0 deletions ramalama/console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
import logging


def is_locale_utf8():
Copy link
Collaborator

@ericcurtin ericcurtin Feb 3, 2025

Choose a reason for hiding this comment

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

I tested this on macOS with alacritty it needs one more check to work, because this is the only env var (LC_ALL) of the 3 env vars populated on this platform:

 $ echo $LC_ALL
 en_IE.UTF-8

So we need to add one more or:

return 'UTF-8' in os.getenv('LC_CTYPE', '') or 'UTF-8' in os.getenv('LANG', '') or 'UTF-8' in os.getenv('LC_ALL', '')

Copy link
Collaborator

@ericcurtin ericcurtin Feb 3, 2025

Choose a reason for hiding this comment

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

Could also do:

return any('UTF-8' in os.getenv(var, '') for var in ['LC_CTYPE', 'LANG', 'LC_ALL'])

to be less long-winded, as long as it works I don't mind too much. And it just checks these three env vars for UTF-8, I don't care after that point.

"""Check if the system locale is UTF-8."""
return 'UTF-8' in os.getenv('LC_CTYPE', '') or 'UTF-8' in os.getenv('LANG', '')


def supports_emoji():
"""Detect if the terminal supports emoji output."""
term = os.getenv("TERM")
if not term or term in ("dumb", "linux"):
return False

return is_locale_utf8()


# Allow users to override emoji support via an environment variable
# If RAMALAMA_FORCE_EMOJI is not set, it defaults to checking supports_emoji()
RAMALAMA_FORCE_EMOJI = os.getenv("RAMALAMA_FORCE_EMOJI")
FORCE_EMOJI = RAMALAMA_FORCE_EMOJI.lower() == "true" if RAMALAMA_FORCE_EMOJI else None
EMOJI = FORCE_EMOJI if FORCE_EMOJI is not None else supports_emoji()
Copy link
Collaborator

@ericcurtin ericcurtin Feb 3, 2025

Choose a reason for hiding this comment

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

Lines 21-23 are long-winded it took me a while to read this and try and understand if it was correct, lets simplify:

EMOJI = os.getenv("RAMALAMA_FORCE_EMOJI") or supports_emoji()



# Define emoji-aware logging messages
def error(msg):
Copy link
Collaborator

@ericcurtin ericcurtin Feb 3, 2025

Choose a reason for hiding this comment

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

The spacing isn't consistent in the printed output in these three functions, some are double-space some are single-space.

If you are pushing new commits you might as well catch it here, we can also do in a follow on PR.

formatted_msg = f"❌ {msg}" if EMOJI else f"[ERROR] {msg}"
logging.error(formatted_msg)


def warning(msg):
formatted_msg = f"⚠️ {msg}" if EMOJI else f"[WARNING] {msg}"
logging.warning(formatted_msg)


def info(msg):
formatted_msg = f"ℹ️ {msg}" if EMOJI else f"[INFO] {msg}"
logging.info(formatted_msg)
Loading