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

Prefix store #117

Merged
merged 5 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/api/store/middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Middleware

Wrappers around other `ObjectStore` instances to provide monitoring or other modifications.

::: obstore.store.PrefixStore
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ nav:
- api/store/local.md
- api/store/memory.md
- api/store/config.md
- api/store/middleware.md
- api/copy.md
- api/delete.md
- api/get.md
Expand Down
2 changes: 1 addition & 1 deletion obstore/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "obstore"
version = "0.3.0-beta.9"
version = "0.3.0-beta.10"
authors = { workspace = true }
edition = { workspace = true }
description = "A Python interface to the Rust object_store crate, providing a uniform API for interacting with object storage services and local files."
Expand Down
5 changes: 4 additions & 1 deletion obstore/python/obstore/store/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ from ._client import ClientConfigKey as ClientConfigKey
from ._gcs import GCSConfigKey as GCSConfigKey
from ._gcs import GCSStore as GCSStore
from ._http import HTTPStore as HTTPStore
from ._prefix import PrefixStore as PrefixStore
from ._retry import BackoffConfig as BackoffConfig
from ._retry import RetryConfig as RetryConfig

Expand Down Expand Up @@ -57,5 +58,7 @@ class MemoryStore:
def __init__(self) -> None: ...
def __repr__(self) -> str: ...

ObjectStore = AzureStore | GCSStore | HTTPStore | S3Store | LocalStore | MemoryStore
ObjectStore = (
AzureStore | GCSStore | HTTPStore | S3Store | LocalStore | MemoryStore | PrefixStore
)
"""All supported ObjectStore implementations."""
42 changes: 42 additions & 0 deletions obstore/python/obstore/store/_prefix.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from obstore.store import ObjectStore

class PrefixStore:
"""Store wrapper that applies a constant prefix to all paths handled by the store.

**Example**:

```py
import obstore as obs
from obstore.store import MemoryStore, PrefixStore

store = MemoryStore()

data = b"the quick brown fox jumps over the lazy dog"
path = "a/b/c/data.txt"

obs.put(store, path, data)

prefix_store = PrefixStore(store, "a/")
assert obs.get(prefix_store, "b/c/data.txt").bytes() == data

# The / after the passed-in prefix is inferred
prefix_store2 = PrefixStore(store, "a")
assert obs.get(prefix_store2, "b/c/data.txt").bytes() == data

# The prefix is removed from list results
assert obs.list(prefix_store).collect()[0]["path"] == "b/c/data.txt"

# More deeply nested prefix
prefix_store3 = PrefixStore(store, "a/b/c")
assert obs.get(prefix_store3, "data.txt").bytes() == data
```
"""
def __init__(self, store: ObjectStore, prefix: str) -> None:
"""Create a new PrefixStore with the provided prefix.

Args:
store: The underlying store to wrap.
prefix: If the prefix does not end with `/`, one will be added.
"""

def __repr__(self) -> str: ...
5 changes: 4 additions & 1 deletion pyo3-object_store/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use pyo3::intern;
use pyo3::prelude::*;

use crate::error::*;
use crate::{PyAzureStore, PyGCSStore, PyHttpStore, PyLocalStore, PyMemoryStore, PyS3Store};
use crate::{
PyAzureStore, PyGCSStore, PyHttpStore, PyLocalStore, PyMemoryStore, PyPrefixStore, PyS3Store,
};

/// Export the default Python API as a submodule named `store` within the given parent module
///
Expand Down Expand Up @@ -50,6 +52,7 @@ pub fn register_store_module(
child_module.add_class::<PyLocalStore>()?;
child_module.add_class::<PyMemoryStore>()?;
child_module.add_class::<PyS3Store>()?;
child_module.add_class::<PyPrefixStore>()?;

parent_module.add_submodule(&child_module)?;

Expand Down
2 changes: 2 additions & 0 deletions pyo3-object_store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod gcp;
mod http;
mod local;
mod memory;
mod prefix;
mod retry;
mod store;

Expand All @@ -22,4 +23,5 @@ pub use gcp::PyGCSStore;
pub use http::PyHttpStore;
pub use local::PyLocalStore;
pub use memory::PyMemoryStore;
pub use prefix::PyPrefixStore;
pub use store::PyObjectStore;
30 changes: 30 additions & 0 deletions pyo3-object_store/src/prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std::sync::Arc;

use pyo3::prelude::*;

use object_store::prefix::PrefixStore;
use object_store::ObjectStore;

use crate::PyObjectStore;

/// A Python-facing wrapper around a [`PrefixStore`].
#[pyclass(name = "PrefixStore", frozen)]
pub struct PyPrefixStore(Arc<PrefixStore<Arc<dyn ObjectStore>>>);

impl AsRef<Arc<PrefixStore<Arc<dyn ObjectStore>>>> for PyPrefixStore {
fn as_ref(&self) -> &Arc<PrefixStore<Arc<dyn ObjectStore>>> {
&self.0
}
}

#[pymethods]
impl PyPrefixStore {
#[new]
fn new(store: PyObjectStore, prefix: String) -> Self {
Self(Arc::new(PrefixStore::new(store.into_inner(), prefix)))
}

fn __repr__(&self) -> String {
self.0.to_string()
}
}
8 changes: 6 additions & 2 deletions pyo3-object_store/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use pyo3::intern;
use pyo3::prelude::*;
use pyo3::pybacked::PyBackedStr;

use crate::http::PyHttpStore;
use crate::{PyAzureStore, PyGCSStore, PyLocalStore, PyMemoryStore, PyS3Store};
use crate::{
PyAzureStore, PyGCSStore, PyHttpStore, PyLocalStore, PyMemoryStore, PyPrefixStore, PyS3Store,
};

/// A wrapper around a Rust ObjectStore instance that allows any rust-native implementation of
/// ObjectStore.
Expand All @@ -29,6 +30,8 @@ impl<'py> FromPyObject<'py> for PyObjectStore {
Ok(Self(store.get().as_ref().clone()))
} else if let Ok(store) = ob.downcast::<PyMemoryStore>() {
Ok(Self(store.get().as_ref().clone()))
} else if let Ok(store) = ob.downcast::<PyPrefixStore>() {
Ok(Self(store.get().as_ref().clone()))
} else {
let py = ob.py();
// Check for object-store instance from other library
Expand All @@ -43,6 +46,7 @@ impl<'py> FromPyObject<'py> for PyObjectStore {
"LocalStore",
"MemoryStore",
"S3Store",
"PrefixStore",
]
.contains(&cls_name.as_ref())
{
Expand Down
25 changes: 25 additions & 0 deletions tests/store/test_prefix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import obstore as obs
from obstore.store import MemoryStore, PrefixStore


def test_prefix_store():
store = MemoryStore()

data = b"the quick brown fox jumps over the lazy dog"
path = "a/b/c/data.txt"

obs.put(store, path, data)

prefix_store = PrefixStore(store, "a/")
assert obs.get(prefix_store, "b/c/data.txt").bytes() == data

# The / after the passed-in prefix is inferred
prefix_store2 = PrefixStore(store, "a")
assert obs.get(prefix_store2, "b/c/data.txt").bytes() == data

# The prefix is removed from list results
assert obs.list(prefix_store).collect()[0]["path"] == "b/c/data.txt"

# More deeply nested prefix
prefix_store3 = PrefixStore(store, "a/b/c")
assert obs.get(prefix_store3, "data.txt").bytes() == data
Loading