Skip to content
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
4 changes: 2 additions & 2 deletions DEVELOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ Install [uv](https://docs.astral.sh/uv/) and [Rust](https://www.rust-lang.org/to
From the top-level directory, run

```
uv run maturin develop -m obstore/Cargo.toml
uv run maturin dev -m obstore/Cargo.toml
```

this will compile `obstore` and add it to the uv-managed Python environment.

If you wish to do any benchmarking, run

```
uv run maturin develop -m obstore/Cargo.toml --release
uv run maturin dev -m obstore/Cargo.toml --release
```

to compile `obstore` with release optimizations turned on.
Expand Down
88 changes: 88 additions & 0 deletions docs/examples/stream-zip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Streaming ZIP file creation

This example demonstrates how to create a zip archive from files in one store and upload it to another store using the [`stream_zip`](https://github.com/uktrade/stream-zip) library.

This never stores any entire source file or the target zip file in memory, so you can zip large files with low memory overhead.

## Example

!!! note

This example is also [available on Github](https://github.com/developmentseed/obstore/blob/main/examples/stream-zip/README.md) if you'd like to test it out locally.

```py
from __future__ import annotations

import asyncio
from pathlib import Path
from stat import S_IFREG
from typing import TYPE_CHECKING

import stream_zip
from stream_zip import ZIP_32, AsyncMemberFile

from obstore.store import LocalStore, MemoryStore

if TYPE_CHECKING:
from collections.abc import AsyncIterable, Iterable

from obstore.store import ObjectStore


async def member_file(store: ObjectStore, path: str) -> AsyncMemberFile:
"""Create a member file for the zip archive."""
resp = await store.get_async(path)
last_modified = resp.meta["last_modified"]
mode = S_IFREG | 0o644
# Unclear why but we need to wrap the response in an async generator
return (path, last_modified, mode, ZIP_32, (byte async for byte in resp.stream()))


async def member_files(
store: ObjectStore,
paths: Iterable[str],
) -> AsyncIterable[AsyncMemberFile]:
"""Create an async iterable of files for the zip archive."""
for path in paths:
yield await member_file(store, path)


async def zip_copy() -> None:
"""Copy files from one store into a zip archive that we upload to another store."""
# Input store with source data
input_store = MemoryStore()
input_store.put("foo", b"hello")
input_store.put("bar", b"world")

# Output store where the zip file will be saved
output_store = LocalStore(Path())

# We can pass the streaming zip directly to `put`
await output_store.put_async(
"my.zip",
stream_zip.async_stream_zip(
member_files(input_store, ["foo", "bar"]),
chunk_size=10 * 1024 * 1024,
),
)
```

This creates a zip file in the current directory:

```
> unzip -l my.zip
Archive: my.zip
Length Date Time Name
--------- ---------- ----- ----
5 05-22-2025 13:37 foo
5 05-22-2025 13:37 bar
--------- -------
10 2 files
```

And we can read a file:

```
> unzip -p my.zip foo
hello
```
1 change: 1 addition & 0 deletions examples/stream-zip/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.zip
1 change: 1 addition & 0 deletions examples/stream-zip/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
5 changes: 5 additions & 0 deletions examples/stream-zip/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Obstore stream-zip example

This example demonstrates how to create a zip archive from files in one store and upload it to another store using the [`stream_zip`](https://github.com/uktrade/stream-zip) library.

This never stores any entire source file or the target zip file in memory, so you can zip large files with low memory overhead.
65 changes: 65 additions & 0 deletions examples/stream-zip/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Example for using stream-zip with obstore."""

from __future__ import annotations

import asyncio
from pathlib import Path
from stat import S_IFREG
from typing import TYPE_CHECKING

import stream_zip
from stream_zip import ZIP_32, AsyncMemberFile

from obstore.store import LocalStore, MemoryStore

if TYPE_CHECKING:
from collections.abc import AsyncIterable, Iterable

from obstore.store import ObjectStore


async def member_file(store: ObjectStore, path: str) -> AsyncMemberFile:
"""Create a member file for the zip archive."""
resp = await store.get_async(path)
last_modified = resp.meta["last_modified"]
mode = S_IFREG | 0o644
# Unclear why but we need to wrap the response in an async generator
return (path, last_modified, mode, ZIP_32, (byte async for byte in resp.stream()))


async def member_files(
store: ObjectStore,
paths: Iterable[str],
) -> AsyncIterable[AsyncMemberFile]:
"""Create an async iterable of files for the zip archive."""
for path in paths:
yield await member_file(store, path)


async def zip_copy() -> None:
"""Copy files from one store into a zip archive that we upload to another store."""
# Input store with source data
input_store = MemoryStore()
input_store.put("foo", b"hello")
input_store.put("bar", b"world")

# Output store where the zip file will be saved
output_store = LocalStore(Path())

# We can pass the streaming zip directly to `put`
await output_store.put_async(
"my.zip",
stream_zip.async_stream_zip(
member_files(input_store, ["foo", "bar"]),
chunk_size=10 * 1024 * 1024,
),
)


def main() -> None:
"""Run the zip copy example."""
asyncio.run(zip_copy())


if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions examples/stream-zip/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
name = "stream-zip-example"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = ["obstore>=0.6", "stream-zip>=0.0.83"]

[dependency-groups]
dev = ["ipykernel>=6.29.5"]
Loading
Loading