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

Commit 9cfd146

Browse files
feat: creating methods to make Links to improve readability feat: making pagination token return Maybe[str] instead of usng empty strings feat: tweaking logic path in endpoint functions to make a single return point tests: updating tests as necessary to accomodating changes
1 parent a5999bd commit 9cfd146

File tree

9 files changed

+142
-123
lines changed

9 files changed

+142
-123
lines changed

src/stapi_fastapi/backends/product_backend.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Protocol
44

55
from fastapi import Request
6+
from returns.maybe import Maybe
67
from returns.result import ResultE
78

89
from stapi_fastapi.models.opportunity import Opportunity, OpportunityRequest
@@ -18,9 +19,9 @@ async def search_opportunities(
1819
request: Request,
1920
next: str | None,
2021
limit: int,
21-
) -> ResultE[tuple[list[Opportunity], str]]:
22+
) -> ResultE[tuple[list[Opportunity], Maybe[str]]]:
2223
"""
23-
Search for ordering opportunities for the given search parameters.
24+
Search for ordering opportunities for the given search parameters and return pagination token if applicable.
2425
2526
Backends must validate search constraints and return
2627
`stapi_fastapi.exceptions.ConstraintsException` if not valid.

src/stapi_fastapi/backends/root_backend.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
class RootBackend[T: OrderStatus](Protocol): # pragma: nocover
1414
async def get_orders(
1515
self, request: Request, next: str | None, limit: int
16-
) -> ResultE[tuple[list[Order], str]]:
16+
) -> ResultE[tuple[list[Order], Maybe[str]]]:
1717
"""
1818
Return a list of existing orders and pagination token if applicable
1919
No pagination will return empty string for token
@@ -35,7 +35,7 @@ async def get_order(self, order_id: str, request: Request) -> ResultE[Maybe[Orde
3535

3636
async def get_order_statuses(
3737
self, order_id: str, request: Request, next: str | None, limit: int
38-
) -> ResultE[tuple[list[T], str]]:
38+
) -> ResultE[tuple[list[T], Maybe[str]]]:
3939
"""
4040
Get statuses for order with `order_id`.
4141

src/stapi_fastapi/routers/product_router.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from fastapi import APIRouter, HTTPException, Request, Response, status
88
from geojson_pydantic.geometries import Geometry
9+
from returns.maybe import Some
910
from returns.result import Failure, Success
1011

1112
from stapi_fastapi.constants import TYPE_JSON
@@ -170,36 +171,25 @@ async def search_opportunities(
170171
"""
171172
Explore the opportunities available for a particular set of constraints
172173
"""
174+
links: list[Link] = []
173175
match await self.product.backend.search_opportunities(
174176
self, search, request, next, limit
175177
):
176-
case Success((features, pagination_token)):
177-
links = [
178+
case Success((features, Some(pagination_token))):
179+
links.append(self.order_link(request, "create-order"))
180+
links.append(
178181
Link(
179182
href=str(
180-
request.url_for(
181-
f"{self.root_router.name}:{self.product.id}:create-order",
182-
),
183+
request.url.remove_query_params(keys=["next", "limit"])
183184
),
184-
rel="create-order",
185+
rel="next",
185186
type=TYPE_JSON,
186187
method="POST",
187-
),
188-
]
189-
if pagination_token:
190-
links.append(
191-
Link(
192-
href=str(
193-
request.url.include_query_params(next=pagination_token)
194-
),
195-
rel="next",
196-
type=TYPE_JSON,
197-
method="POST",
198-
body=search,
199-
)
188+
body={"next": pagination_token, "search": search},
200189
)
201-
return OpportunityCollection(features=features, links=links)
202-
return OpportunityCollection(features=features, links=links)
190+
)
191+
case Success((features, Nothing)): # noqa: F841
192+
links.append(self.order_link(request, "create-order"))
203193
case Failure(e) if isinstance(e, ConstraintsException):
204194
raise e
205195
case Failure(e):
@@ -213,6 +203,7 @@ async def search_opportunities(
213203
)
214204
case x:
215205
raise AssertionError(f"Expected code to be unreachable {x}")
206+
return OpportunityCollection(features=features, links=links)
216207

217208
def get_product_constraints(self: Self) -> JsonSchemaModel:
218209
"""
@@ -255,3 +246,15 @@ async def create_order(
255246
)
256247
case x:
257248
raise AssertionError(f"Expected code to be unreachable {x}")
249+
250+
def order_link(self, request: Request, suffix: str):
251+
return Link(
252+
href=str(
253+
request.url_for(
254+
f"{self.root_router.name}:{self.product.id}:{suffix}",
255+
),
256+
),
257+
rel="create-order",
258+
type=TYPE_JSON,
259+
method="POST",
260+
)

src/stapi_fastapi/routers/root_router.py

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(
4242
self.conformances = conformances
4343
self.openapi_endpoint_name = openapi_endpoint_name
4444
self.docs_endpoint_name = docs_endpoint_name
45+
self.product_ids: list = []
4546

4647
# A dict is used to track the product routers so we can ensure
4748
# idempotentcy in case a product is added multiple times, and also to
@@ -144,29 +145,28 @@ def get_products(
144145
self, request: Request, next: str | None = None, limit: int = 10
145146
) -> ProductsCollection:
146147
start = 0
147-
product_ids = [*self.product_routers.keys()]
148148
try:
149149
if next:
150-
start = product_ids.index(next)
150+
start = self.product_ids.index(next)
151151
except ValueError:
152-
logging.exception("An error occurred while retrieving orders")
153-
raise NotFoundException(detail="Error finding pagination token") from None
154-
end = min(start + limit, len(product_ids))
155-
ids = product_ids[start:end]
152+
logging.exception("An error occurred while retrieving products")
153+
raise NotFoundException(
154+
detail="Error finding pagination token for products"
155+
) from None
156+
end = start + limit
157+
ids = self.product_ids[start:end]
156158
links = [
157159
Link(
158160
href=str(request.url_for(f"{self.name}:list-products")),
159161
rel="self",
160162
type=TYPE_JSON,
161163
),
162164
]
163-
if end < len(product_ids) and end != 0:
165+
if end > 0 and end < len(self.product_ids):
164166
links.append(
165167
Link(
166168
href=str(
167-
request.url.include_query_params(
168-
next=self.product_routers[product_ids[end]].product.id
169-
),
169+
request.url.include_query_params(next=self.product_ids[end]),
170170
),
171171
rel="next",
172172
type=TYPE_JSON,
@@ -183,36 +183,24 @@ def get_products(
183183
async def get_orders(
184184
self, request: Request, next: str | None = None, limit: int = 10
185185
) -> OrderCollection:
186+
# links: list[Link] = []
186187
match await self.backend.get_orders(request, next, limit):
187-
case Success((orders, pagination_token)):
188+
case Success((orders, Some(pagination_token))):
188189
for order in orders:
189-
order.links.append(
190-
Link(
191-
href=str(
192-
request.url_for(
193-
f"{self.name}:get-order", order_id=order.id
194-
)
195-
),
196-
rel="self",
197-
type=TYPE_JSON,
198-
)
199-
)
200-
if pagination_token:
201-
return OrderCollection(
202-
features=orders,
203-
links=[
204-
Link(
205-
href=str(
206-
request.url.include_query_params(
207-
next=pagination_token
208-
)
209-
),
210-
rel="next",
211-
type=TYPE_JSON,
212-
)
213-
],
190+
order.links.append(self.order_link(request, "get-order", order))
191+
links = [
192+
Link(
193+
href=str(
194+
request.url.include_query_params(next=pagination_token)
195+
),
196+
rel="next",
197+
type=TYPE_JSON,
214198
)
215-
return OrderCollection(features=orders)
199+
]
200+
case Success((orders, Nothing)): # noqa: F841
201+
for order in orders:
202+
order.links.append(self.order_link(request, "get-order", order))
203+
links = []
216204
case Failure(e):
217205
logger.error(
218206
"An error occurred while retrieving orders: %s",
@@ -227,6 +215,7 @@ async def get_orders(
227215
)
228216
case _:
229217
raise AssertionError("Expected code to be unreachable")
218+
return OrderCollection(features=orders, links=links)
230219

231220
async def get_order(self: Self, order_id: str, request: Request) -> Order:
232221
"""
@@ -258,34 +247,24 @@ async def get_order_statuses(
258247
next: str | None = None,
259248
limit: int = 10,
260249
) -> OrderStatuses:
250+
links: list[Link] = []
261251
match await self.backend.get_order_statuses(order_id, request, next, limit):
262-
case Success((statuses, pagination_token)):
263-
links = [
252+
case Success((statuses, Some(pagination_token))):
253+
links.append(
254+
self.order_statuses_link(request, "list-order-statuses", order_id)
255+
)
256+
links.append(
264257
Link(
265258
href=str(
266-
request.url_for(
267-
f"{self.name}:list-order-statuses",
268-
order_id=order_id,
269-
)
259+
request.url.include_query_params(next=pagination_token)
270260
),
271-
rel="self",
261+
rel="next",
272262
type=TYPE_JSON,
273263
)
274-
]
275-
if pagination_token:
276-
links.append(
277-
Link(
278-
href=str(
279-
request.url.include_query_params(next=pagination_token)
280-
),
281-
rel="next",
282-
type=TYPE_JSON,
283-
)
284-
)
285-
return OrderStatuses(statuses=statuses, links=links)
286-
return OrderStatuses(
287-
statuses=statuses,
288-
links=links,
264+
)
265+
case Success((statuses, Nothing)): # noqa: F841
266+
links.append(
267+
self.order_statuses_link(request, "list-order-statuses", order_id)
289268
)
290269
case Failure(e):
291270
logger.error(
@@ -301,12 +280,14 @@ async def get_order_statuses(
301280
)
302281
case _:
303282
raise AssertionError("Expected code to be unreachable")
283+
return OrderStatuses(statuses=statuses, links=links)
304284

305285
def add_product(self: Self, product: Product, *args, **kwargs) -> None:
306286
# Give the include a prefix from the product router
307287
product_router = ProductRouter(product, self, *args, **kwargs)
308288
self.include_router(product_router, prefix=f"/products/{product.id}")
309289
self.product_routers[product.id] = product_router
290+
self.product_ids = [*self.product_routers.keys()]
310291

311292
def generate_order_href(self: Self, request: Request, order_id: str) -> URL:
312293
return request.url_for(f"{self.name}:get-order", order_id=order_id)
@@ -331,3 +312,22 @@ def add_order_links(self, order: Order, request: Request):
331312
type=TYPE_JSON,
332313
),
333314
)
315+
316+
def order_link(self, request: Request, link_suffix: str, order: Order):
317+
return Link(
318+
href=str(request.url_for(f"{self.name}:{link_suffix}", order_id=order.id)),
319+
rel="self",
320+
type=TYPE_JSON,
321+
)
322+
323+
def order_statuses_link(self, request: Request, link_suffix: str, order_id: str):
324+
return Link(
325+
href=str(
326+
request.url_for(
327+
f"{self.name}:{link_suffix}",
328+
order_id=order_id,
329+
)
330+
),
331+
rel="self",
332+
type=TYPE_JSON,
333+
)

tests/application.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from fastapi import FastAPI, Request
77
from pydantic import BaseModel, Field, model_validator
8-
from returns.maybe import Maybe
8+
from returns.maybe import Maybe, Nothing, Some
99
from returns.result import Failure, ResultE, Success
1010

1111
from stapi_fastapi.backends.product_backend import ProductBackend
@@ -44,7 +44,7 @@ def __init__(self, orders: InMemoryOrderDB) -> None:
4444

4545
async def get_orders(
4646
self, request: Request, next: str | None, limit: int
47-
) -> ResultE[tuple[list[Order], str]]:
47+
) -> ResultE[tuple[list[Order], Maybe[str]]]:
4848
"""
4949
Return orders from backend. Handle pagination/limit if applicable
5050
"""
@@ -56,13 +56,15 @@ async def get_orders(
5656

5757
if next:
5858
start = order_ids.index(next)
59-
end = min(start + limit, len(order_ids))
59+
end = start + limit
6060
ids = order_ids[start:end]
6161
orders = [self._orders_db._orders[order_id] for order_id in ids]
6262

63-
if end < len(order_ids) and end != 0:
64-
return Success((orders, self._orders_db._orders[order_ids[end]].id))
65-
return Success((orders, ""))
63+
if end > 0 and end < len(order_ids):
64+
return Success(
65+
(orders, Some(self._orders_db._orders[order_ids[end]].id))
66+
)
67+
return Success((orders, Nothing))
6668
except Exception as e:
6769
return Failure(e)
6870

@@ -75,7 +77,7 @@ async def get_order(self, order_id: str, request: Request) -> ResultE[Maybe[Orde
7577

7678
async def get_order_statuses(
7779
self, order_id: str, request: Request, next: str | None, limit: int
78-
) -> ResultE[tuple[list[OrderStatus], str]]:
80+
) -> ResultE[tuple[list[OrderStatus], Maybe[str]]]:
7981
try:
8082
start = 0
8183
if limit > 100:
@@ -84,12 +86,12 @@ async def get_order_statuses(
8486

8587
if next:
8688
start = int(next)
87-
end = min(start + limit, len(statuses))
89+
end = start + limit
8890
stati = statuses[start:end]
8991

90-
if end < len(statuses) and end != 0:
91-
return Success((stati, str(end)))
92-
return Success((stati, ""))
92+
if end > 0 and end < len(statuses):
93+
return Success((stati, Some(str(end))))
94+
return Success((stati, Nothing))
9395
except Exception as e:
9496
return Failure(e)
9597

@@ -107,21 +109,21 @@ async def search_opportunities(
107109
request: Request,
108110
next: str | None,
109111
limit: int,
110-
) -> ResultE[tuple[list[Opportunity], str]]:
112+
) -> ResultE[tuple[list[Opportunity], Maybe[str]]]:
111113
try:
112114
start = 0
113115
if limit > 100:
114116
limit = 100
115117
if next:
116118
start = int(next)
117-
end = min(start + limit, len(self._opportunities))
119+
end = start + limit
118120
opportunities = [
119121
o.model_copy(update=search.model_dump())
120122
for o in self._opportunities[start:end]
121123
]
122-
if end < len(self._opportunities) and end != 0:
123-
return Success((opportunities, str(end)))
124-
return Success((opportunities, ""))
124+
if end > 0 and end < len(self._opportunities):
125+
return Success((opportunities, Some(str(end))))
126+
return Success((opportunities, Nothing))
125127
except Exception as e:
126128
return Failure(e)
127129

0 commit comments

Comments
 (0)