diff --git a/vlmrun/cli/cli.py b/vlmrun/cli/cli.py index 002bda7..b103bd3 100644 --- a/vlmrun/cli/cli.py +++ b/vlmrun/cli/cli.py @@ -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: """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( @@ -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" @@ -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, @@ -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 diff --git a/vlmrun/client/base_requestor.py b/vlmrun/client/base_requestor.py index 9158cbb..4b33ee7 100644 --- a/vlmrun/client/base_requestor.py +++ b/vlmrun/client/base_requestor.py @@ -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 @@ -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 @@ -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( diff --git a/vlmrun/client/client.py b/vlmrun/client/client.py index 25c80e5..7c48c44 100644 --- a/vlmrun/client/client.py +++ b/vlmrun/client/client.py @@ -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 @@ -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( diff --git a/vlmrun/version.py b/vlmrun/version.py index 43a1e95..6b27eee 100644 --- a/vlmrun/version.py +++ b/vlmrun/version.py @@ -1 +1 @@ -__version__ = "0.5.3" +__version__ = "0.5.4"