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

feat(bindings/python): expose presign api #2950

Merged
merged 7 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 17 additions & 8 deletions bindings/python/CONTRIBUTING.md
@@ -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
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: int) -> PresignedRequest: ...
Xuanwo marked this conversation as resolved.
Show resolved Hide resolved
async def presign_read(self, path: str, expire: int) -> PresignedRequest: ...
async def presign_write(self, path: str, expire: 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, bytes]: ...
65 changes: 65 additions & 0 deletions bindings/python/src/asyncio.rs
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,69 @@ impl AsyncOperator {
})
}

/// Presign an operation for stat(head).
///
/// The returned `PresignedRequest` will be expired after `expire` seconds.
pub fn presign_stat<'p>(
&'p self,
py: Python<'p>,
path: String,
expire: 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))
.await
.map_err(format_pyerr)
.map(PresignedRequest)?;

Ok(res)
})
}

/// Presign an operation for read.
///
/// The returned `PresignedRequest` will be expired after `expire` seconds.
pub fn presign_read<'p>(
&'p self,
py: Python<'p>,
path: String,
expire: 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))
.await
.map_err(format_pyerr)
.map(PresignedRequest)?;

Ok(res)
})
}

/// Presign an operation for write.
///
/// The returned `PresignedRequest` will be expired after `expire` seconds.
pub fn presign_write<'p>(
&'p self,
py: Python<'p>,
path: String,
expire: 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))
.await
.map_err(format_pyerr)
.map(PresignedRequest)?;

Ok(res)
})
}

fn __repr__(&self) -> String {
let info = self.0.info();
let name = info.name();
Expand Down
29 changes: 29 additions & 0 deletions bindings/python/src/lib.rs
Expand Up @@ -368,6 +368,34 @@ 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<'p>(&'p self, py: Python<'p>) -> HashMap<&'p str, &'p PyBytes> {
silver-ymz marked this conversation as resolved.
Show resolved Hide resolved
self.0
.header()
.iter()
.map(|(k, v)| (k.as_str(), PyBytes::new(py, v.as_bytes())))
.collect()
}
}

fn format_pyerr(err: od::Error) -> PyErr {
use od::ErrorKind::*;
match err.kind() {
Expand Down Expand Up @@ -416,6 +444,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
11 changes: 11 additions & 0 deletions bindings/python/tests/steps/binding.py
Expand Up @@ -88,3 +88,14 @@ async def step_impl(context, filename, size):
async def step_impl(context, filename, content):
bs = await context.op.read(filename)
assert bs == content.encode()

@given("A new OpenDAL Async Operator (s3 presign only)")
def step_impl(context):
context.op = opendal.AsyncOperator("s3", bucket="test", region="us-east-1", access_key_id="test", secret_access_key="test")

@then("The operator is available for presign")
@async_run_until_complete
async def step_impl(context):
await context.op.presign_stat("test.txt", 10)
await context.op.presign_read("test.txt", 10)
await context.op.presign_write("test.txt", 10)
4 changes: 4 additions & 0 deletions bindings/tests/features/binding.feature
Expand Up @@ -32,3 +32,7 @@ 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!"

Scenario: OpenDAL Operations with Presign
Given A new OpenDAL Async Operator (s3 presign only)
Xuanwo marked this conversation as resolved.
Show resolved Hide resolved
Then The operator is available for presign