Skip to content

Commit 06e6f2d

Browse files
authored
Python: Support obspec reader input to TIFF.open (#64)
1 parent 32eba16 commit 06e6f2d

File tree

14 files changed

+182
-29
lines changed

14 files changed

+182
-29
lines changed

python/Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ crate-type = ["cdylib"]
1919
[dependencies]
2020
async-tiff = { path = "../" }
2121
bytes = "1.10.1"
22+
futures = "0.3.31"
2223
object_store = "0.12"
2324
pyo3 = { version = "0.23.0", features = ["macros"] }
2425
pyo3-async-runtimes = "0.23"

python/docs/api/tiff.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
::: async_tiff.TIFF
44
options:
55
show_if_no_docstring: true
6+
::: async_tiff.ObspecReader

python/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "maturin"
55
[project]
66
name = "async-tiff"
77
requires-python = ">=3.9"
8-
dependencies = []
8+
dependencies = ["obspec>=0.1.0-beta.3"]
99
dynamic = ["version"]
1010
classifiers = [
1111
"Programming Language :: Rust",

python/python/async_tiff/_async_tiff.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ from ._decoder import DecoderRegistry as DecoderRegistry
33
from ._geo import GeoKeyDirectory as GeoKeyDirectory
44
from ._ifd import ImageFileDirectory as ImageFileDirectory
55
from ._thread_pool import ThreadPool as ThreadPool
6+
from ._tiff import ObspecInput as ObspecInput
67
from ._tiff import TIFF as TIFF
78
from ._tile import Tile as Tile

python/python/async_tiff/_tiff.pyi

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1-
import obstore
1+
from typing import Protocol
22
from ._tile import Tile
33
from ._ifd import ImageFileDirectory
44
from .store import ObjectStore
55

6+
# Fix exports
7+
from obspec._get import GetRangeAsync, GetRangesAsync
8+
9+
class ObspecInput(GetRangeAsync, GetRangesAsync, Protocol):
10+
"""Supported obspec input to reader."""
11+
612
class TIFF:
713
@classmethod
814
async def open(
915
cls,
1016
path: str,
1117
*,
12-
store: obstore.store.ObjectStore | ObjectStore,
18+
store: ObjectStore | ObspecInput,
1319
prefetch: int | None = 16384,
1420
) -> TIFF:
1521
"""Open a new TIFF.

python/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod decoder;
44
mod enums;
55
mod geo;
66
mod ifd;
7+
mod reader;
78
mod thread_pool;
89
mod tiff;
910
mod tile;

python/src/reader.rs

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use std::ops::Range;
2+
use std::sync::Arc;
3+
4+
use async_tiff::error::{AsyncTiffError, AsyncTiffResult};
5+
use async_tiff::reader::{AsyncFileReader, ObjectReader};
6+
use bytes::Bytes;
7+
use futures::future::BoxFuture;
8+
use futures::FutureExt;
9+
use pyo3::exceptions::PyTypeError;
10+
use pyo3::intern;
11+
use pyo3::prelude::*;
12+
use pyo3::types::PyDict;
13+
use pyo3_async_runtimes::tokio::into_future;
14+
use pyo3_bytes::PyBytes;
15+
use pyo3_object_store::PyObjectStore;
16+
17+
#[derive(FromPyObject)]
18+
pub(crate) enum StoreInput {
19+
ObjectStore(PyObjectStore),
20+
ObspecBackend(ObspecBackend),
21+
}
22+
23+
impl StoreInput {
24+
pub(crate) fn into_async_file_reader(self, path: String) -> Arc<dyn AsyncFileReader> {
25+
match self {
26+
Self::ObjectStore(store) => {
27+
Arc::new(ObjectReader::new(store.into_inner(), path.into()))
28+
}
29+
Self::ObspecBackend(backend) => Arc::new(ObspecReader { backend, path }),
30+
}
31+
}
32+
}
33+
34+
/// A Python backend for making requests that conforms to the GetRangeAsync and GetRangesAsync
35+
/// protocols defined by obspec.
36+
/// https://developmentseed.org/obspec/latest/api/get/#obspec.GetRangeAsync
37+
/// https://developmentseed.org/obspec/latest/api/get/#obspec.GetRangesAsync
38+
#[derive(Debug)]
39+
pub(crate) struct ObspecBackend(PyObject);
40+
41+
impl ObspecBackend {
42+
async fn get_range(&self, path: &str, range: Range<u64>) -> PyResult<PyBytes> {
43+
let future = Python::with_gil(|py| {
44+
let kwargs = PyDict::new(py);
45+
kwargs.set_item(intern!(py, "path"), path)?;
46+
kwargs.set_item(intern!(py, "start"), range.start)?;
47+
kwargs.set_item(intern!(py, "end"), range.end)?;
48+
49+
let coroutine = self
50+
.0
51+
.call_method(py, intern!(py, "get_range"), (), Some(&kwargs))?;
52+
into_future(coroutine.bind(py).clone())
53+
})?;
54+
let result = future.await?;
55+
Python::with_gil(|py| result.extract(py))
56+
}
57+
58+
async fn get_ranges(&self, path: &str, ranges: &[Range<u64>]) -> PyResult<Vec<PyBytes>> {
59+
let starts = ranges.iter().map(|r| r.start).collect::<Vec<_>>();
60+
let ends = ranges.iter().map(|r| r.end).collect::<Vec<_>>();
61+
62+
let future = Python::with_gil(|py| {
63+
let kwargs = PyDict::new(py);
64+
kwargs.set_item(intern!(py, "path"), path)?;
65+
kwargs.set_item(intern!(py, "starts"), starts)?;
66+
kwargs.set_item(intern!(py, "ends"), ends)?;
67+
68+
let coroutine = self
69+
.0
70+
.call_method(py, intern!(py, "get_range"), (), Some(&kwargs))?;
71+
into_future(coroutine.bind(py).clone())
72+
})?;
73+
let result = future.await?;
74+
Python::with_gil(|py| result.extract(py))
75+
}
76+
77+
async fn get_range_wrapper(&self, path: &str, range: Range<u64>) -> AsyncTiffResult<Bytes> {
78+
let result = self
79+
.get_range(path, range)
80+
.await
81+
.map_err(|err| AsyncTiffError::External(Box::new(err)))?;
82+
Ok(result.into_inner())
83+
}
84+
85+
async fn get_ranges_wrapper(
86+
&self,
87+
path: &str,
88+
ranges: Vec<Range<u64>>,
89+
) -> AsyncTiffResult<Vec<Bytes>> {
90+
let result = self
91+
.get_ranges(path, &ranges)
92+
.await
93+
.map_err(|err| AsyncTiffError::External(Box::new(err)))?;
94+
Ok(result.into_iter().map(|b| b.into_inner()).collect())
95+
}
96+
}
97+
98+
impl<'py> FromPyObject<'py> for ObspecBackend {
99+
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
100+
let py = ob.py();
101+
if ob.hasattr(intern!(py, "get_range_async"))?
102+
&& ob.hasattr(intern!(py, "get_ranges_async"))?
103+
{
104+
Ok(Self(ob.clone().unbind()))
105+
} else {
106+
Err(PyTypeError::new_err("Expected obspec-compatible class with `get_range_async` and `get_ranges_async` method."))
107+
}
108+
}
109+
}
110+
111+
#[derive(Debug)]
112+
struct ObspecReader {
113+
backend: ObspecBackend,
114+
path: String,
115+
}
116+
117+
impl AsyncFileReader for ObspecReader {
118+
fn get_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
119+
self.backend.get_range_wrapper(&self.path, range).boxed()
120+
}
121+
122+
fn get_byte_ranges(
123+
&self,
124+
ranges: Vec<Range<u64>>,
125+
) -> BoxFuture<'_, AsyncTiffResult<Vec<Bytes>>> {
126+
self.backend.get_ranges_wrapper(&self.path, ranges).boxed()
127+
}
128+
}

python/src/tiff.rs

+9-14
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use std::sync::Arc;
22

3-
use async_tiff::reader::{AsyncFileReader, ObjectReader, PrefetchReader};
3+
use async_tiff::reader::{AsyncFileReader, PrefetchReader};
44
use async_tiff::TIFF;
55
use pyo3::exceptions::PyIndexError;
66
use pyo3::prelude::*;
77
use pyo3::types::PyType;
88
use pyo3_async_runtimes::tokio::future_into_py;
9-
use pyo3_object_store::AnyObjectStore;
109

10+
use crate::reader::StoreInput;
1111
use crate::tile::PyTile;
1212
use crate::PyImageFileDirectory;
1313

@@ -25,25 +25,20 @@ impl PyTIFF {
2525
_cls: &'py Bound<PyType>,
2626
py: Python<'py>,
2727
path: String,
28-
store: AnyObjectStore,
28+
store: StoreInput,
2929
prefetch: Option<u64>,
3030
) -> PyResult<Bound<'py, PyAny>> {
31-
let reader = ObjectReader::new(store.into_dyn(), path.into());
32-
let object_reader = reader.clone();
31+
let reader = store.into_async_file_reader(path);
3332

3433
let cog_reader = future_into_py(py, async move {
35-
let reader: Box<dyn AsyncFileReader> = if let Some(prefetch) = prefetch {
36-
Box::new(
37-
PrefetchReader::new(Box::new(reader), prefetch)
38-
.await
39-
.unwrap(),
40-
)
34+
let reader: Arc<dyn AsyncFileReader> = if let Some(prefetch) = prefetch {
35+
Arc::new(PrefetchReader::new(reader, prefetch).await.unwrap())
4136
} else {
42-
Box::new(reader)
37+
reader
4338
};
4439
Ok(PyTIFF {
45-
tiff: TIFF::try_open(reader).await.unwrap(),
46-
reader: Arc::new(object_reader),
40+
tiff: TIFF::try_open(reader.clone()).await.unwrap(),
41+
reader,
4742
})
4843
})?;
4944
Ok(cog_reader)

python/uv.lock

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cog.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::Arc;
2+
13
use crate::error::AsyncTiffResult;
24
use crate::ifd::ImageFileDirectories;
35
use crate::reader::{AsyncCursor, AsyncFileReader};
@@ -13,7 +15,7 @@ impl TIFF {
1315
/// Open a new TIFF file.
1416
///
1517
/// This will read all the Image File Directories (IFDs) in the file.
16-
pub async fn try_open(reader: Box<dyn AsyncFileReader>) -> AsyncTiffResult<Self> {
18+
pub async fn try_open(reader: Arc<dyn AsyncFileReader>) -> AsyncTiffResult<Self> {
1719
let mut cursor = AsyncCursor::try_open_tiff(reader).await?;
1820
let version = cursor.read_u16().await?;
1921

@@ -72,13 +74,13 @@ mod test {
7274
let folder = "/Users/kyle/github/developmentseed/async-tiff/";
7375
let path = object_store::path::Path::parse("m_4007307_sw_18_060_20220803.tif").unwrap();
7476
let store = Arc::new(LocalFileSystem::new_with_prefix(folder).unwrap());
75-
let reader = ObjectReader::new(store, path);
77+
let reader = Arc::new(ObjectReader::new(store, path));
7678

77-
let cog_reader = TIFF::try_open(Box::new(reader.clone())).await.unwrap();
79+
let cog_reader = TIFF::try_open(reader.clone()).await.unwrap();
7880

7981
let ifd = &cog_reader.ifds.as_ref()[1];
8082
let decoder_registry = DecoderRegistry::default();
81-
let tile = ifd.fetch_tile(0, 0, &reader).await.unwrap();
83+
let tile = ifd.fetch_tile(0, 0, reader.as_ref()).await.unwrap();
8284
let tile = tile.decode(&decoder_registry).unwrap();
8385
std::fs::write("img.buf", tile).unwrap();
8486
}

src/error.rs

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ pub enum AsyncTiffError {
3434
/// Reqwest error
3535
#[error(transparent)]
3636
ReqwestError(#[from] reqwest::Error),
37+
38+
/// External error
39+
#[error(transparent)]
40+
External(Box<dyn std::error::Error + Send + Sync>),
3741
}
3842

3943
/// Crate-specific result type.

src/reader.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,13 @@ impl AsyncFileReader for ReqwestReader {
165165
/// An AsyncFileReader that caches the first `prefetch` bytes of a file.
166166
#[derive(Debug)]
167167
pub struct PrefetchReader {
168-
reader: Box<dyn AsyncFileReader>,
168+
reader: Arc<dyn AsyncFileReader>,
169169
buffer: Bytes,
170170
}
171171

172172
impl PrefetchReader {
173173
/// Construct a new PrefetchReader, catching the first `prefetch` bytes of the file.
174-
pub async fn new(reader: Box<dyn AsyncFileReader>, prefetch: u64) -> AsyncTiffResult<Self> {
174+
pub async fn new(reader: Arc<dyn AsyncFileReader>, prefetch: u64) -> AsyncTiffResult<Self> {
175175
let buffer = reader.get_bytes(0..prefetch).await?;
176176
Ok(Self { reader, buffer })
177177
}
@@ -213,14 +213,14 @@ pub(crate) enum Endianness {
213213
// TODO: in the future add buffering to this
214214
#[derive(Debug)]
215215
pub(crate) struct AsyncCursor {
216-
reader: Box<dyn AsyncFileReader>,
216+
reader: Arc<dyn AsyncFileReader>,
217217
offset: u64,
218218
endianness: Endianness,
219219
}
220220

221221
impl AsyncCursor {
222222
/// Create a new AsyncCursor from a reader and endianness.
223-
pub(crate) fn new(reader: Box<dyn AsyncFileReader>, endianness: Endianness) -> Self {
223+
pub(crate) fn new(reader: Arc<dyn AsyncFileReader>, endianness: Endianness) -> Self {
224224
Self {
225225
reader,
226226
offset: 0,
@@ -230,7 +230,7 @@ impl AsyncCursor {
230230

231231
/// Create a new AsyncCursor for a TIFF file, automatically inferring endianness from the first
232232
/// two bytes.
233-
pub(crate) async fn try_open_tiff(reader: Box<dyn AsyncFileReader>) -> AsyncTiffResult<Self> {
233+
pub(crate) async fn try_open_tiff(reader: Arc<dyn AsyncFileReader>) -> AsyncTiffResult<Self> {
234234
// Initialize with little endianness and then set later
235235
let mut cursor = Self::new(reader, Endianness::LittleEndian);
236236
let magic_bytes = cursor.read(2).await?;
@@ -252,7 +252,7 @@ impl AsyncCursor {
252252

253253
/// Consume self and return the underlying [`AsyncFileReader`].
254254
#[allow(dead_code)]
255-
pub(crate) fn into_inner(self) -> Box<dyn AsyncFileReader> {
255+
pub(crate) fn into_inner(self) -> Arc<dyn AsyncFileReader> {
256256
self.reader
257257
}
258258

@@ -316,7 +316,7 @@ impl AsyncCursor {
316316
}
317317

318318
#[allow(dead_code)]
319-
pub(crate) fn reader(&self) -> &dyn AsyncFileReader {
319+
pub(crate) fn reader(&self) -> &Arc<dyn AsyncFileReader> {
320320
&self.reader
321321
}
322322

tests/image_tiff/util.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ const TEST_IMAGE_DIR: &str = "tests/image_tiff/images/";
1010
pub(crate) async fn open_tiff(filename: &str) -> TIFF {
1111
let store = Arc::new(LocalFileSystem::new_with_prefix(current_dir().unwrap()).unwrap());
1212
let path = format!("{TEST_IMAGE_DIR}/{filename}");
13-
let reader = Box::new(ObjectReader::new(store.clone(), path.as_str().into()));
13+
let reader = Arc::new(ObjectReader::new(store.clone(), path.as_str().into()));
1414
TIFF::try_open(reader).await.unwrap()
1515
}

0 commit comments

Comments
 (0)