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
41 changes: 39 additions & 2 deletions sdks/python/boxlite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,27 @@
# Import Python convenience wrappers (re-exported via __all__)
try:
from .codebox import CodeBox # noqa: F401
from .errors import BoxliteError, ExecError, ParseError, TimeoutError # noqa: F401
from .errors import ( # noqa: F401
AlreadyExistsError,
BoxliteError,
ConfigError,
DatabaseError,
EngineError,
ExecError,
ExecutionError,
ImageError,
InternalError,
InvalidArgumentError,
InvalidStateError,
NetworkError,
NotFoundError,
ParseError,
PortalError,
RpcError,
StoppedError,
StorageError,
TimeoutError,
)
from .exec import ExecResult # noqa: F401
from .simplebox import SimpleBox # noqa: F401
Comment on lines 67 to 92
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

This single try/except couples exporting pure-Python error classes to importing CodeBox/ExecResult/SimpleBox. If CodeBox (or another convenience wrapper) fails to import (e.g., missing native extension), the error types won’t be re-exported from boxlite, even though boxlite.errors itself is available. Consider importing/re-exporting errors in a separate try block (or before CodeBox) so typed exceptions remain available independently.

Copilot uses AI. Check for mistakes.

Expand All @@ -77,8 +97,25 @@
"SimpleBox",
"CodeBox",
"ExecResult",
# Error types
# Error types (base)
"BoxliteError",
# Error types (mapped from Rust)
"EngineError",
"ConfigError",
"StorageError",
"ImageError",
"PortalError",
"NetworkError",
"RpcError",
"InternalError",
"ExecutionError",
"NotFoundError",
"AlreadyExistsError",
"InvalidStateError",
"DatabaseError",
"InvalidArgumentError",
"StoppedError",
# Error types (Python convenience)
"ExecError",
"TimeoutError",
"ParseError",
Expand Down
121 changes: 119 additions & 2 deletions sdks/python/boxlite/errors.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
"""
BoxLite error types.

Provides a hierarchy of exceptions for different failure modes.
Provides a hierarchy of exceptions matching the Rust BoxliteError variants.
"""

__all__ = ["BoxliteError", "ExecError", "TimeoutError", "ParseError"]
__all__ = [
"BoxliteError",
"EngineError",
"ConfigError",
"StorageError",
"ImageError",
"PortalError",
"NetworkError",
"RpcError",
"InternalError",
"ExecutionError",
"NotFoundError",
"AlreadyExistsError",
"InvalidStateError",
"DatabaseError",
"InvalidArgumentError",
"StoppedError",
# Convenience aliases
"ExecError",
"TimeoutError",
"ParseError",
]


class BoxliteError(Exception):
Expand All @@ -13,6 +34,102 @@ class BoxliteError(Exception):
pass


# ── Mapped from Rust BoxliteError variants ───────────────────────────────


class EngineError(BoxliteError):
"""Raised when the VM engine reports an error."""

pass


class ConfigError(BoxliteError):
"""Raised for configuration errors (invalid options, incompatible settings)."""

pass


class StorageError(BoxliteError):
"""Raised when a filesystem or storage operation fails."""

pass


class ImageError(BoxliteError):
"""Raised when image pull, resolution, or extraction fails."""

pass


class PortalError(BoxliteError):
"""Raised when host-guest communication (gRPC portal) fails."""

pass


class NetworkError(BoxliteError):
"""Raised when a networking operation fails."""

pass


class RpcError(BoxliteError):
"""Raised when a gRPC or transport-level error occurs."""

pass


class InternalError(BoxliteError):
"""Raised for unexpected internal errors."""

pass


class ExecutionError(BoxliteError):
"""Raised when command execution fails at the runtime level."""

pass


class NotFoundError(BoxliteError):
"""Raised when a box or resource is not found."""

pass


class AlreadyExistsError(BoxliteError):
"""Raised when a box or resource already exists."""

pass


class InvalidStateError(BoxliteError):
"""Raised when a box is in the wrong state for the requested operation."""

pass


class DatabaseError(BoxliteError):
"""Raised when a database operation fails."""

pass


class InvalidArgumentError(BoxliteError):
"""Raised when an invalid argument is provided."""

pass


class StoppedError(BoxliteError):
"""Raised when operating on a stopped box or shutdown runtime."""

pass


# ── Convenience exceptions (Python-side only) ────────────────────────────


class ExecError(BoxliteError):
"""
Raised when a command execution fails (non-zero exit code).
Expand Down
25 changes: 14 additions & 11 deletions sdks/python/src/box_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::info::PyBoxInfo;
use crate::metrics::PyBoxMetrics;
use crate::snapshot_options::{PyCloneOptions, PyExportOptions};
use crate::snapshots::PySnapshotHandle;
use crate::util::map_err;
use crate::util::map_boxlite_err;
use boxlite::{BoxCommand, CloneOptions, ExportOptions, LiteBox};
use pyo3::prelude::*;

Expand Down Expand Up @@ -78,7 +78,7 @@ impl PyBox {
cmd = cmd.working_dir(cwd);
}

let execution = handle.exec(cmd).await.map_err(map_err)?;
let execution = handle.exec(cmd).await.map_err(map_boxlite_err)?;

Ok(PyExecution {
execution: Arc::new(execution),
Expand All @@ -91,7 +91,7 @@ impl PyBox {
let handle = Arc::clone(&self.handle);

pyo3_async_runtimes::tokio::future_into_py(py, async move {
handle.start().await.map_err(map_err)?;
handle.start().await.map_err(map_boxlite_err)?;
Ok(())
})
}
Expand All @@ -101,7 +101,7 @@ impl PyBox {
let handle = Arc::clone(&self.handle);

pyo3_async_runtimes::tokio::future_into_py(py, async move {
handle.stop().await.map_err(map_err)?;
handle.stop().await.map_err(map_boxlite_err)?;
Ok(())
})
}
Expand All @@ -110,7 +110,7 @@ impl PyBox {
let handle = Arc::clone(&self.handle);

pyo3_async_runtimes::tokio::future_into_py(py, async move {
let metrics = handle.metrics().await.map_err(map_err)?;
let metrics = handle.metrics().await.map_err(map_boxlite_err)?;
Ok(PyBoxMetrics::from(metrics))
})
}
Expand All @@ -129,7 +129,7 @@ impl PyBox {
let archive = handle
.export(options, std::path::Path::new(&dest))
.await
.map_err(map_err)?;
.map_err(map_boxlite_err)?;
Ok(archive.path().to_string_lossy().to_string())
})
}
Expand All @@ -145,7 +145,10 @@ impl PyBox {
let handle = Arc::clone(&self.handle);
let options: CloneOptions = options.map(Into::into).unwrap_or_default();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let cloned = handle.clone_box(options, name).await.map_err(map_err)?;
let cloned = handle
.clone_box(options, name)
.await
.map_err(map_boxlite_err)?;
Ok(PyBox {
handle: Arc::new(cloned),
})
Expand All @@ -169,7 +172,7 @@ impl PyBox {
handle
.copy_into(std::path::Path::new(&host_path), &container_dest, opts)
.await
.map_err(map_err)?;
.map_err(map_boxlite_err)?;
Ok(())
})
}
Expand All @@ -191,7 +194,7 @@ impl PyBox {
handle
.copy_out(&container_src, std::path::Path::new(&host_dest), opts)
.await
.map_err(map_err)?;
.map_err(map_boxlite_err)?;
Ok(())
})
}
Expand All @@ -202,7 +205,7 @@ impl PyBox {

pyo3_async_runtimes::tokio::future_into_py(py, async move {
// Auto-start on context entry
handle.start().await.map_err(map_err)?;
handle.start().await.map_err(map_boxlite_err)?;
Ok(PyBox { handle })
})
}
Expand All @@ -218,7 +221,7 @@ impl PyBox {
let handle = Arc::clone(&slf.handle);

pyo3_async_runtimes::tokio::future_into_py(py, async move {
handle.stop().await.map_err(map_err)?;
handle.stop().await.map_err(map_boxlite_err)?;
Ok(())
})
}
Expand Down
Loading