rerun_bindings/catalog/
errors.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! Error handling for the catalog module.
//!
//! ## Guide
//!
//! - For each encountered error (e.g. `re_uri::Error` when parsing), define a new variant in the
//!   `Error` enum, and implement a mapping to a user-facing Python error in the `to_py_err`
//!   function. Then, use `?`.
//!
//! - Don't hesitate to introduce new error classes if this could help the user catch specific
//!   errors. Use the [`pyo3::create_exception`] macro for that and update [`super::register`] to
//!   expose it.
//!
//! - Error type (either built-in such as [`pyo3::exceptions::PyValueError`] or custom) can always
//!   be used directly using, e.g. `PyValueError::new_err("message")`.

use pyo3::exceptions::{PyConnectionError, PyValueError};
use pyo3::PyErr;
use std::error::Error as _;

use re_grpc_client::redap::ConnectionError;
use re_protos::manifest_registry::v1alpha1::ext::GetDatasetSchemaResponseError;
// ---

/// Private error type to server as a bridge between various external error type and the
/// [`to_py_err`] function.
#[derive(Debug, thiserror::Error)]
#[expect(clippy::enum_variant_names)] // this is by design
enum ExternalError {
    #[error("{0}")]
    ConnectionError(#[from] ConnectionError),

    #[error("{0}")]
    TonicStatusError(#[from] tonic::Status),

    #[error("{0}")]
    UriError(#[from] re_uri::Error),

    #[error("{0}")]
    ChunkError(#[from] re_chunk::ChunkError),

    #[error("{0}")]
    ChunkStoreError(#[from] re_chunk_store::ChunkStoreError),

    #[error("{0}")]
    StreamError(#[from] re_grpc_client::StreamError),

    #[error("{0}")]
    ArrowError(#[from] arrow::error::ArrowError),

    #[error("{0}")]
    UrlParseError(#[from] url::ParseError),

    #[error("{0}")]
    DatafusionError(#[from] datafusion::error::DataFusionError),

    #[error(transparent)]
    CodecError(#[from] re_log_encoding::codec::CodecError),
}

impl From<re_protos::manifest_registry::v1alpha1::ext::GetDatasetSchemaResponseError>
    for ExternalError
{
    fn from(value: GetDatasetSchemaResponseError) -> Self {
        match value {
            GetDatasetSchemaResponseError::ArrowError(err) => err.into(),
            GetDatasetSchemaResponseError::TypeConversionError(err) => {
                re_grpc_client::StreamError::from(err).into()
            }
        }
    }
}

impl From<ExternalError> for PyErr {
    fn from(err: ExternalError) -> Self {
        match err {
            ExternalError::ConnectionError(err) => PyConnectionError::new_err(err.to_string()),

            ExternalError::TonicStatusError(status) => {
                let mut msg = format!(
                    "tonic status error: {} (code: {}",
                    status.message(),
                    status.code()
                );
                if let Some(source) = status.source() {
                    msg.push_str(&format!(", source: {source})"));
                } else {
                    msg.push(')');
                }
                PyConnectionError::new_err(msg)
            }

            ExternalError::UriError(err) => PyValueError::new_err(format!("Invalid URI: {err}")),

            ExternalError::ChunkError(err) => PyValueError::new_err(format!("Chunk error: {err}")),

            ExternalError::ChunkStoreError(err) => {
                PyValueError::new_err(format!("Chunk store error: {err}"))
            }

            ExternalError::StreamError(err) => {
                PyValueError::new_err(format!("Data streaming error: {err}"))
            }

            ExternalError::ArrowError(err) => PyValueError::new_err(format!("Arrow error: {err}")),

            ExternalError::UrlParseError(err) => {
                PyValueError::new_err(format!("Could not parse URL: {err}"))
            }

            ExternalError::DatafusionError(err) => {
                PyValueError::new_err(format!("DataFusion error: {err}"))
            }

            ExternalError::CodecError(err) => PyValueError::new_err(format!("Codec error: {err}")),
        }
    }
}

/// Global mapping of all our internal error to user-facing Python errors.
///
/// Use as `.map_err(to_py_err)?`.
#[expect(private_bounds)] // this is by design
pub fn to_py_err(err: impl Into<ExternalError>) -> PyErr {
    err.into().into()
}