Skip to main content

fastexcel/
lib.rs

1mod data;
2mod error;
3mod types;
4mod utils;
5
6use std::fmt::Display;
7
8#[cfg(feature = "python")]
9use error::py_errors;
10#[cfg(feature = "python")]
11use pyo3::prelude::*;
12#[cfg(feature = "python")]
13use types::excelsheet::{CellError, CellErrors};
14
15pub use data::{FastExcelColumn, FastExcelSeries};
16use error::ErrorContext;
17pub use error::{FastExcelError, FastExcelErrorKind, FastExcelResult};
18pub use types::{
19    ColumnInfo, ColumnNameFrom, DType, DTypeCoercion, DTypeFrom, DTypes, DefinedName, ExcelReader,
20    ExcelSheet, ExcelTable, IdxOrName, LoadSheetOrTableOptions, SelectedColumns, SheetVisible,
21    SkipRows,
22};
23
24/// Reads an excel file and returns an object allowing to access its sheets, tables, and a bit of metadata.
25/// This is a wrapper around `ExcelReader::try_from_path`.
26pub fn read_excel<S: AsRef<str> + Display>(path: S) -> FastExcelResult<ExcelReader> {
27    ExcelReader::try_from_path(path.as_ref())
28        .with_context(|| format!("could not load excel file at {path}"))
29}
30
31#[cfg(feature = "python")]
32/// Reads an excel file and returns an object allowing to access its sheets, tables, and a bit of metadata
33#[pyfunction(name = "read_excel")]
34fn py_read_excel<'py>(source: &Bound<'_, PyAny>, py: Python<'py>) -> PyResult<ExcelReader> {
35    use py_errors::IntoPyResult;
36
37    if let Ok(path) = source.extract::<String>() {
38        py.detach(|| ExcelReader::try_from_path(&path))
39            .with_context(|| format!("could not load excel file at {path}"))
40            .into_pyresult()
41    } else if let Ok(bytes) = source.extract::<&[u8]>() {
42        py.detach(|| ExcelReader::try_from(bytes))
43            .with_context(|| "could not load excel file for those bytes")
44            .into_pyresult()
45    } else {
46        Err(py_errors::InvalidParametersError::new_err(
47            "source must be a string or bytes",
48        ))
49    }
50}
51
52// Taken from pydantic-core:
53// https://github.com/pydantic/pydantic-core/blob/main/src/lib.rs#L24
54#[cfg(feature = "python")]
55fn get_python_version() -> String {
56    let version = env!("CARGO_PKG_VERSION").to_string();
57    // cargo uses "1.0-alpha1" etc. while python uses "1.0.0a1", this is not full compatibility,
58    // but it's good enough for now
59    // see https://docs.rs/semver/1.0.9/semver/struct.Version.html#method.parse for rust spec
60    // see https://peps.python.org/pep-0440/ for python spec
61    // it seems the dot after "alpha/beta" e.g. "-alpha.1" is not necessary, hence why this works
62    version.replace("-alpha", "a").replace("-beta", "b")
63}
64
65#[cfg(feature = "python")]
66#[pymodule(gil_used = false)]
67fn _fastexcel(m: &Bound<'_, PyModule>) -> PyResult<()> {
68    use crate::types::excelsheet::column_info::{ColumnInfo, ColumnInfoNoDtype};
69
70    pyo3_log::init();
71
72    let py = m.py();
73    m.add_function(wrap_pyfunction!(py_read_excel, m)?)?;
74    m.add_class::<ColumnInfo>()?;
75    m.add_class::<ColumnInfoNoDtype>()?;
76    m.add_class::<DefinedName>()?;
77    m.add_class::<CellError>()?;
78    m.add_class::<CellErrors>()?;
79    m.add_class::<ExcelSheet>()?;
80    m.add_class::<ExcelReader>()?;
81    m.add_class::<ExcelTable>()?;
82    m.add("__version__", get_python_version())?;
83
84    // errors
85    [
86        ("FastExcelError", py.get_type::<py_errors::FastExcelError>()),
87        (
88            "UnsupportedColumnTypeCombinationError",
89            py.get_type::<py_errors::UnsupportedColumnTypeCombinationError>(),
90        ),
91        (
92            "CannotRetrieveCellDataError",
93            py.get_type::<py_errors::CannotRetrieveCellDataError>(),
94        ),
95        (
96            "CalamineCellError",
97            py.get_type::<py_errors::CalamineCellError>(),
98        ),
99        ("CalamineError", py.get_type::<py_errors::CalamineError>()),
100        (
101            "SheetNotFoundError",
102            py.get_type::<py_errors::SheetNotFoundError>(),
103        ),
104        (
105            "ColumnNotFoundError",
106            py.get_type::<py_errors::ColumnNotFoundError>(),
107        ),
108        ("ArrowError", py.get_type::<py_errors::ArrowError>()),
109        (
110            "InvalidParametersError",
111            py.get_type::<py_errors::InvalidParametersError>(),
112        ),
113    ]
114    .into_iter()
115    .try_for_each(|(exc_name, exc_type)| m.add(exc_name, exc_type))
116}