Skip to content
Open
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
43 changes: 29 additions & 14 deletions vlmrun/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,28 @@ def version_callback(value: bool) -> None:
raise typer.Exit()


def check_credentials(
ctx: typer.Context, api_key: Optional[str], base_url: str
) -> None:
def resolve_base_url(base_url: Optional[str]) -> str:
"""Resolve base_url with priority: arg > env > config > default."""
if base_url:
return base_url
if env_url := os.getenv("VLMRUN_BASE_URL"):
return env_url
if config_url := get_config().base_url:
return config_url
return DEFAULT_BASE_URL


def resolve_api_key(api_key: Optional[str]) -> Optional[str]:
"""Resolve api_key with priority: arg > env > config."""
if api_key:
return api_key
if env_key := os.getenv("VLMRUN_API_KEY"):
return env_key
return get_config().api_key


def check_credentials(ctx: typer.Context, api_key: str, base_url: str) -> None:

Choose a reason for hiding this comment

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

medium

The type hint for api_key is str, but it can receive None. The resolve_api_key function returns Optional[str], and its result is passed here. The logic correctly handles the None case, but the type hint should be updated to Optional[str] to match the actual type and avoid potential issues with static analysis tools.

Suggested change
def check_credentials(ctx: typer.Context, api_key: str, base_url: str) -> None:
def check_credentials(ctx: typer.Context, api_key: Optional[str], base_url: str) -> None:

"""Check if API key is present and show helpful message if missing."""
config = get_config()
api_key = api_key or config.api_key
base_url = base_url or config.base_url

if not api_key:
console.print("\n[red bold]Error:[/] API key not found! 🔑\n")
console.print(
Expand All @@ -69,7 +83,7 @@ def check_credentials(
)
raise typer.Exit(1)

is_default_url = base_url == DEFAULT_BASE_URL and not os.getenv("VLMRUN_BASE_URL")
is_default_url = base_url == DEFAULT_BASE_URL
if is_default_url and not ctx.meta.get("has_shown_base_url_notice"):
console.print(
f"[yellow]Note:[/] Using default API endpoint: [blue]{base_url}[/]\n"
Expand All @@ -89,14 +103,12 @@ def main(
api_key: Optional[str] = typer.Option(
None,
"--api-key",
envvar="VLMRUN_API_KEY",
help="VLM Run API key. Can also be set via VLMRUN_API_KEY environment variable.",
help="VLM Run API key. Can also be set via VLMRUN_API_KEY env var or config.",
),
base_url: Optional[str] = typer.Option(
None,
"--base-url",
envvar="VLMRUN_BASE_URL",
help="VLM Run API base URL. Can also be set via VLMRUN_BASE_URL environment variable.",
help="VLM Run API base URL. Can also be set via VLMRUN_BASE_URL env var or config.",
),
version: Optional[bool] = typer.Option(
None,
Expand All @@ -119,8 +131,11 @@ def main(

# Skip credential check for config commands (needed to set API key)
if ctx.invoked_subcommand is not None and ctx.invoked_subcommand != "config":
check_credentials(ctx, api_key, base_url)
ctx.obj = VLMRun(api_key=api_key, base_url=base_url)
# Resolve with priority: arg > env > config > default
resolved_api_key = resolve_api_key(api_key)
resolved_base_url = resolve_base_url(base_url)
check_credentials(ctx, resolved_api_key, resolved_base_url)
ctx.obj = VLMRun(api_key=resolved_api_key, base_url=resolved_base_url)


# Add subcommands
Expand Down
10 changes: 5 additions & 5 deletions vlmrun/client/base_requestor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""VLM Run API requestor implementation."""

from typing import Any, Dict, Tuple, TYPE_CHECKING, Union, Optional
from urllib.parse import urljoin

if TYPE_CHECKING:
from vlmrun.types.abstract import VLMRunProtocol
Expand Down Expand Up @@ -49,12 +48,13 @@ def __init__(

Args:
client: VLMRun API instance
base_url: Base URL for API
base_url: Base URL for API (without trailing slash)
timeout: Request timeout in seconds
max_retries: Maximum number of retry attempts
"""
self._client = client
self._base_url = base_url or client.base_url
# Normalize: strip trailing slashes for consistent URL building
self._base_url = (base_url or client.base_url).rstrip("/")
self._timeout = timeout
self._max_retries = (
max_retries
Expand Down Expand Up @@ -131,8 +131,8 @@ def _request_with_retry():
if "X-Client-Id" not in _headers:
_headers["X-Client-Id"] = f"python-sdk-{__version__}"

# Build full URL
full_url = urljoin(self._base_url.rstrip("/") + "/", url.lstrip("/"))
# Build full URL (base_url is pre-normalized without trailing slash)
full_url = f"{self._base_url}/{url.lstrip('/')}"

try:
response = self._session.request(
Expand Down
62 changes: 34 additions & 28 deletions vlmrun/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,42 @@
)


def _resolve_base_url(base_url: Optional[str]) -> str:
"""Resolve base_url with priority: arg > env > default.

Also normalizes the URL by removing trailing slashes.
"""
url = base_url or os.getenv("VLMRUN_BASE_URL") or DEFAULT_BASE_URL
return url.rstrip("/")


def _resolve_api_key(api_key: Optional[str]) -> str:
"""Resolve api_key with priority: arg > env.

Raises ConfigurationError if no API key is found.
"""
resolved = api_key or os.getenv("VLMRUN_API_KEY")
if not resolved:
raise ConfigurationError(
message="Missing API key",
error_type="missing_api_key",
suggestion="Please provide your VLM Run API key:\n\n"
"1. Set it in your code:\n"
" client = VLMRun(api_key='your-api-key')\n\n"
"2. Or set the environment variable:\n"
" export VLMRUN_API_KEY='your-api-key'\n\n"
"Get your API key at https://app.vlm.run/dashboard",
)
return resolved


@dataclass
class VLMRun:
"""VLM Run API client.

Attributes:
api_key: API key for authentication. Can be provided through constructor
or VLMRUN_API_KEY environment variable.
base_url: Base URL for API. Defaults to None, which falls back to
VLMRUN_BASE_URL environment variable or https://api.vlm.run/v1.
api_key: API key for authentication. Priority: arg > VLMRUN_API_KEY env var.
base_url: Base URL for API. Priority: arg > VLMRUN_BASE_URL env var > default.
timeout: Request timeout in seconds. Defaults to 120.0.
max_retries: Maximum number of retry attempts for failed requests. Defaults to 5.
files: Files resource for managing files
Expand All @@ -54,30 +81,9 @@ class VLMRun:
max_retries: int = 5

def __post_init__(self):
"""Initialize the client after dataclass initialization.

This method handles environment variable fallbacks:
- api_key: Falls back to VLMRUN_API_KEY environment variable
- base_url: Can be overridden by constructor or VLMRUN_BASE_URL environment variable
"""
# Handle API key first
if not self.api_key: # Handle both None and empty string
self.api_key = os.getenv("VLMRUN_API_KEY", None)
if not self.api_key: # Still None or empty after env check
raise ConfigurationError(
message="Missing API key",
error_type="missing_api_key",
suggestion="Please provide your VLM Run API key:\n\n"
"1. Set it in your code:\n"
" client = VLMRun(api_key='your-api-key')\n\n"
"2. Or set the environment variable:\n"
" export VLMRUN_API_KEY='your-api-key'\n\n"
"Get your API key at https://app.vlm.run/dashboard",
)

# Handle base URL
if self.base_url is None:
self.base_url = os.getenv("VLMRUN_BASE_URL", DEFAULT_BASE_URL)
"""Initialize the client after dataclass initialization."""
self.api_key = _resolve_api_key(self.api_key)
self.base_url = _resolve_base_url(self.base_url)

# Initialize requestor for API key validation
requestor = APIRequestor(
Expand Down
2 changes: 1 addition & 1 deletion vlmrun/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.5.3"
__version__ = "0.5.4"
Loading