Skip to content
This repository was archived by the owner on Apr 2, 2025. It is now read-only.

Commit 690e44f

Browse files
feat: refactoring get_products pagination based on feedback, refactoring get_orders pagination based on feedback, feat: adding pagination for
get_order_statuses feat: updating mock backend for get_order_statuses pagination tests: adding test for get order status pagination tests: adding tests for order_satuses paginatio
1 parent ac661d9 commit 690e44f

File tree

5 files changed

+174
-77
lines changed

5 files changed

+174
-77
lines changed

src/stapi_fastapi/backends/root_backend.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66

77
from stapi_fastapi.models.order import (
88
Order,
9-
OrderCollection,
109
OrderStatus,
1110
)
1211

1312

1413
class RootBackend[T: OrderStatus](Protocol): # pragma: nocover
1514
async def get_orders(
1615
self, request: Request, next: str | None, limit: int
17-
) -> ResultE[tuple[OrderCollection, str]]:
16+
) -> ResultE[tuple[list[Order], str]]:
1817
"""
1918
Return a list of existing orders and pagination token if applicable
2019
No pagination will return empty string for token

src/stapi_fastapi/routers/root_router.py

Lines changed: 81 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -143,51 +143,50 @@ def get_conformance(self, request: Request) -> Conformance:
143143
def get_products(
144144
self, request: Request, next: str | None = None, limit: int = 10
145145
) -> ProductsCollection:
146+
start = 0
147+
product_ids = [*self.product_routers.keys()]
148+
end = min(start + limit, len(product_ids))
146149
try:
147-
start = 0
148-
product_ids = [*self.product_routers.keys()]
149150
if next:
150151
start = product_ids.index(next)
151-
if not product_ids and not next:
152-
ProductsCollection(
153-
products=[],
154-
links=[
155-
Link(
156-
href=str(request.url_for(f"{self.name}:list-products")),
157-
rel="self",
158-
type=TYPE_JSON,
159-
)
160-
],
152+
except ValueError as e:
153+
logging.exception("An error occurred while retrieving orders")
154+
raise NotFoundException(detail="Error finding pagination token") from e
155+
156+
ids = product_ids[start:end]
157+
links = [
158+
Link(
159+
href=str(request.url_for(f"{self.name}:list-products")),
160+
rel="self",
161+
type=TYPE_JSON,
162+
),
163+
]
164+
if end < len(product_ids):
165+
links.append(
166+
Link(
167+
href=str(
168+
request.url.include_query_params(
169+
next=self.product_routers[product_ids[end]].product.id
170+
),
171+
),
172+
rel="next",
173+
type=TYPE_JSON,
161174
)
162-
end = start + limit
163-
ids = product_ids[start:end]
164-
products = [
175+
)
176+
return ProductsCollection(
177+
products=[
165178
self.product_routers[product_id].get_product(request)
166179
for product_id in ids
167-
]
168-
links = [
169-
Link(
170-
href=str(request.url_for(f"{self.name}:list-products")),
171-
rel="self",
172-
type=TYPE_JSON,
173-
),
174-
]
175-
next = ""
176-
if end < len(product_ids):
177-
next = self.product_routers[product_ids[end]].product.id
178-
updated_url = request.url.include_query_params(next=next)
179-
links.append(Link(href=str(updated_url), rel="next", type=TYPE_JSON))
180-
return ProductsCollection(products=products, links=links)
181-
except ValueError as e:
182-
logging.exception(f"An error occurred while retrieving orders: {e}")
183-
raise NotFoundException(detail="Error finding pagination token")
180+
],
181+
links=links,
182+
)
184183

185184
async def get_orders(
186185
self, request: Request, next: str | None = None, limit: int = 10
187186
) -> OrderCollection:
188187
match await self.backend.get_orders(request, next, limit):
189-
case Success((collections, token)):
190-
for order in collections:
188+
case Success((orders, pagination_token)):
189+
for order in orders:
191190
order.links.append(
192191
Link(
193192
href=str(
@@ -199,14 +198,27 @@ async def get_orders(
199198
type=TYPE_JSON,
200199
)
201200
)
202-
if token: # pagination link if backend returns token
203-
updated_url = request.url.include_query_params(next=token)
204-
collections.links.append(
205-
Link(href=str(updated_url), rel="next", type=TYPE_JSON)
201+
if pagination_token:
202+
return OrderCollection(
203+
features=orders,
204+
links=[
205+
Link(
206+
href=str(
207+
request.url.include_query_params(
208+
next=pagination_token
209+
)
210+
),
211+
rel="next",
212+
type=TYPE_JSON,
213+
)
214+
],
206215
)
207-
return collections
216+
return OrderCollection(features=orders)
208217
case Failure(e):
209-
logging.exception(f"An error occurred while retrieving orders: {e}")
218+
logger.error(
219+
"An error occurred while retrieving orders: %s",
220+
traceback.format_exception(e),
221+
)
210222
if isinstance(e, ValueError):
211223
raise NotFoundException(detail="Error finding pagination token")
212224
else:
@@ -248,31 +260,46 @@ async def get_order_statuses(
248260
limit: int = 10,
249261
) -> OrderStatuses:
250262
match await self.backend.get_order_statuses(order_id, request, next, limit):
251-
case Success(statuses):
252-
return OrderStatuses(
253-
statuses=statuses,
254-
links=[
263+
case Success((statuses, pagination_token)):
264+
links = [
265+
Link(
266+
href=str(
267+
request.url_for(
268+
f"{self.name}:list-order-statuses",
269+
order_id=order_id,
270+
)
271+
),
272+
rel="self",
273+
type=TYPE_JSON,
274+
)
275+
]
276+
if pagination_token:
277+
links.append(
255278
Link(
256279
href=str(
257-
request.url_for(
258-
f"{self.name}:list-order-statuses",
259-
order_id=order_id,
260-
)
280+
request.url.include_query_params(next=pagination_token)
261281
),
262-
rel="self",
282+
rel="next",
263283
type=TYPE_JSON,
264284
)
265-
],
285+
)
286+
return OrderStatuses(statuses=statuses, links=links)
287+
return OrderStatuses(
288+
statuses=statuses,
289+
links=links,
266290
)
267291
case Failure(e):
268292
logger.error(
269293
"An error occurred while retrieving order statuses: %s",
270294
traceback.format_exception(e),
271295
)
272-
raise HTTPException(
273-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
274-
detail="Error finding Order Statuses",
275-
)
296+
if isinstance(e, KeyError):
297+
raise NotFoundException(detail="Error finding pagination token")
298+
else:
299+
raise HTTPException(
300+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
301+
detail="Error finding Order Statuses",
302+
)
276303
case _:
277304
raise AssertionError("Expected code to be unreachable")
278305

tests/application.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
)
1919
from stapi_fastapi.models.order import (
2020
Order,
21-
OrderCollection,
2221
OrderParameters,
2322
OrderPayload,
2423
OrderStatus,
@@ -45,30 +44,25 @@ def __init__(self, orders: InMemoryOrderDB) -> None:
4544

4645
async def get_orders(
4746
self, request: Request, next: str | None, limit: int
48-
) -> ResultE[tuple[OrderCollection, str]]:
47+
) -> ResultE[tuple[list[Order], str]]:
4948
"""
5049
Return orders from backend. Handle pagination/limit if applicable
5150
"""
5251
try:
5352
start = 0
5453
if limit > 100:
5554
limit = 100
56-
5755
order_ids = [*self._orders_db._orders.keys()]
5856

5957
if next:
6058
start = order_ids.index(next)
61-
if not order_ids and not next: # no data in db
62-
return Success((OrderCollection(features=[]), ""))
63-
64-
end = start + limit
59+
end = min(start + limit, len(order_ids))
6560
ids = order_ids[start:end]
66-
feats = [self._orders_db._orders[order_id] for order_id in ids]
61+
orders = [self._orders_db._orders[order_id] for order_id in ids]
6762

68-
next = ""
6963
if end < len(order_ids):
70-
next = self._orders_db._orders[order_ids[end]].id
71-
return Success((OrderCollection(features=feats), next))
64+
return Success((orders, self._orders_db._orders[order_ids[end]].id))
65+
return Success((orders, ""))
7266
except Exception as e:
7367
return Failure(e)
7468

@@ -90,17 +84,12 @@ async def get_order_statuses(
9084

9185
if next:
9286
start = int(next)
93-
if not statuses and not next:
94-
return Success(([], ""))
95-
96-
end = start + limit
87+
end = min(start + limit, len(statuses))
9788
stati = statuses[start:end]
9889

99-
next = ""
10090
if end < len(statuses):
101-
next = str(end)
102-
return Success((stati, next))
103-
# return Success(self._orders_db._statuses[order_id])
91+
return Success((stati, str(end)))
92+
return Success((stati, ""))
10493
except Exception as e:
10594
return Failure(e)
10695

@@ -217,3 +206,23 @@ class MyOrderParameters(OrderParameters):
217206
root_router.add_product(product)
218207
app: FastAPI = FastAPI()
219208
app.include_router(root_router, prefix="")
209+
210+
TEST_STATUSES = {
211+
"test_order_id": [
212+
OrderStatus(
213+
timestamp=datetime(2025, 1, 14, 2, 21, 48, 466726, tzinfo=timezone.utc),
214+
status_code=OrderStatusCode.received,
215+
links=[],
216+
),
217+
OrderStatus(
218+
timestamp=datetime(2025, 1, 15, 5, 20, 48, 466726, tzinfo=timezone.utc),
219+
status_code=OrderStatusCode.accepted,
220+
links=[],
221+
),
222+
OrderStatus(
223+
timestamp=datetime(2025, 1, 16, 10, 15, 32, 466726, tzinfo=timezone.utc),
224+
status_code=OrderStatusCode.completed,
225+
links=[],
226+
),
227+
]
228+
}

tests/conftest.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from stapi_fastapi.routers.root_router import RootRouter
2222

2323
from .application import (
24+
TEST_STATUSES,
2425
InMemoryOrderDB,
2526
MockProductBackend,
2627
MockRootBackend,
@@ -41,6 +42,13 @@ def order_db() -> InMemoryOrderDB:
4142
return InMemoryOrderDB()
4243

4344

45+
@pytest.fixture
46+
def order_db_statuses() -> InMemoryOrderDB:
47+
order_db = InMemoryOrderDB()
48+
order_db._statuses = TEST_STATUSES
49+
return order_db
50+
51+
4452
@pytest.fixture
4553
def product_backend(order_db: InMemoryOrderDB) -> MockProductBackend:
4654
return MockProductBackend(order_db)
@@ -51,6 +59,11 @@ def root_backend(order_db: InMemoryOrderDB) -> MockRootBackend:
5159
return MockRootBackend(order_db)
5260

5361

62+
@pytest.fixture
63+
def root_backend_preloaded(order_db_statuses: InMemoryOrderDB) -> MockRootBackend:
64+
return MockRootBackend(order_db_statuses)
65+
66+
5467
@pytest.fixture
5568
def mock_product_test_spotlight(
5669
product_backend: MockProductBackend, mock_provider: Provider
@@ -118,6 +131,16 @@ def empty_stapi_client(root_backend, base_url: str) -> Iterator[TestClient]:
118131
yield client
119132

120133

134+
@pytest.fixture
135+
def statuses_client(root_backend_preloaded, base_url: str) -> Iterator[TestClient]:
136+
root_router = RootRouter(root_backend_preloaded)
137+
app = FastAPI()
138+
app.include_router(root_router, prefix="")
139+
140+
with TestClient(app, base_url=f"{base_url}") as client:
141+
yield client
142+
143+
121144
@pytest.fixture(scope="session")
122145
def url_for(base_url: str) -> Iterator[Callable[[str], str]]:
123146
def with_trailing_slash(value: str) -> str:

0 commit comments

Comments
 (0)