Skip to content

Commit 73ba3f1

Browse files
committed
add api/v1 route
add page size add more tests
1 parent 8c3f67d commit 73ba3f1

File tree

8 files changed

+184
-76
lines changed

8 files changed

+184
-76
lines changed

app/const.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
PAGE_SIZE = 100
2-
BASE_URL = "http://localhost:8000"
1+
DEFAULT_PAGE_SIZE = 100
2+
API_V1_PREFIX = "/api/v1"

app/db.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import duckdb
22
import logging
33

4-
logger = logging.getLogger("fastapi")
4+
logger = logging.getLogger()
55

66

77
POPULATED_DB: duckdb.DuckDBPyConnection | None = None
@@ -27,30 +27,18 @@ def get_db() -> duckdb.DuckDBPyConnection:
2727
)
2828
POPULATED_DB = db
2929

30-
logger.info("Loaded seed data")
31-
32-
logger.info(
30+
logger.warning("Loaded seed data")
31+
logger.warning(
3332
f"Customers: {db.sql('SELECT COUNT(*) FROM customers').fetchone()[0]}"
3433
)
35-
logger.info(db.sql("DESCRIBE customers").fetchall())
36-
37-
logger.info(f"Orders: {db.sql('SELECT COUNT(*) FROM orders').fetchone()[0]}")
38-
logger.info(db.sql("DESCRIBE orders").fetchall())
39-
40-
logger.info(f"Items: {db.sql('SELECT COUNT(*) FROM items').fetchone()[0]}")
41-
logger.info(db.sql("DESCRIBE items").fetchall())
42-
43-
logger.info(
34+
logger.warning(f"Orders: {db.sql('SELECT COUNT(*) FROM orders').fetchone()[0]}")
35+
logger.warning(f"Items: {db.sql('SELECT COUNT(*) FROM items').fetchone()[0]}")
36+
logger.warning(
4437
f"Products: {db.sql('SELECT COUNT(*) FROM products').fetchone()[0]}"
4538
)
46-
logger.info(db.sql("DESCRIBE products").fetchall())
47-
48-
logger.info(f"Stores: {db.sql('SELECT COUNT(*) FROM stores').fetchone()[0]}")
49-
logger.info(db.sql("DESCRIBE stores").fetchall())
50-
51-
logger.info(
39+
logger.warning(f"Stores: {db.sql('SELECT COUNT(*) FROM stores').fetchone()[0]}")
40+
logger.warning(
5241
f"Supplies: {db.sql('SELECT COUNT(*) FROM supplies').fetchone()[0]}"
5342
)
54-
logger.info(db.sql("DESCRIBE supplies").fetchall())
5543

5644
return POPULATED_DB

app/main.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
from fastapi import FastAPI
22

33
from app import routers
4+
from app.const import API_V1_PREFIX
45

56
app = FastAPI()
67

7-
app.include_router(routers.customers_router)
8-
app.include_router(routers.orders_router)
9-
app.include_router(routers.item_router)
10-
app.include_router(routers.product_router)
11-
app.include_router(routers.supplies_router)
12-
app.include_router(routers.store_router)
8+
# api v1
9+
app.include_router(routers.customers_router, prefix=API_V1_PREFIX)
10+
app.include_router(routers.orders_router, prefix=API_V1_PREFIX)
11+
app.include_router(routers.item_router, prefix=API_V1_PREFIX)
12+
app.include_router(routers.product_router, prefix=API_V1_PREFIX)
13+
app.include_router(routers.supplies_router, prefix=API_V1_PREFIX)
14+
app.include_router(routers.store_router, prefix=API_V1_PREFIX)
1315

1416

1517
@app.get("/")

app/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
TODO: add pydantic models for jaffle shop
3+
"""
4+
5+
from pydantic import BaseModel
6+
7+
8+
class Customer(BaseModel):
9+
id: str
10+
name: str

app/routers.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
"""
2-
All jaffle shop routers
2+
All jaffle shop routers for api v1
33
"""
44

5+
from typing import List
6+
57
import duckdb
68
from urllib.parse import urlencode
79
from fastapi import APIRouter, Depends, Response, Request
810

911
from app.db import get_db
10-
from app.const import PAGE_SIZE
12+
from app.const import DEFAULT_PAGE_SIZE, API_V1_PREFIX
13+
from app.models import Customer
1114

12-
# defined routers
1315
customers_router = APIRouter(tags=["customers"])
1416
orders_router = APIRouter(tags=["orders"])
1517
item_router = APIRouter(tags=["items"])
@@ -35,14 +37,15 @@ def _get_paged_response(
3537
response: Response = None,
3638
where_clause: str = "",
3739
sort_by: str = "",
40+
page_size: int = DEFAULT_PAGE_SIZE,
3841
):
3942
"""
4043
Get a paged response from a collection endpoint
4144
Will insert next header if applicable
4245
"""
4346

44-
offset = (page - 1) * PAGE_SIZE
45-
last_item = offset + PAGE_SIZE
47+
offset = (page - 1) * page_size
48+
last_item = offset + page_size
4649
count = db.query(f"SELECT COUNT(*) FROM {table_name} {where_clause}").fetchone()[0]
4750

4851
# get base url from request
@@ -51,7 +54,7 @@ def _get_paged_response(
5154
# )
5255
# scheme = request.headers.get("X-Forwarded-Proto", "http")
5356

54-
url = f"/{table_name}"
57+
url = API_V1_PREFIX + f"/{table_name}"
5558
query_params = dict(request.query_params)
5659
if last_item < count and response:
5760
query_params["page"] = page + 1
@@ -60,7 +63,7 @@ def _get_paged_response(
6063

6164
return _get_list_response(
6265
db,
63-
f"SELECT * FROM {table_name} {where_clause} {sort_by} LIMIT {PAGE_SIZE} OFFSET {offset}",
66+
f"SELECT * FROM {table_name} {where_clause} {sort_by} LIMIT {page_size} OFFSET {offset}",
6467
)
6568

6669

@@ -78,17 +81,25 @@ def _get_single_response(db: duckdb.DuckDBPyConnection, query: str):
7881
#
7982

8083

81-
@customers_router.get("/customers")
84+
@customers_router.get("/customers", response_model=List[Customer])
8285
async def get_customers(
8386
page: int = 1,
87+
page_size: int = DEFAULT_PAGE_SIZE,
8488
response: Response = None,
8589
request: Request = None,
8690
db: duckdb.DuckDBPyConnection = Depends(get_db),
8791
):
88-
return _get_paged_response(db, request, "customers", page, response)
92+
return _get_paged_response(
93+
db=db,
94+
request=request,
95+
table_name="customers",
96+
page=page,
97+
response=response,
98+
page_size=page_size,
99+
)
89100

90101

91-
@customers_router.get("/customers/{customer_id}")
102+
@customers_router.get("/customers/{customer_id}", response_model=Customer)
92103
async def get_customer(
93104
customer_id: str, db: duckdb.DuckDBPyConnection = Depends(get_db)
94105
):
@@ -116,6 +127,7 @@ def _enrich_orders(db: duckdb.DuckDBPyConnection, orders: list[dict]):
116127
@orders_router.get("/orders")
117128
async def get_orders(
118129
page: int = 1,
130+
page_size: int = DEFAULT_PAGE_SIZE,
119131
start_date: str = None,
120132
end_date: str = None,
121133
response: Response = None,
@@ -124,14 +136,19 @@ async def get_orders(
124136
):
125137
where_clause = ""
126138
if start_date:
127-
where_clause += f" AND ordered_at >= '{start_date}'"
139+
where_clause += f" AND ordered_at::DATE >= '{start_date}'"
128140
if end_date:
129-
where_clause += f" AND ordered_at <= '{end_date}'"
141+
where_clause += f" AND ordered_at::DATE <= '{end_date}'"
130142
if where_clause:
131143
where_clause = f"WHERE {where_clause[5:]}"
132-
sort_by = "ORDER BY ordered_at ASC"
133144
orders = _get_paged_response(
134-
db, request, "orders", page, response, where_clause, sort_by
145+
db=db,
146+
request=request,
147+
table_name="orders",
148+
page=page,
149+
page_size=page_size,
150+
response=response,
151+
where_clause=where_clause,
135152
)
136153
return _enrich_orders(db, orders)
137154

@@ -147,11 +164,19 @@ async def get_order(order_id: str, db: duckdb.DuckDBPyConnection = Depends(get_d
147164
@item_router.get("/items")
148165
async def get_items(
149166
page: int = 1,
167+
page_size: int = DEFAULT_PAGE_SIZE,
150168
response: Response = None,
151169
request: Request = None,
152170
db: duckdb.DuckDBPyConnection = Depends(get_db),
153171
):
154-
return _get_paged_response(db, request, "items", page, response)
172+
return _get_paged_response(
173+
db=db,
174+
request=request,
175+
table_name="items",
176+
page=page,
177+
response=response,
178+
page_size=page_size,
179+
)
155180

156181

157182
@item_router.get("/items/{item_id}")
@@ -165,11 +190,19 @@ async def get_item(item_id: str, db: duckdb.DuckDBPyConnection = Depends(get_db)
165190
@product_router.get("/products")
166191
async def get_products(
167192
page: int = 1,
193+
page_size: int = DEFAULT_PAGE_SIZE,
168194
response: Response = None,
169195
request: Request = None,
170196
db: duckdb.DuckDBPyConnection = Depends(get_db),
171197
):
172-
return _get_paged_response(db, request, "products", page, response)
198+
return _get_paged_response(
199+
db=db,
200+
request=request,
201+
table_name="products",
202+
page=page,
203+
response=response,
204+
page_size=page_size,
205+
)
173206

174207

175208
@product_router.get("/products/{sku}")
@@ -183,11 +216,19 @@ async def get_product(sku: str, db: duckdb.DuckDBPyConnection = Depends(get_db))
183216
@store_router.get("/stores")
184217
async def get_stores(
185218
page: int = 1,
219+
page_size: int = DEFAULT_PAGE_SIZE,
186220
response: Response = None,
187221
request: Request = None,
188222
db: duckdb.DuckDBPyConnection = Depends(get_db),
189223
):
190-
return _get_paged_response(db, request, "stores", page, response)
224+
return _get_paged_response(
225+
db=db,
226+
request=request,
227+
table_name="stores",
228+
page=page,
229+
response=response,
230+
page_size=page_size,
231+
)
191232

192233

193234
@store_router.get("/stores/{store_id}")
@@ -201,11 +242,19 @@ async def get_store(store_id: str, db: duckdb.DuckDBPyConnection = Depends(get_d
201242
@supplies_router.get("/supplies")
202243
async def get_supplies(
203244
page: int = 1,
245+
page_size: int = DEFAULT_PAGE_SIZE,
204246
response: Response = None,
205247
request: Request = None,
206248
db: duckdb.DuckDBPyConnection = Depends(get_db),
207249
):
208-
return _get_paged_response(db, request, "supplies", page, response)
250+
return _get_paged_response(
251+
db=db,
252+
request=request,
253+
table_name="supplies",
254+
page=page,
255+
response=response,
256+
page_size=page_size,
257+
)
209258

210259

211260
@supplies_router.get("/supplies/{supply_id}")

tests/test_dlt_extraction.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from dlt.sources.rest_api import rest_api_source
1616
from dlt.common.destination.dataset import SupportsReadableDataset
1717

18+
from tests.utils import EXPECTED_TABLES_COUNTS_JANUARY_2017
1819

1920
app = TestClient(app)
2021

@@ -44,7 +45,7 @@ def test_extract_full_dataset():
4445
source = rest_api_source(
4546
{
4647
"client": {
47-
"base_url": "http://localhost:8000",
48+
"base_url": "http://localhost:8000/api/v1",
4849
"paginator": {
4950
"type": "header_link",
5051
},
@@ -62,6 +63,7 @@ def test_extract_full_dataset():
6263
"endpoint": {
6364
"path": "orders",
6465
"params": {
66+
"page_size": 1000,
6567
"start_date": "2017-01-01",
6668
"end_date": "2017-01-31",
6769
},
@@ -79,14 +81,7 @@ def test_extract_full_dataset():
7981
)
8082

8183
pipeline.run(source)
82-
assert row_counts(pipeline.dataset()) == {
83-
"customers": 935,
84-
"products": 10,
85-
"stores": 6,
86-
"supplies": 65,
87-
"orders": 3303, # 3303 orders in january 2017
88-
"orders__items": 5072,
89-
}
84+
assert row_counts(pipeline.dataset()) == EXPECTED_TABLES_COUNTS_JANUARY_2017
9085

9186

9287
@pytest.mark.skip(reason="This is a live test against the deployed shop")

0 commit comments

Comments
 (0)