Skip to content

Commit

Permalink
Sign multiple URLs at once (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
kylebarron authored Oct 21, 2024
1 parent 57f3043 commit adbe5e3
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 90 deletions.
4 changes: 2 additions & 2 deletions object-store-rs/python/object_store_rs/_object_store_rs.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ from ._rename import rename as rename
from ._rename import rename_async as rename_async
from ._sign import HTTP_METHOD as HTTP_METHOD
from ._sign import SignCapableStore as SignCapableStore
from ._sign import sign_url as sign_url
from ._sign import sign_url_async as sign_url_async
from ._sign import sign as sign
from ._sign import sign_async as sign_async
57 changes: 44 additions & 13 deletions object-store-rs/python/object_store_rs/_sign.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import timedelta
from typing import Literal
from typing import List, Literal, Sequence, overload

from .store import AzureStore, GCSStore, S3Store

Expand All @@ -11,31 +11,62 @@ HTTP_METHOD = Literal[
SignCapableStore = AzureStore | GCSStore | S3Store
"""ObjectStore instances that are capable of signing."""

def sign_url(
store: SignCapableStore, method: HTTP_METHOD, path: str, expires_in: timedelta
) -> str:
@overload
def sign( # type: ignore
store: SignCapableStore, method: HTTP_METHOD, paths: str, expires_in: timedelta
) -> str: ...
@overload
def sign(
store: SignCapableStore,
method: HTTP_METHOD,
paths: Sequence[str],
expires_in: timedelta,
) -> List[str]: ...
def sign(
store: SignCapableStore,
method: HTTP_METHOD,
paths: str | Sequence[str],
expires_in: timedelta,
) -> str | List[str]:
"""Create a signed URL.
Given the intended [`Method`] and [`Path`] to use and the desired length of time for
which the URL should be valid, return a signed [`Url`] created with the object store
Given the intended `method` and `paths` to use and the desired length of time for
which the URL should be valid, return a signed URL created with the object store
implementation's credentials such that the URL can be handed to something that
doesn't have access to the object store's credentials, to allow limited access to
the object store.
Args:
store: The ObjectStore instance to use.
method: The HTTP method to use.
path: The path within ObjectStore to retrieve.
expires_in: How long the signed URL should be valid.
paths: The path(s) within ObjectStore to retrieve. If
expires_in: How long the signed URL(s) should be valid.
Returns:
_description_
"""

async def sign_url_async(
store: SignCapableStore, method: HTTP_METHOD, path: str, expires_in: timedelta
) -> str:
"""Call `sign_url` asynchronously.
@overload
async def sign_async(
store: SignCapableStore,
method: HTTP_METHOD,
paths: str,
expires_in: timedelta,
) -> str: ...
@overload
async def sign_async(
store: SignCapableStore,
method: HTTP_METHOD,
paths: Sequence[str],
expires_in: timedelta,
) -> List[str]: ...
async def sign_async(
store: SignCapableStore,
method: HTTP_METHOD,
paths: str | Sequence[str],
expires_in: timedelta,
) -> str | List[str]:
"""Call `sign` asynchronously.
Refer to the documentation for [sign_url][object_store_rs.sign_url].
Refer to the documentation for [sign][object_store_rs.sign].
"""
37 changes: 7 additions & 30 deletions object-store-rs/src/delete.rs
Original file line number Diff line number Diff line change
@@ -1,48 +1,25 @@
use futures::{StreamExt, TryStreamExt};
use object_store::path::Path;
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;
use pyo3_object_store::error::{PyObjectStoreError, PyObjectStoreResult};
use pyo3_object_store::PyObjectStore;

use crate::path::PyPaths;
use crate::runtime::get_runtime;

pub(crate) enum PyLocations {
One(Path),
// TODO: also support an Arrow String Array here.
Many(Vec<Path>),
}

impl<'py> FromPyObject<'py> for PyLocations {
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
if let Ok(path) = ob.extract::<String>() {
Ok(Self::One(path.into()))
} else if let Ok(paths) = ob.extract::<Vec<String>>() {
Ok(Self::Many(
paths.into_iter().map(|path| path.into()).collect(),
))
} else {
Err(PyTypeError::new_err(
"Expected string path or sequence of string paths.",
))
}
}
}

#[pyfunction]
pub(crate) fn delete(
py: Python,
store: PyObjectStore,
locations: PyLocations,
locations: PyPaths,
) -> PyObjectStoreResult<()> {
let runtime = get_runtime(py)?;
let store = store.into_inner();
py.allow_threads(|| {
match locations {
PyLocations::One(path) => {
PyPaths::One(path) => {
runtime.block_on(store.delete(&path))?;
}
PyLocations::Many(paths) => {
PyPaths::Many(paths) => {
// TODO: add option to allow some errors here?
let stream =
store.delete_stream(futures::stream::iter(paths.into_iter().map(Ok)).boxed());
Expand All @@ -57,18 +34,18 @@ pub(crate) fn delete(
pub(crate) fn delete_async(
py: Python,
store: PyObjectStore,
locations: PyLocations,
locations: PyPaths,
) -> PyResult<Bound<PyAny>> {
let store = store.into_inner();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
match locations {
PyLocations::One(path) => {
PyPaths::One(path) => {
store
.delete(&path)
.await
.map_err(PyObjectStoreError::ObjectStoreError)?;
}
PyLocations::Many(paths) => {
PyPaths::Many(paths) => {
// TODO: add option to allow some errors here?
let stream =
store.delete_stream(futures::stream::iter(paths.into_iter().map(Ok)).boxed());
Expand Down
5 changes: 3 additions & 2 deletions object-store-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod delete;
mod get;
mod head;
mod list;
mod path;
mod put;
mod rename;
mod runtime;
Expand Down Expand Up @@ -44,8 +45,8 @@ fn _object_store_rs(py: Python, m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(put::put))?;
m.add_wrapped(wrap_pyfunction!(rename::rename_async))?;
m.add_wrapped(wrap_pyfunction!(rename::rename))?;
m.add_wrapped(wrap_pyfunction!(signer::sign_url_async))?;
m.add_wrapped(wrap_pyfunction!(signer::sign_url))?;
m.add_wrapped(wrap_pyfunction!(signer::sign_async))?;
m.add_wrapped(wrap_pyfunction!(signer::sign))?;

Ok(())
}
25 changes: 25 additions & 0 deletions object-store-rs/src/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use object_store::path::Path;
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;

pub(crate) enum PyPaths {
One(Path),
// TODO: also support an Arrow String Array here.
Many(Vec<Path>),
}

impl<'py> FromPyObject<'py> for PyPaths {
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
if let Ok(path) = ob.extract::<String>() {
Ok(Self::One(path.into()))
} else if let Ok(paths) = ob.extract::<Vec<String>>() {
Ok(Self::Many(
paths.into_iter().map(|path| path.into()).collect(),
))
} else {
Err(PyTypeError::new_err(
"Expected string path or sequence of string paths.",
))
}
}
}
Loading

0 comments on commit adbe5e3

Please sign in to comment.