Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom exceptions #48

Merged
merged 2 commits into from
Oct 25, 2024
Merged
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
2 changes: 2 additions & 0 deletions docs/api/delete.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# Delete

::: obstore.delete
::: obstore.delete_async
3 changes: 3 additions & 0 deletions docs/api/exceptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Exceptions

::: obstore.exceptions
3 changes: 1 addition & 2 deletions docs/overrides/stylesheets/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ h6 {
.md-typeset h2,
.md-typeset h3,
.md-typeset h4 {
font-weight: bold;
font-weight: normal;
color: var(--md-default-fg-color);
margin: 0;
}
.md-button,
.md-typeset .md-button {
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ nav:
- api/put.md
- api/rename.md
- api/sign.md
- api/exceptions.md

watch:
- obstore/python
Expand Down
2 changes: 2 additions & 0 deletions obstore/python/obstore/_obstore.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ from ._sign import HTTP_METHOD as HTTP_METHOD
from ._sign import SignCapableStore as SignCapableStore
from ._sign import sign as sign
from ._sign import sign_async as sign_async

def ___version() -> str: ...
38 changes: 38 additions & 0 deletions obstore/python/obstore/exceptions.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class ObstoreError(Exception):
"""The base exception class"""

class GenericError(ObstoreError):
"""A fallback error type when no variant matches."""

class NotFoundError(ObstoreError):
"""Error when the object is not found at given location."""

class InvalidPathError(ObstoreError):
"""Error for invalid path."""

class JoinError(ObstoreError):
"""Error when `tokio::spawn` failed."""

class NotSupportedError(ObstoreError):
"""Error when the attempted operation is not supported."""

class AlreadyExistsError(ObstoreError):
"""Error when the object already exists."""

class PreconditionError(ObstoreError):
"""Error when the required conditions failed for the operation."""

class NotModifiedError(ObstoreError):
"""Error when the object at the location isn't modified."""

class PermissionDeniedError(ObstoreError):
"""
Error when the used credentials don't have enough permission
to perform the requested operation
"""

class UnauthenticatedError(ObstoreError):
"""Error when the used credentials lack valid authentication."""

class UnknownConfigurationKeyError(ObstoreError):
"""Error when a configuration key is invalid for the store used."""
1 change: 1 addition & 0 deletions obstore/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fn _obstore(py: Python, m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(___version))?;

pyo3_object_store::register_store_module(py, m, "obstore")?;
pyo3_object_store::register_exceptions_module(py, m, "obstore")?;

m.add_wrapped(wrap_pyfunction!(copy::copy_async))?;
m.add_wrapped(wrap_pyfunction!(copy::copy))?;
Expand Down
56 changes: 56 additions & 0 deletions pyo3-object_store/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use pyo3::intern;
use pyo3::prelude::*;

use crate::error::*;
use crate::{PyAzureStore, PyGCSStore, PyHttpStore, PyLocalStore, PyMemoryStore, PyS3Store};

/// Export the default Python API as a submodule named `store` within the given parent module
Expand Down Expand Up @@ -33,3 +34,58 @@ pub fn register_store_module(

Ok(())
}

/// Export exceptions as a submodule named `exceptions` within the given parent module
// https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021
// https://github.com/PyO3/pyo3/issues/759#issuecomment-977835119
pub fn register_exceptions_module(
py: Python<'_>,
parent_module: &Bound<'_, PyModule>,
parent_module_str: &str,
) -> PyResult<()> {
let full_module_string = format!("{}.exceptions", parent_module_str);

let child_module = PyModule::new_bound(parent_module.py(), "exceptions")?;

child_module.add("ObstoreError", py.get_type_bound::<ObstoreError>())?;
child_module.add("GenericError", py.get_type_bound::<GenericError>())?;
child_module.add("NotFoundError", py.get_type_bound::<NotFoundError>())?;
child_module.add("InvalidPathError", py.get_type_bound::<InvalidPathError>())?;
child_module.add("JoinError", py.get_type_bound::<JoinError>())?;
child_module.add(
"NotSupportedError",
py.get_type_bound::<NotSupportedError>(),
)?;
child_module.add(
"AlreadyExistsError",
py.get_type_bound::<AlreadyExistsError>(),
)?;
child_module.add(
"PreconditionError",
py.get_type_bound::<PreconditionError>(),
)?;
child_module.add("NotModifiedError", py.get_type_bound::<NotModifiedError>())?;
child_module.add(
"PermissionDeniedError",
py.get_type_bound::<PermissionDeniedError>(),
)?;
child_module.add(
"UnauthenticatedError",
py.get_type_bound::<UnauthenticatedError>(),
)?;
child_module.add(
"UnknownConfigurationKeyError",
py.get_type_bound::<UnknownConfigurationKeyError>(),
)?;

parent_module.add_submodule(&child_module)?;

py.import_bound(intern!(py, "sys"))?
.getattr(intern!(py, "modules"))?
.set_item(full_module_string.as_str(), child_module.to_object(py))?;

// needs to be set *after* `add_submodule()`
child_module.setattr("__name__", full_module_string)?;

Ok(())
}
63 changes: 58 additions & 5 deletions pyo3-object_store/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
//! Contains the [`PyObjectStoreError`], the Error returned by most fallible functions in this
//! crate.

use pyo3::exceptions::{
PyException, PyFileNotFoundError, PyIOError, PyNotImplementedError, PyValueError,
};
#![allow(missing_docs)]

use pyo3::exceptions::{PyFileNotFoundError, PyIOError, PyNotImplementedError, PyValueError};
use pyo3::prelude::*;
use pyo3::DowncastError;
use pyo3::{create_exception, DowncastError};
use thiserror::Error;

// Base exception
create_exception!(
pyo3_object_store,
ObstoreError,
pyo3::exceptions::PyException
);

// Subclasses from base exception
create_exception!(pyo3_object_store, GenericError, ObstoreError);
create_exception!(pyo3_object_store, NotFoundError, ObstoreError);
create_exception!(pyo3_object_store, InvalidPathError, ObstoreError);
create_exception!(pyo3_object_store, JoinError, ObstoreError);
create_exception!(pyo3_object_store, NotSupportedError, ObstoreError);
create_exception!(pyo3_object_store, AlreadyExistsError, ObstoreError);
create_exception!(pyo3_object_store, PreconditionError, ObstoreError);
create_exception!(pyo3_object_store, NotModifiedError, ObstoreError);
create_exception!(pyo3_object_store, PermissionDeniedError, ObstoreError);
create_exception!(pyo3_object_store, UnauthenticatedError, ObstoreError);
create_exception!(
pyo3_object_store,
UnknownConfigurationKeyError,
ObstoreError
);

/// The Error variants returned by this crate.
#[derive(Error, Debug)]
#[non_exhaustive]
Expand All @@ -30,13 +54,42 @@ impl From<PyObjectStoreError> for PyErr {
match error {
PyObjectStoreError::PyErr(err) => err,
PyObjectStoreError::ObjectStoreError(ref err) => match err {
object_store::Error::Generic {
store: _,
source: _,
} => GenericError::new_err(err.to_string()),
object_store::Error::NotFound { path: _, source: _ } => {
PyFileNotFoundError::new_err(err.to_string())
}
object_store::Error::InvalidPath { source: _ } => {
InvalidPathError::new_err(err.to_string())
}
object_store::Error::JoinError { source: _ } => JoinError::new_err(err.to_string()),
object_store::Error::NotSupported { source: _ } => {
NotSupportedError::new_err(err.to_string())
}
object_store::Error::AlreadyExists { path: _, source: _ } => {
AlreadyExistsError::new_err(err.to_string())
}
object_store::Error::Precondition { path: _, source: _ } => {
PreconditionError::new_err(err.to_string())
}
object_store::Error::NotModified { path: _, source: _ } => {
NotModifiedError::new_err(err.to_string())
}
object_store::Error::NotImplemented => {
PyNotImplementedError::new_err(err.to_string())
}
_ => PyException::new_err(err.to_string()),
object_store::Error::PermissionDenied { path: _, source: _ } => {
PermissionDeniedError::new_err(err.to_string())
}
object_store::Error::Unauthenticated { path: _, source: _ } => {
UnauthenticatedError::new_err(err.to_string())
}
object_store::Error::UnknownConfigurationKey { store: _, key: _ } => {
UnknownConfigurationKeyError::new_err(err.to_string())
}
_ => GenericError::new_err(err.to_string()),
},
PyObjectStoreError::IOError(err) => PyIOError::new_err(err.to_string()),
}
Expand Down
2 changes: 1 addition & 1 deletion pyo3-object_store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ mod memory;
mod retry;
mod store;

pub use api::register_store_module;
pub use api::{register_exceptions_module, register_store_module};
pub use aws::PyS3Store;
pub use azure::PyAzureStore;
pub use client::{PyClientConfigKey, PyClientOptions};
Expand Down
4 changes: 2 additions & 2 deletions tests/test_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def test_delete_one_local_fs():
obs.delete(store, "file3.txt")
assert len(obs.list(store).collect()) == 0

with pytest.raises(Exception, match="No such file"):
with pytest.raises(FileNotFoundError):
obs.delete(store, "file1.txt")


Expand All @@ -68,7 +68,7 @@ def test_delete_many_local_fs():
["file1.txt", "file2.txt", "file3.txt"],
)

with pytest.raises(Exception, match="No such file"):
with pytest.raises(FileNotFoundError):
obs.delete(
store,
["file1.txt", "file2.txt", "file3.txt"],
Expand Down