Source code for tlc_plugin_sdk.shared.url_utils

# Copyright 2026 3LC Inc.
# SPDX-License-Identifier: Apache-2.0
"""URL normalization utilities for 3LC object URLs.

Ensures file-path URLs are absolute before passing to the tlc SDK,
preventing the CWD from being prepended to relative-looking paths.
"""

from __future__ import annotations

import logging
import os
from typing import Any

logger = logging.getLogger(__name__)


[docs] def normalize_url(url: str) -> str: """Normalize a 3LC URL for use with the tlc SDK. - If the URL is a protocol URL (e.g. api://, s3://, gs://), return as-is. - If the URL looks like a file path, expand ``~`` and ensure it's absolute. - Handles URL-decoded paths that may have lost their leading slash. """ if not url: return url # Protocol URLs — pass through if "://" in url: scheme = url.split("://", 1)[0].lower() # File paths on macOS/Linux look like /Users/... not a protocol if scheme in ("api", "s3", "gs", "http", "https", "3lc"): return url # File path — expand tilde, then ensure absolute url = os.path.expanduser(url) if not os.path.isabs(url): # Common case: path like "Users/paul/..." that lost its leading / if url.startswith(("Users/", "home/")): return "/" + url # Project-relative URL (e.g. "tinycoco/runs/demo1") — resolve # against the 3LC project root directory. try: import tlc project_root = str(tlc.config.project_root_url).rstrip("/") candidate = os.path.join(project_root, url) if os.path.exists(candidate): return candidate except Exception: logger.debug("Could not resolve relative URL against project root: %s", url) # Fallback: return as-is and let the SDK try return url return url
[docs] def normalize_local_path(path: str) -> str: """Normalize a user-typed local filesystem path. Strips whitespace and expands ``~``/``~user``. Plugins run with the plugin venv as CWD, so a bare-relative path would silently resolve somewhere no user ever looks — reject it instead. Args: path: Raw path string as typed by the user. Returns: The expanded, absolute path. Raises: ValueError: If the path is empty or not absolute after expansion. """ expanded = os.path.expanduser(path.strip()) if not expanded: msg = "Path is empty." raise ValueError(msg) if not os.path.isabs(expanded): msg = ( f"Path must be absolute (got {expanded!r}). " "Relative paths would resolve against the plugin's working directory, not yours." ) raise ValueError(msg) return expanded
[docs] def get_url_column_names(table: Any) -> list[str]: """Return the names of a table's URL/path-valued columns. Reads the table's ``_url_columns`` attribute, which is a private 3lc attribute that is not part of the typed public API and may be absent depending on the 3lc version, so reach it defensively. It can be ``[['image']]`` (nested) or ``['image']`` (flat). Args: table: A ``tlc.Table``. Returns: Flat list of column names; empty if none could be determined. """ names: list[str] = [] try: for entry in list(getattr(table, "_url_columns", [])): if isinstance(entry, list): names.extend(str(col) for col in entry) else: names.append(str(entry)) except Exception: logger.debug("Could not extract URL column names from table", exc_info=True) return names