Skip to content

Commit

Permalink
feat(bindings/python): expose presign api (#2950)
Browse files Browse the repository at this point in the history
* feat(bindings/python): expose presign api

Signed-off-by: silver-ymz <yinmingzhuo@gmail.com>

* add test for presign

Signed-off-by: silver-ymz <yinmingzhuo@gmail.com>

* fix HeaderMap to return error for invalid value

Signed-off-by: silver-ymz <yinmingzhuo@gmail.com>

* update headers function signature && rename presign test

Signed-off-by: silver-ymz <yinmingzhuo@gmail.com>

* typo

Signed-off-by: silver-ymz <yinmingzhuo@gmail.com>

* update test for presign

Signed-off-by: silver-ymz <yinmingzhuo@gmail.com>

* make clear for expire second

Signed-off-by: silver-ymz <yinmingzhuo@gmail.com>

---------

Signed-off-by: silver-ymz <yinmingzhuo@gmail.com>
  • Loading branch information
silver-ymz committed Aug 29, 2023
1 parent e144caf commit 8169efc
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 8 deletions.
25 changes: 17 additions & 8 deletions bindings/python/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Contributing

- [Setup](#setup)
- [Using a dev container environment](#using-a-devcontainer-environment)
- [Bring your own toolbox](#bring-your-own-toolbox)
- [Prepare](#prepare)
- [Build](#build)
- [Test](#test)
- [Docs](#docs)
- [Contributing](#contributing)
- [Setup](#setup)
- [Using a dev container environment](#using-a-dev-container-environment)
- [Bring your own toolbox](#bring-your-own-toolbox)
- [Prepare](#prepare)
- [Build](#build)
- [Test](#test)
- [Docs](#docs)

## Setup

Expand Down Expand Up @@ -50,12 +51,20 @@ pip install maturin[patchelf]

## Build

To build python binding:
To build python binding only:

```shell
maturin build
```

To build and install python binding directly in the current virtualenv:

```shell
maturin develop
```

Note: `maturin develop` will be faster, but doesn't support all the features. In most development cases, we recommend using `maturin develop`.

## Test

OpenDAL adopts `behave` for behavior tests:
Expand Down
11 changes: 11 additions & 0 deletions bindings/python/python/opendal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class AsyncOperator:
async def delete(self, path: str): ...
async def list(self, path: str) -> AsyncIterable[Entry]: ...
async def scan(self, path: str) -> AsyncIterable[Entry]: ...
async def presign_stat(self, path: str, expire_second: int) -> PresignedRequest: ...
async def presign_read(self, path: str, expire_second: int) -> PresignedRequest: ...
async def presign_write(self, path: str, expire_second: int) -> PresignedRequest: ...

class Reader:
def read(self, size: Optional[int] = None) -> bytes: ...
Expand Down Expand Up @@ -76,3 +79,11 @@ class Metadata:
class EntryMode:
def is_file(self) -> bool: ...
def is_dir(self) -> bool: ...

class PresignedRequest:
@property
def url(self) -> str: ...
@property
def method(self) -> str: ...
@property
def headers(self) -> dict[str, str]: ...
59 changes: 59 additions & 0 deletions bindings/python/src/asyncio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::collections::HashMap;
use std::io::SeekFrom;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

use ::opendal as od;
use futures::TryStreamExt;
Expand All @@ -39,6 +40,7 @@ use crate::format_pyerr;
use crate::layers;
use crate::Entry;
use crate::Metadata;
use crate::PresignedRequest;

/// `AsyncOperator` is the entry for all public async APIs
///
Expand Down Expand Up @@ -159,6 +161,63 @@ impl AsyncOperator {
})
}

/// Presign an operation for stat(head) which expires after `expire_second` seconds.
pub fn presign_stat<'p>(
&'p self,
py: Python<'p>,
path: String,
expire_second: u64,
) -> PyResult<&'p PyAny> {
let this = self.0.clone();
future_into_py(py, async move {
let res = this
.presign_stat(&path, Duration::from_secs(expire_second))
.await
.map_err(format_pyerr)
.map(PresignedRequest)?;

Ok(res)
})
}

/// Presign an operation for read which expires after `expire_second` seconds.
pub fn presign_read<'p>(
&'p self,
py: Python<'p>,
path: String,
expire_second: u64,
) -> PyResult<&'p PyAny> {
let this = self.0.clone();
future_into_py(py, async move {
let res = this
.presign_read(&path, Duration::from_secs(expire_second))
.await
.map_err(format_pyerr)
.map(PresignedRequest)?;

Ok(res)
})
}

/// Presign an operation for write which expires after `expire_second` seconds.
pub fn presign_write<'p>(
&'p self,
py: Python<'p>,
path: String,
expire_second: u64,
) -> PyResult<&'p PyAny> {
let this = self.0.clone();
future_into_py(py, async move {
let res = this
.presign_write(&path, Duration::from_secs(expire_second))
.await
.map_err(format_pyerr)
.map(PresignedRequest)?;

Ok(res)
})
}

fn __repr__(&self) -> String {
let info = self.0.info();
let name = info.name();
Expand Down
38 changes: 38 additions & 0 deletions bindings/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ use std::str::FromStr;
use ::opendal as od;
use pyo3::create_exception;
use pyo3::exceptions::PyException;
use pyo3::exceptions::PyFileExistsError;
use pyo3::exceptions::PyFileNotFoundError;
use pyo3::exceptions::PyIOError;
use pyo3::exceptions::PyNotImplementedError;
use pyo3::exceptions::PyPermissionError;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
Expand Down Expand Up @@ -368,10 +370,45 @@ impl EntryMode {
}
}

#[pyclass(module = "opendal")]
struct PresignedRequest(od::raw::PresignedRequest);

#[pymethods]
impl PresignedRequest {
/// Return the URL of this request.
#[getter]
pub fn url(&self) -> String {
self.0.uri().to_string()
}

/// Return the HTTP method of this request.
#[getter]
pub fn method(&self) -> &str {
self.0.method().as_str()
}

/// Return the HTTP headers of this request.
#[getter]
pub fn headers(&self) -> PyResult<HashMap<&str, &str>> {
let mut headers = HashMap::new();
for (k, v) in self.0.header().iter() {
let k = k.as_str();
let v = v.to_str().map_err(|err| Error::new_err(err.to_string()))?;
if headers.insert(k, v).is_some() {
return Err(Error::new_err("duplicate header"));
}
}
Ok(headers)
}
}

fn format_pyerr(err: od::Error) -> PyErr {
use od::ErrorKind::*;
match err.kind() {
NotFound => PyFileNotFoundError::new_err(err.to_string()),
AlreadyExists => PyFileExistsError::new_err(err.to_string()),
PermissionDenied => PyPermissionError::new_err(err.to_string()),
Unsupported => PyNotImplementedError::new_err(err.to_string()),
_ => Error::new_err(err.to_string()),
}
}
Expand Down Expand Up @@ -416,6 +453,7 @@ fn _opendal(py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<Entry>()?;
m.add_class::<EntryMode>()?;
m.add_class::<Metadata>()?;
m.add_class::<PresignedRequest>()?;
m.add("Error", py.get_type::<Error>())?;

let layers = layers::create_submodule(py)?;
Expand Down
10 changes: 10 additions & 0 deletions bindings/python/tests/steps/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,13 @@ async def step_impl(context, filename, size):
async def step_impl(context, filename, content):
bs = await context.op.read(filename)
assert bs == content.encode()

@then("The presign operation should success or raise exception Unsupported")
@async_run_until_complete
async def step_impl(context):
try:
await context.op.presign_stat("test.txt", 10)
await context.op.presign_read("test.txt", 10)
await context.op.presign_write("test.txt", 10)
except NotImplementedError:
pass
1 change: 1 addition & 0 deletions bindings/tests/features/binding.feature
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ Feature: OpenDAL Binding
Then The async file "test" entry mode must be file
Then The async file "test" content length must be 13
Then The async file "test" must have content "Hello, World!"
Then The presign operation should success or raise exception Unsupported

0 comments on commit 8169efc

Please sign in to comment.