Source code for embedl_hub._internal.tracking.errors

from __future__ import annotations

from pathlib import Path

from embedl_hub._internal.tracking.rest_api import ApiError, ErrorCode


class DomainError(Exception):
    """Base class for domain-specific exceptions."""


[docs] class TrackingFailureWarning(UserWarning): """Warns that a tracking operation failed and was skipped. Emitted instead of raising when the failing call is covered by a safe scope — a run started with ``safe=True``, or a logging call made inside :meth:`~embedl_hub.tracking.Client.safe_log`. The script continues, and whether the affected data reached the Hub is unknown: a request that failed while the response was being read may already have been committed. Treat it as uncertain rather than lost, since retrying on the assumption it was lost can duplicate it. This is its own category so it can be filtered independently of other warnings. Escalating it to an error is not applied to a failed run completion while an exception is already leaving the ``with`` body: that exception is the one the caller needs, so it is never replaced:: # Silence them warnings.filterwarnings("ignore", category=TrackingFailureWarning) # Or turn them back into errors warnings.filterwarnings("error", category=TrackingFailureWarning) """
[docs] class TrackingProjectChangedWarning(UserWarning): """Warns that selecting a run also changed the active project. A run belongs to exactly one project, so adopting one from another project has to move the client with it. That is not a failure, and it is not what the caller literally asked for either, so it is said out loud rather than done quietly — a later ``log_metric`` would otherwise land somewhere the caller did not choose. Its own category, so it can be silenced by a script that switches deliberately without also silencing :class:`TrackingFailureWarning`:: warnings.filterwarnings( "ignore", category=TrackingProjectChangedWarning ) """
[docs] class TrackingUsageError(RuntimeError): """Raised when a tracking call itself is malformed. Safe mode (``safe=True``, :meth:`~embedl_hub.tracking.Client.safe_log`) tolerates an unreliable Hub, not a malformed call, so this is raised even inside a safe scope: a warning the caller cannot act on would just hide the mistake. It is marked with its own class rather than inferred from the exception type, because the type does not carry the distinction — a missing project or absent active run raises :class:`RuntimeError` too, and those *are* degradable, being the downstream consequence of a failure the caller already chose to tolerate. Subclasses :class:`RuntimeError` so that existing handlers and the ``:raises RuntimeError:`` contracts keep working. """
class StorageQuotaExceededError(DomainError): """Raised when storage quota is exceeded.""" def __init__(self, file_path: Path, message: str | None = None) -> None: self.file_path = file_path if message is None: message = "Storage quota exceeded. Please delete some artifacts or contact support." super().__init__(message) class FileTooLargeError(DomainError): """Raised when the file for an artifact is too large to upload.""" def __init__(self, file_path: Path, message: str | None = None) -> None: self.file_path = file_path if message is None: message = "File too large to upload. Please try a smaller file." super().__init__(message) class ArtifactUploadError(DomainError): """Raised when the file upload for an artifact fails.""" def __init__(self, file_path: Path, message: str | None = None) -> None: self.file_path = file_path if message is None: message = "File upload failed. Please try again." super().__init__(message) class UnsupportedDeviceError(DomainError): """Raised when an unsupported device is specified.""" def __init__(self, device: str, message: str | None = None) -> None: self.device = device if message is None: message = f"The device '{device}' is not supported." super().__init__(message) class JobQuotaExceededError(DomainError): """Raised when the job quota is exceeded.""" def __init__(self, message: str | None = None) -> None: if message is None: message = "Job quota exceeded. Please try again later or contact support." super().__init__(message) class InvalidRunLineageError(DomainError): """Raised when the API rejects an invalid parent run link.""" def __init__(self, message: str | None = None) -> None: if message is None: message = "Invalid run lineage." super().__init__(message) def raise_if_artifact_error(api_error: ApiError, *, file_path: Path): """Re-raise a domain-specific exception if the API error is related to artifacts.""" for error in api_error.errors: if error.code == ErrorCode.FILE_TOO_LARGE.value: raise FileTooLargeError( file_path, message=error.detail ) from api_error if error.code == ErrorCode.STORAGE_QUOTA_EXCEEDED.value: raise StorageQuotaExceededError( file_path, message=error.detail ) from api_error def raise_if_job_error(api_error: ApiError): """Re-raise a domain-specific exception if the API error is related to jobs.""" for error in api_error.errors: if error.code == ErrorCode.JOB_QUOTA_EXCEEDED.value: raise JobQuotaExceededError(message=error.detail) from api_error def raise_if_run_error(api_error: ApiError): """Re-raise a domain-specific exception if the API error is related to runs.""" for error in api_error.errors: if error.code == ErrorCode.INVALID_RUN_LINEAGE.value: raise InvalidRunLineageError( message=error.detail or error.title ) from api_error text = " ".join( part for part in [error.title, error.detail] if part ).lower() if ( "parent run" in text or "lineage" in text or "compile runs cannot have a parent run" in text ): raise InvalidRunLineageError( message=error.detail or error.title ) from api_error