Skip to content

Commit

Permalink
FastAPI/Starlette example (#145)
Browse files Browse the repository at this point in the history
* FastAPI example

* Add FastAPI example to docs
  • Loading branch information
kylebarron authored Jan 15, 2025
1 parent 1a32b00 commit 36f876a
Show file tree
Hide file tree
Showing 8 changed files with 558 additions and 1 deletion.
65 changes: 65 additions & 0 deletions docs/examples/fastapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# FastAPI

[FastAPI](https://fastapi.tiangolo.com/) is a modern, high-performance, web framework for building APIs with Python based on standard Python type hints.

It's easy to integrate obstore with FastAPI routes, where you want to download a file from an object store and return it to the user.

FastAPI has a [`StreamingResponse`](https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse), which neatly integrates with [`BytesStream`][obstore.BytesStream] to stream the response to the user.

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

First, import `fastapi` and `obstore` and create the FastAPI application.

```py
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

import obstore as obs
from obstore.store import HTTPStore, S3Store

app = FastAPI()
```

Next, we can add our route. Here, we create a simple route that fetches a small
Parquet file from an HTTP url and returns it to the user.

Passing `resp` directly to `StreamingResponse` calls
[`GetResult.stream()`][obstore.GetResult.stream] under the hood and thus uses
the default chunking behavior of `GetResult.stream()`.

```py
@app.get("/example.parquet")
async def download_example() -> StreamingResponse:
store = HTTPStore.from_url("https://raw.githubusercontent.com")
path = "opengeospatial/geoparquet/refs/heads/main/examples/example.parquet"

# Make the request. This only begins the download; it does not wait for the
# download to finish.
resp = await obs.get_async(store, path)
return StreamingResponse(resp)
```

You may also want to customize the chunking behavior of the async stream. To do
this, call [`GetResult.stream()`][obstore.GetResult.stream] before passing to
`StreamingResponse`.

```py
@app.get("/large.parquet")
async def large_example() -> StreamingResponse:
# Example large Parquet file hosted in AWS open data
store = S3Store("ookla-open-data", region="us-west-2", skip_signature=True)
path = "parquet/performance/type=fixed/year=2024/quarter=1/2024-01-01_performance_fixed_tiles.parquet"

# Note: for large file downloads you may need to increase the timeout in
# the client configuration
resp = await obs.get_async(store, path)

# Example: Ensure the stream returns at least 5MB of data in each chunk.
return StreamingResponse(resp.stream(min_chunk_size=5 * 1024 * 1024))
```

Note that here FastAPI wraps
[`starlette.responses.StreamingResponse`](https://www.starlette.io/responses/#streamingresponse).
So any web server that uses [Starlette](https://www.starlette.io/) for responses
can use this same code.
1 change: 1 addition & 0 deletions examples/fastapi-example/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
10 changes: 10 additions & 0 deletions examples/fastapi-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# obstore FastAPI example

Example returning a streaming response via FastAPI.

```
uv run fastapi dev main.py
```

Note that here FastAPI wraps `starlette.responses`. So any web server that uses
Starlette for responses can use this same code.
42 changes: 42 additions & 0 deletions examples/fastapi-example/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

import obstore as obs
from obstore.store import HTTPStore, S3Store

app = FastAPI()


@app.get("/")
def read_root():
return {"Hello": "World"}


@app.get("/example.parquet")
async def download_example() -> StreamingResponse:
store = HTTPStore.from_url("https://raw.githubusercontent.com")
path = "opengeospatial/geoparquet/refs/heads/main/examples/example.parquet"

# Make the request. This only begins the download; it does not wait for the download
# to finish.
resp = await obs.get_async(store, path)

# Passing `GetResult` directly to `StreamingResponse` calls `GetResult.stream()`
# under the hood and thus uses the default chunking behavior of
# `GetResult.stream()`.
return StreamingResponse(resp)


@app.get("/large.parquet")
async def large_example() -> StreamingResponse:
# Example large Parquet file hosted in AWS open data
store = S3Store("ookla-open-data", region="us-west-2", skip_signature=True)
path = "parquet/performance/type=fixed/year=2024/quarter=1/2024-01-01_performance_fixed_tiles.parquet"

# Make the request
# Note: for large file downloads you may need to increase the timeout in the client
# configuration
resp = await obs.get_async(store, path)

# Example: Ensure the stream returns at least 10MB of data in each chunk.
return StreamingResponse(resp.stream(min_chunk_size=10 * 1024 * 1024))
11 changes: 11 additions & 0 deletions examples/fastapi-example/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[project]
name = "fastapi-example"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = ["fastapi[standard]>=0.115.6", "obstore"]

# TODO: remove this; use published obstore
[tool.uv.sources]
obstore = { path = "../../obstore" }
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ extra:

nav:
- "index.md"
- Examples:
- examples/fastapi.md
- API Reference:
- obstore.store:
- api/store/index.md
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ dev-dependencies = [
"pyarrow>=17.0.0",
"pytest-asyncio>=0.24.0",
"pytest>=8.3.3",
"ruff>=0.8.4"
"ruff>=0.8.4",
]

[tool.uv.workspace]
members = ["examples/fastapi-example"]

[tool.ruff]
select = [
# Pyflakes
Expand Down
Loading

0 comments on commit 36f876a

Please sign in to comment.