Skip to content

Commit 3fce81c

Browse files
authored
Add developer documentation for functional API choice (#215)
1 parent 71e873a commit 3fce81c

File tree

2 files changed

+73
-0
lines changed

2 files changed

+73
-0
lines changed

docs/dev/functional-api.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Functional API Design Choice
2+
3+
> Last edited 2025-02-04.
4+
>
5+
> See further discussion in [this issue](https://github.com/developmentseed/obstore/issues/160).
6+
7+
Obstore intentionally presents its main API as top-level functions. E.g. users must use the top level `obstore.put` function:
8+
9+
```py
10+
import obstore as obs
11+
from obstore.store import AzureStore
12+
13+
store = AzureStore()
14+
obs.put(store, ....)
15+
```
16+
17+
instead of a method on the store itself:
18+
19+
```py
20+
import obstore as obs
21+
from obstore.store import AzureStore
22+
23+
store = AzureStore()
24+
store.put(....)
25+
```
26+
27+
This page documents the design decisions for this API.
28+
29+
## Store-specific vs generic API
30+
31+
This presents a nice separation of concerns, in my opinion, between store-specific properties and a generic API that works for _every_ `ObjectStore`.
32+
33+
Python store classes such as `S3Store` have a few properties to access the _store-specific_ configuration, e.g. `S3Store.config` accesses the S3 credentials. Anything that's a property/method of the store class is specific to that type of store. Whereas any top-level method should work on _any_ store equally well.
34+
35+
## Simpler Rust code
36+
37+
On the Rust side, each Python class is a separate `struct`. A pyo3 `#[pyclass]` can't implement a trait, so the only way to implement the same methods on multiple Rust structs without copy-pasting is by having a macro. That isn't out of the question, however it does hamper extensibility, and having one and only one way to call commands is simpler to maintain.
38+
39+
## Simpler Middlewares
40+
41+
> The `PrefixStore` concept has since been taken out, in favor of natively handling store prefixes, but this argument still holds for other potential middlewares in the future.
42+
43+
In https://github.com/developmentseed/obstore/pull/117 we added a binding for `PrefixStore`. Because we use object store classes functionally, we only needed 20 lines of Rust code:
44+
https://github.com/developmentseed/obstore/blob/b40d59b4e060ba4fd3dc69468b3ba7da1149758e/pyo3-object_store/src/prefix.rs#L10-L25
45+
46+
If we exposed methods on an `S3Store`, then those methods would be lost whenever you apply a middleware around it, such as `PrefixStore(S3Store(...))`. So we'd have to ensure those same methods are also installed onto every middleware or other wrapper.
47+
48+
## External FFI for ObjectStore
49+
50+
There was recently [discussion on Discord](https://discord.com/channels/885562378132000778/885562378132000781/1328392836353360007) about the merits of having a stable FFI for `ObjectStore`. If this comes to fruition in the future, then by having a functional API we could seamlessly use _third party_ ObjectStore implementations or middlewares, with no Python overhead.
51+
52+
I use a similar functional API in other Python bindings, especially in cases with zero-copy FFI, such as https://kylebarron.dev/geo-index/latest/api/rtree/#geoindex_rs.rtree.search (where the spatial index is passed in as the first argument instead) and https://kylebarron.dev/arro3/latest/api/compute/#arro3.compute.cast where the `cast` is not a method on the Arrow Array.
53+
54+
## Smaller core for third-party Rust bindings
55+
56+
This repo has twin goals:
57+
58+
1. Provide bindings to `object_store` for _Python users_ who want a _Python API_.
59+
2. Make it easier for other Rust developers who are making Python bindings, who are using `object_store` on the Rust side already, and who want to expose `ObjectStore` bindings to Python in their own projects.
60+
61+
The first goal is served by the `obstore` Python package and the second is served by the `pyo3-object_store` Rust crate. The latter provides builders for `S3Store`, `AzureStore`, `GCSStore`, which means that those third party Rust-Python bindings can have code as simple as:
62+
63+
```rs
64+
#[pyfunction]
65+
fn use_object_store(store: PyObjectStore) {
66+
let store: Arc<dyn ObjectStore> = store.into_inner();
67+
}
68+
```
69+
70+
Those third party bindings don't need the Python bindings to perform arbitrary `get`, `list`, `put` from Python. Instead, they use this to access a raw `Arc<dyn ObjectStore>` from the Rust side.
71+
72+
You'll notice that `S3Store`, `GCSStore`, and `AzureStore` **aren't** in the `obstore` library; they're in `pyo3-object_store`. We can't add methods to a pyclass from an external crate, so we couldn't leave those builders in `pyo3_object_store` while having the Python-facing operations live in `obstore`. Instead we'd have to put the entire content of the Python bindings in the `pyo3-object_store` crate. Then this would expose whatever class methods from the `obstore` Python API onto any external Rust-Python library that uses `pyo3-object_store`. I don't want to leak this abstraction nor make that public to other Rust consumers.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ nav:
5353
- Advanced:
5454
- advanced/pickle.md
5555
- Developer Docs:
56+
- dev/functional-api.md
5657
- dev/pickle.md
5758
- CHANGELOG.md
5859

0 commit comments

Comments
 (0)