diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index 0db67810589b..4c0d444e6e73 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -198,7 +198,6 @@ name = "_opendal" [dependencies] bytes = "1.5.0" -dict_derive = "0.6.0" futures = "0.3.28" jiff = { version = "0.2.15" } mea = "0.5.1" @@ -207,8 +206,8 @@ opendal = { version = ">=0", path = "../../core", features = [ "blocking", "layers-mime-guess", ] } -pyo3 = { version = "0.26.0", features = ["generate-import-lib", "jiff-02"] } -pyo3-async-runtimes = { version = "0.26.0", features = ["tokio-runtime"] } +pyo3 = { version = "0.27.2", features = ["generate-import-lib", "jiff-02"] } +pyo3-async-runtimes = { version = "0.27.0", features = ["tokio-runtime"] } pyo3-stub-gen = { version = "0.17" } tokio = "1" diff --git a/bindings/python/src/options.rs b/bindings/python/src/options.rs index fa5f74061930..f623be266d5f 100644 --- a/bindings/python/src/options.rs +++ b/bindings/python/src/options.rs @@ -15,13 +15,22 @@ // specific language governing permissions and limitations // under the License. -use dict_derive::FromPyObject; use opendal::{self as ocore, raw::BytesRange}; +use pyo3::Borrowed; +use pyo3::FromPyObject; +use pyo3::PyAny; +use pyo3::PyErr; +use pyo3::PyResult; +use pyo3::conversion::FromPyObjectOwned; +use pyo3::exceptions::PyTypeError; use pyo3::pyclass; +use pyo3::types::PyAnyMethods; +use pyo3::types::PyDict; +use pyo3::types::PyDictMethods; use std::collections::HashMap; #[pyclass(module = "opendal")] -#[derive(FromPyObject, Default)] +#[derive(Default)] pub struct ReadOptions { pub version: Option, pub concurrent: Option, @@ -39,6 +48,55 @@ pub struct ReadOptions { pub content_disposition: Option, } +fn map_exception(name: &str, err: PyErr) -> PyErr { + PyErr::new::(format!("Unable to convert key: {name}. Error: {err}")) +} + +fn extract_optional<'py, T>(dict: &pyo3::Bound<'py, PyDict>, name: &str) -> PyResult> +where + T: FromPyObjectOwned<'py>, +{ + match dict.get_item(name)? { + Some(v) => v + .extract::() + .map(Some) + .map_err(|err| map_exception(name, err.into())), + None => Ok(None), + } +} + +fn downcast_kwargs<'a, 'py>(obj: Borrowed<'a, 'py, PyAny>) -> PyResult> { + let obj: &pyo3::Bound<'_, PyAny> = &obj; + obj.cast::() + .cloned() + .map_err(|_| PyErr::new::("Invalid type to convert, expected dict")) +} + +impl<'a, 'py> FromPyObject<'a, 'py> for ReadOptions { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + let dict = downcast_kwargs(obj)?; + + Ok(Self { + version: extract_optional(&dict, "version")?, + concurrent: extract_optional(&dict, "concurrent")?, + chunk: extract_optional(&dict, "chunk")?, + gap: extract_optional(&dict, "gap")?, + offset: extract_optional(&dict, "offset")?, + prefetch: extract_optional(&dict, "prefetch")?, + size: extract_optional(&dict, "size")?, + if_match: extract_optional(&dict, "if_match")?, + if_none_match: extract_optional(&dict, "if_none_match")?, + if_modified_since: extract_optional(&dict, "if_modified_since")?, + if_unmodified_since: extract_optional(&dict, "if_unmodified_since")?, + content_type: extract_optional(&dict, "content_type")?, + cache_control: extract_optional(&dict, "cache_control")?, + content_disposition: extract_optional(&dict, "content_disposition")?, + }) + } +} + impl ReadOptions { pub fn make_range(&self) -> BytesRange { let offset = self.offset.unwrap_or_default() as u64; @@ -49,7 +107,7 @@ impl ReadOptions { } #[pyclass(module = "opendal")] -#[derive(FromPyObject, Default)] +#[derive(Default)] pub struct WriteOptions { pub append: Option, pub chunk: Option, @@ -64,6 +122,28 @@ pub struct WriteOptions { pub user_metadata: Option>, } +impl<'a, 'py> FromPyObject<'a, 'py> for WriteOptions { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + let dict = downcast_kwargs(obj)?; + + Ok(Self { + append: extract_optional(&dict, "append")?, + chunk: extract_optional(&dict, "chunk")?, + concurrent: extract_optional(&dict, "concurrent")?, + cache_control: extract_optional(&dict, "cache_control")?, + content_type: extract_optional(&dict, "content_type")?, + content_disposition: extract_optional(&dict, "content_disposition")?, + content_encoding: extract_optional(&dict, "content_encoding")?, + if_match: extract_optional(&dict, "if_match")?, + if_none_match: extract_optional(&dict, "if_none_match")?, + if_not_exists: extract_optional(&dict, "if_not_exists")?, + user_metadata: extract_optional(&dict, "user_metadata")?, + }) + } +} + impl From for ocore::options::ReadOptions { fn from(opts: ReadOptions) -> Self { Self { @@ -118,7 +198,7 @@ impl From for ocore::options::WriteOptions { } #[pyclass(module = "opendal")] -#[derive(FromPyObject, Default, Debug)] +#[derive(Default, Debug)] pub struct ListOptions { pub limit: Option, pub start_after: Option, @@ -127,6 +207,22 @@ pub struct ListOptions { pub deleted: Option, } +impl<'a, 'py> FromPyObject<'a, 'py> for ListOptions { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + let dict = downcast_kwargs(obj)?; + + Ok(Self { + limit: extract_optional(&dict, "limit")?, + start_after: extract_optional(&dict, "start_after")?, + recursive: extract_optional(&dict, "recursive")?, + versions: extract_optional(&dict, "versions")?, + deleted: extract_optional(&dict, "deleted")?, + }) + } +} + impl From for ocore::options::ListOptions { fn from(opts: ListOptions) -> Self { Self { @@ -140,7 +236,7 @@ impl From for ocore::options::ListOptions { } #[pyclass(module = "opendal")] -#[derive(FromPyObject, Default, Debug)] +#[derive(Default, Debug)] pub struct StatOptions { pub version: Option, pub if_match: Option, @@ -152,6 +248,25 @@ pub struct StatOptions { pub content_disposition: Option, } +impl<'a, 'py> FromPyObject<'a, 'py> for StatOptions { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + let dict = downcast_kwargs(obj)?; + + Ok(Self { + version: extract_optional(&dict, "version")?, + if_match: extract_optional(&dict, "if_match")?, + if_none_match: extract_optional(&dict, "if_none_match")?, + if_modified_since: extract_optional(&dict, "if_modified_since")?, + if_unmodified_since: extract_optional(&dict, "if_unmodified_since")?, + content_type: extract_optional(&dict, "content_type")?, + cache_control: extract_optional(&dict, "cache_control")?, + content_disposition: extract_optional(&dict, "content_disposition")?, + }) + } +} + impl From for ocore::options::StatOptions { fn from(opts: StatOptions) -> Self { Self {