Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
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
29 changes: 16 additions & 13 deletions packages/jumpstarter/jumpstarter/client/lease.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ async def _wait_for_ready_connection(self, path: str):
else:
logger.warning("Waiting for ready connection to %s: %s", path, e)
await sleep(5)
except ConnectionError:
raise
except Exception as e:
logger.error("Unexpected error while waiting for ready connection to %s: %s", path, e)
raise ConnectionError("Unexpected error while waiting for ready connection to %s" % path) from e
Expand All @@ -252,7 +254,7 @@ async def _monitor():
while True:
lease = await self.get()
if lease.effective_begin_time and lease.effective_duration:
if lease.effective_end_time: # already ended
if lease.effective_end_time: # already ended
end_time = lease.effective_end_time
else:
end_time = lease.effective_begin_time + lease.duration
Expand All @@ -263,8 +265,11 @@ async def _monitor():
break
# Log once when entering the threshold window
if threshold - timedelta(seconds=check_interval) <= remain < threshold:
logger.info("Lease {} ending in {} minutes at {}".format(
self.name, int((remain.total_seconds() + 30) // 60), end_time))
logger.info(
"Lease {} ending in {} minutes at {}".format(
self.name, int((remain.total_seconds() + 30) // 60), end_time
)
)
await sleep(min(remain.total_seconds(), check_interval))
else:
await sleep(1)
Expand Down Expand Up @@ -317,19 +322,17 @@ def _is_non_interactive(self) -> bool:
def _is_terminal_available(self) -> bool:
"""Check if we're running in a terminal/TTY."""
return (
hasattr(sys.stdout, 'isatty') and
sys.stdout.isatty() and
hasattr(sys.stderr, 'isatty') and
sys.stderr.isatty()
hasattr(sys.stdout, "isatty")
and sys.stdout.isatty()
and hasattr(sys.stderr, "isatty")
and sys.stderr.isatty()
)

def __enter__(self):
self.start_time = datetime.now()
if self._should_show_spinner:
self.spinner = self.console.status(
f"Acquiring lease {self.lease_name or '...'}...",
spinner="dots",
spinner_style="blue"
f"Acquiring lease {self.lease_name or '...'}...", spinner="dots", spinner_style="blue"
)
self.spinner.start()
return self
Expand All @@ -343,18 +346,18 @@ def update_status(self, message: str):
if self.spinner and self._should_show_spinner:
self._current_message = f"[blue]{message}[/blue]"
elapsed = datetime.now() - self.start_time
elapsed_str = str(elapsed).split('.')[0] # Remove microseconds
elapsed_str = str(elapsed).split(".")[0] # Remove microseconds
self.spinner.update(f"{self._current_message} [dim]({elapsed_str})[/dim]")
else:
# Log info message when no console is available
elapsed = datetime.now() - self.start_time
elapsed_str = str(elapsed).split('.')[0] # Remove microseconds
elapsed_str = str(elapsed).split(".")[0] # Remove microseconds
logger.info(f"{message} ({elapsed_str})")

def tick(self):
"""Update the spinner with current elapsed time without changing the message."""
if self.spinner and self._should_show_spinner and self._current_message:
elapsed = datetime.now() - self.start_time
elapsed_str = str(elapsed).split('.')[0] # Remove microseconds
elapsed_str = str(elapsed).split(".")[0] # Remove microseconds
# Use the stored current message and update with new elapsed time
self.spinner.update(f"{self._current_message} [dim]({elapsed_str})[/dim]")
35 changes: 35 additions & 0 deletions packages/jumpstarter/jumpstarter/common/importlib.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Reference: https://docs.djangoproject.com/en/5.0/_modules/django/utils/module_loading/#import_string

import logging
import sys
from fnmatch import fnmatchcase
from importlib import import_module

logger = logging.getLogger(__name__)


def cached_import(module_path, class_name):
# Check whether module is loaded and fully initialized.
Expand Down Expand Up @@ -36,5 +39,37 @@ def import_class(class_path: str, allow: list[str], unsafe: bool):
raise ImportError(f"{class_path} doesn't look like a class path") from e
try:
return cached_import(module_path, class_name)
except ModuleNotFoundError as e:
module_name = str(e).split("'")[1] if "'" in str(e) else str(e).split()[-1]
Comment on lines +42 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use ModuleNotFoundError's .name attribute instead of string parsing.

The current string parsing approach is fragile and can fail if the error message format changes. ModuleNotFoundError provides a .name attribute that directly contains the module name.

Apply this diff to use the more reliable approach:

     except ModuleNotFoundError as e:
-        module_name = str(e).split("'")[1] if "'" in str(e) else str(e).split()[-1]
+        module_name = e.name if e.name else "unknown"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except ModuleNotFoundError as e:
module_name = str(e).split("'")[1] if "'" in str(e) else str(e).split()[-1]
except ModuleNotFoundError as e:
module_name = e.name if e.name else "unknown"
🤖 Prompt for AI Agents
In packages/jumpstarter/jumpstarter/common/importlib.py around lines 42-43,
replace the fragile string-parsing that extracts the missing module name from
ModuleNotFoundError with the built-in attribute: use the exception's .name
property (e.name) to obtain the module name and assign that to module_name; keep
behavior the same if .name is None by falling back to str(e) only if necessary.


is_jumpstarter_driver = unsafe or any(fnmatchcase(class_path, pattern) for pattern in allow)

if is_jumpstarter_driver:
logger.error(
"Missing Jumpstarter driver module '%s' for class '%s'. "
"This usually indicates a version mismatch between your client and the exporter.",
module_name,
class_path,
)
raise ConnectionError(
f"Missing Jumpstarter driver module '{module_name}'.\n\n"
"This usually indicates a version mismatch between your client and the exporter.\n"
"Please try to update your client to the latest version and ensure the exporter "
"has the correct version installed.\n"
) from e
else:
logger.error(
"Missing Python module '%s' while importing '%s'. "
"This module needs to be installed in your environment.",
module_name,
class_path,
)
raise ConnectionError(
f"Missing Python module '{module_name}'.\n\n"
"Please install the missing module:\n"
f" pip install {module_name}\n\n"
"or if using uv:\n"
f" uv pip install {module_name}"
) from e
except AttributeError as e:
raise ImportError(f"{module_path} doesn't have specified class {class_name}") from e
Loading