Skip to content

Commit f85e43a

Browse files
authored
Merge pull request #31 from DoyleDev/main
Lakebase FastApi Implementation
2 parents ce9372d + 11e5ddb commit f85e43a

File tree

19 files changed

+2421
-16
lines changed

19 files changed

+2421
-16
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ share/python-wheels/
2626
.installed.cfg
2727
*.egg
2828
MANIFEST
29+
uv.lock
30+
pyproject.toml
2931

3032
# PyInstaller
3133
# Usually these files are written by a python script from a template
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"label": "Lakebase",
3+
"position": 3,
4+
"link": {
5+
"type": "generated-index",
6+
"description": "Learn how to build FastAPI applications with Lakebase PostgreSQL databases, including resource management and data operations."
7+
}
8+
}
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
---
2+
sidebar_position: 3
3+
---
4+
5+
# Interact with Lakebase Tables
6+
7+
This recipe demonstrates how to build a complete orders management API using Lakebase PostgreSQL database. These endpoints provide CRUD operations and various query patterns for handling orders data synchronized from Databricks Unity Catalog.
8+
9+
:::info Prerequisites
10+
- Lakebase resources must be created using the [Create Lakebase Resources](./lakebase_resources_create.mdx) endpoint
11+
- The synced table pipeline must be completed and orders data synchronized from `samples.tpch.orders`
12+
- Database connection must be configured in your application environment
13+
:::
14+
15+
:::info
16+
In this example, we demonstrate multiple HTTP methods (GET, POST) which are the standard choices for data operations in REST APIs as defined in RFC 7231:
17+
- `GET` requests are idempotent and cacheable, ideal for data retrieval
18+
- `POST` requests for updates and modifications
19+
20+
For detailed specifications, refer to [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231) which defines HTTP method semantics.
21+
:::
22+
23+
## Code snippet
24+
25+
```python title="routes/v1/orders.py"
26+
import logging
27+
28+
from config.database import get_async_db
29+
from models.orders import (
30+
CursorPaginationInfo,
31+
Order,
32+
OrderCount,
33+
OrderListCursorResponse,
34+
OrderListResponse,
35+
OrderRead,
36+
OrderSample,
37+
OrderStatusUpdate,
38+
OrderStatusUpdateResponse,
39+
PaginationInfo,
40+
)
41+
from sqlalchemy import func, select
42+
from sqlalchemy.ext.asyncio import AsyncSession
43+
44+
from fastapi import APIRouter, Depends, HTTPException, Query
45+
46+
logger = logging.getLogger(__name__)
47+
router = APIRouter(tags=["orders"])
48+
49+
# 1. GET ORDERS COUNT
50+
@router.get("/count", response_model=OrderCount, summary="Get total order count")
51+
async def get_order_count(db: AsyncSession = Depends(get_async_db)):
52+
try:
53+
stmt = select(func.count(Order.o_orderkey))
54+
result = await db.execute(stmt)
55+
count = result.scalar()
56+
return OrderCount(total_orders=count)
57+
except Exception as e:
58+
logger.error(f"Error getting order count: {e}")
59+
raise HTTPException(status_code=500, detail="Failed to retrieve order count")
60+
61+
# 2. GET SAMPLE ORDERS
62+
@router.get("/sample", response_model=OrderSample, summary="Get 5 random order keys")
63+
async def get_sample_orders(db: AsyncSession = Depends(get_async_db)):
64+
try:
65+
stmt = select(Order.o_orderkey).limit(5)
66+
result = await db.execute(stmt)
67+
order_keys = result.scalars().all()
68+
return OrderSample(sample_order_keys=order_keys)
69+
except Exception as e:
70+
logger.error(f"Error getting sample orders: {e}")
71+
raise HTTPException(status_code=500, detail="Failed to retrieve sample orders")
72+
73+
# 3. PAGE-BASED PAGINATION
74+
@router.get("/pages", response_model=OrderListResponse, summary="Get orders with page-based pagination")
75+
async def get_orders_by_page(
76+
page: int = Query(1, ge=1, description="Page number (1-based)"),
77+
page_size: int = Query(100, ge=1, le=1000, description="Number of records per page (max 1000)"),
78+
include_count: bool = Query(True, description="Include total count for pagination info"),
79+
db: AsyncSession = Depends(get_async_db),
80+
):
81+
try:
82+
if include_count:
83+
count_stmt = select(func.count(Order.o_orderkey))
84+
count_result = await db.execute(count_stmt)
85+
total_count = count_result.scalar()
86+
total_pages = (total_count + page_size - 1) // page_size
87+
else:
88+
total_count = -1
89+
total_pages = -1
90+
91+
offset = (page - 1) * page_size
92+
stmt = (
93+
select(Order)
94+
.order_by(Order.o_orderkey)
95+
.offset(offset)
96+
.limit(page_size + 1)
97+
)
98+
99+
result = await db.execute(stmt)
100+
all_orders = result.scalars().all()
101+
102+
has_next = len(all_orders) > page_size
103+
orders = all_orders[:page_size]
104+
has_previous = page > 1
105+
106+
pagination_info = PaginationInfo(
107+
page=page,
108+
page_size=page_size,
109+
total_pages=total_pages,
110+
total_count=total_count,
111+
has_next=has_next,
112+
has_previous=has_previous,
113+
)
114+
115+
return OrderListResponse(orders=orders, pagination=pagination_info)
116+
117+
except Exception as e:
118+
logger.error(f"Error getting page-based orders: {e}")
119+
raise HTTPException(status_code=500, detail="Failed to retrieve orders")
120+
121+
# 4. CURSOR-BASED PAGINATION
122+
@router.get("/stream", response_model=OrderListCursorResponse, summary="Get orders with cursor-based pagination")
123+
async def get_orders_by_cursor(
124+
cursor: int = Query(0, ge=0, description="Start after this order key (0 for beginning)"),
125+
page_size: int = Query(100, ge=1, le=1000, description="Number of records to fetch (max 1000)"),
126+
db: AsyncSession = Depends(get_async_db),
127+
):
128+
try:
129+
stmt = (
130+
select(Order)
131+
.where(Order.o_orderkey > cursor)
132+
.order_by(Order.o_orderkey)
133+
.limit(page_size + 1)
134+
)
135+
136+
result = await db.execute(stmt)
137+
all_orders = result.scalars().all()
138+
139+
has_next = len(all_orders) > page_size
140+
orders = all_orders[:page_size]
141+
has_previous = cursor > 0
142+
143+
next_cursor = orders[-1].o_orderkey if orders and has_next else None
144+
previous_cursor = max(0, cursor - page_size) if has_previous else None
145+
146+
pagination_info = CursorPaginationInfo(
147+
page_size=page_size,
148+
has_next=has_next,
149+
has_previous=has_previous,
150+
next_cursor=next_cursor,
151+
previous_cursor=previous_cursor,
152+
)
153+
154+
return OrderListCursorResponse(orders=orders, pagination=pagination_info)
155+
156+
except Exception as e:
157+
logger.error(f"Error getting cursor-based orders: {e}")
158+
raise HTTPException(status_code=500, detail="Failed to retrieve orders")
159+
160+
# 5. GET SPECIFIC ORDER
161+
@router.get("/{order_key}", response_model=OrderRead, summary="Get an order by its key")
162+
async def read_order(order_key: int, db: AsyncSession = Depends(get_async_db)):
163+
try:
164+
if order_key <= 0:
165+
raise HTTPException(status_code=400, detail="Invalid order key provided")
166+
167+
stmt = select(Order).where(Order.o_orderkey == order_key)
168+
result = await db.execute(stmt)
169+
order = result.scalars().first()
170+
171+
if not order:
172+
raise HTTPException(status_code=404, detail=f"Order with key '{order_key}' not found")
173+
174+
return order
175+
176+
except HTTPException:
177+
raise
178+
except Exception as e:
179+
logger.error(f"Unexpected error fetching order {order_key}: {e}")
180+
raise HTTPException(status_code=500, detail="Internal server error occurred")
181+
182+
# 6. UPDATE ORDER STATUS
183+
@router.post("/{order_key}/status", response_model=OrderStatusUpdateResponse, summary="Update order status")
184+
async def update_order_status(
185+
order_key: int,
186+
status_data: OrderStatusUpdate,
187+
db: AsyncSession = Depends(get_async_db),
188+
):
189+
try:
190+
if order_key <= 0:
191+
raise HTTPException(status_code=400, detail="Invalid order key provided")
192+
193+
check_stmt = select(Order).where(Order.o_orderkey == order_key)
194+
check_result = await db.execute(check_stmt)
195+
existing_order = check_result.scalars().first()
196+
197+
if not existing_order:
198+
raise HTTPException(status_code=404, detail=f"Order with key '{order_key}' not found")
199+
200+
existing_order.o_orderstatus = status_data.o_orderstatus
201+
await db.commit()
202+
await db.refresh(existing_order)
203+
204+
return OrderStatusUpdateResponse(
205+
o_orderkey=order_key,
206+
o_orderstatus=status_data.o_orderstatus,
207+
message="Order status updated successfully",
208+
)
209+
210+
except HTTPException:
211+
raise
212+
except Exception as e:
213+
logger.error(f"Error updating status for order {order_key}: {e}")
214+
raise HTTPException(status_code=500, detail="Failed to update order status")
215+
```
216+
217+
:::warning
218+
219+
The above example is shortened for brevity and not suitable for production use.
220+
You can find a more advanced sample in the databricks-apps-cookbook GitHub repository.
221+
222+
:::
223+
224+
## Example Usage
225+
226+
The orders API provides six main endpoints for different use cases:
227+
228+
| Endpoint | Method | Purpose | Best For |
229+
|----------|--------|---------|----------|
230+
| `/orders/count` | GET | Get total orders count | Dashboards, monitoring |
231+
| `/orders/sample` | GET | Get 5 sample order keys | Testing, development |
232+
| `/orders/pages` | GET | Page-based pagination | Traditional UIs with page numbers |
233+
| `/orders/stream` | GET | Cursor-based pagination | Large datasets, infinite scroll |
234+
| `/orders/{order_key}` | GET | Get specific order | Order details, lookups |
235+
| `/orders/{order_key}/status` | POST | Update order status | Order processing workflows |
236+
237+
### Get Orders Count
238+
239+
```bash
240+
curl -X GET "http://localhost:8000/api/v1/orders/count"
241+
```
242+
243+
```json
244+
{
245+
"total_orders": 1500000
246+
}
247+
```
248+
249+
### Get Sample Orders
250+
251+
```bash
252+
curl -X GET "http://localhost:8000/api/v1/orders/sample"
253+
```
254+
255+
```json
256+
{
257+
"sample_order_keys": [1, 32, 33, 34, 35]
258+
}
259+
```
260+
261+
### Page-Based Pagination
262+
263+
```bash
264+
curl -X GET "http://localhost:8000/api/v1/orders/pages?page=1&page_size=2"
265+
```
266+
267+
```json
268+
{
269+
"orders": [
270+
{
271+
"o_orderkey": 1,
272+
"o_custkey": 370,
273+
"o_orderstatus": "O",
274+
"o_totalprice": 172799.49,
275+
"o_orderdate": "1996-01-02",
276+
"o_orderpriority": "5-LOW",
277+
"o_clerk": "Clerk#000000951",
278+
"o_shippriority": 0,
279+
"o_comment": "nstructions sleep furiously among"
280+
},
281+
{
282+
"o_orderkey": 2,
283+
"o_custkey": 781,
284+
"o_orderstatus": "O",
285+
"o_totalprice": 46929.18,
286+
"o_orderdate": "1996-12-01",
287+
"o_orderpriority": "1-URGENT",
288+
"o_clerk": "Clerk#000000880",
289+
"o_shippriority": 0,
290+
"o_comment": "foxes. pending accounts at the pending"
291+
}
292+
],
293+
"pagination": {
294+
"page": 1,
295+
"page_size": 2,
296+
"total_pages": 750000,
297+
"total_count": 1500000,
298+
"has_next": true,
299+
"has_previous": false
300+
}
301+
}
302+
```
303+
304+
### Update Order Status
305+
306+
```bash
307+
curl -X POST "http://localhost:8000/api/v1/orders/1/status" \
308+
-H "Content-Type: application/json" \
309+
-d '{"o_orderstatus": "F"}'
310+
```
311+
312+
```json
313+
{
314+
"o_orderkey": 1,
315+
"o_orderstatus": "F",
316+
"message": "Order status updated successfully"
317+
}
318+
```
319+
320+
## Resources
321+
322+
- [Lakebase PostgreSQL](https://docs.databricks.com/en/database/index.html)
323+
- [SQLAlchemy Async](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html)
324+
- [FastAPI Dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/)
325+
326+
## Permissions
327+
328+
Your [app service principal](https://docs.databricks.com/aws/en/dev-tools/databricks-apps/#how-does-databricks-apps-manage-authorization) needs the following permissions:
329+
330+
- Database instance connection access via OAuth tokens
331+
- `SELECT` permissions on the `orders_synced` table in your Lakebase database
332+
- `UPDATE` permissions on the `orders_synced` table for status updates
333+
- Database user role with appropriate table access
334+
335+
See [Lakebase permissions](https://docs.databricks.com/en/database/permissions.html) for more information.
336+
337+
## Dependencies
338+
339+
- [Databricks SDK for Python](https://pypi.org/project/databricks-sdk/) - `databricks-sdk`
340+
- [FastAPI](https://pypi.org/project/fastapi/) - `fastapi`
341+
- [SQLAlchemy](https://pypi.org/project/sqlalchemy/) - `sqlalchemy`
342+
- [SQLModel](https://pypi.org/project/sqlmodel/) - `sqlmodel`
343+
- [asyncpg](https://pypi.org/project/asyncpg/) - `asyncpg`
344+
- [uvicorn](https://pypi.org/project/uvicorn/) - `uvicorn`
345+
346+
```python title="requirements.txt"
347+
databricks-sdk
348+
fastapi
349+
sqlalchemy
350+
sqlmodel
351+
asyncpg
352+
uvicorn
353+
```

0 commit comments

Comments
 (0)