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

Commit f3adb87

Browse files
authored
refine pagination code, add body to create-order link in Opportunities Search result, rename OpportunityRequest to OpportunityPayload (#140)
1 parent 8baad02 commit f3adb87

File tree

11 files changed

+86
-64
lines changed

11 files changed

+86
-64
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2121
token.
2222
- Moved `OrderCollection` construction from the root backend to the `RootRouter`
2323
`get_orders` method.
24+
- Renamed `OpportunityRequest` to `OpportunityPayload` so that would not be confused as
25+
being a subclass of the Starlette/FastAPI Request class.
26+
27+
### Fixed
28+
29+
- Opportunities Search result now has the search body in the `create-order` link.
2430

2531
## [v0.5.0] - 2025-01-08
2632

src/stapi_fastapi/backends/product_backend.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
from returns.maybe import Maybe
77
from returns.result import ResultE
88

9-
from stapi_fastapi.models.opportunity import Opportunity, OpportunityRequest
9+
from stapi_fastapi.models.opportunity import Opportunity, OpportunityPayload
1010
from stapi_fastapi.models.order import Order, OrderPayload
1111
from stapi_fastapi.routers.product_router import ProductRouter
1212

1313
SearchOpportunities = Callable[
14-
[ProductRouter, OpportunityRequest, str | None, int, Request],
14+
[ProductRouter, OpportunityPayload, str | None, int, Request],
1515
Coroutine[Any, Any, ResultE[tuple[list[Opportunity], Maybe[str]]]],
1616
]
1717
"""

src/stapi_fastapi/models/opportunity.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Literal, TypeVar
1+
from typing import Any, Literal, TypeVar
22

33
from geojson_pydantic import Feature, FeatureCollection
44
from geojson_pydantic.geometries import Geometry
@@ -16,16 +16,22 @@ class OpportunityProperties(BaseModel):
1616
model_config = ConfigDict(extra="allow")
1717

1818

19-
class OpportunityRequest(BaseModel):
19+
class OpportunityPayload(BaseModel):
2020
datetime: DatetimeInterval
2121
geometry: Geometry
22-
# TODO: validate the CQL2 filter?
2322
filter: CQL2Filter | None = None
23+
2424
next: str | None = None
2525
limit: int = 10
2626

2727
model_config = ConfigDict(strict=True)
2828

29+
def search_body(self) -> dict[str, Any]:
30+
return self.model_dump(mode="json", include={"datetime", "geometry", "filter"})
31+
32+
def body(self) -> dict[str, Any]:
33+
return self.model_dump(mode="json")
34+
2935

3036
G = TypeVar("G", bound=Geometry)
3137
P = TypeVar("P", bound=OpportunityProperties)

src/stapi_fastapi/routers/product_router.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from stapi_fastapi.exceptions import ConstraintsException
1414
from stapi_fastapi.models.opportunity import (
1515
OpportunityCollection,
16-
OpportunityRequest,
16+
OpportunityPayload,
1717
)
1818
from stapi_fastapi.models.order import Order, OrderPayload
1919
from stapi_fastapi.models.product import Product
@@ -163,7 +163,7 @@ def get_product(self, request: Request) -> Product:
163163

164164
async def search_opportunities(
165165
self,
166-
search: OpportunityRequest,
166+
search: OpportunityPayload,
167167
request: Request,
168168
) -> OpportunityCollection:
169169
"""
@@ -178,13 +178,10 @@ async def search_opportunities(
178178
request,
179179
):
180180
case Success((features, Some(pagination_token))):
181-
links.append(self.order_link(request))
182-
search.next = pagination_token
183-
links.append(
184-
self.pagination_link(request, search.model_dump(mode="json"))
185-
)
181+
links.append(self.order_link(request, search))
182+
links.append(self.pagination_link(request, search, pagination_token))
186183
case Success((features, Nothing)): # noqa: F841
187-
links.append(self.order_link(request))
184+
links.append(self.order_link(request, search))
188185
case Failure(e) if isinstance(e, ConstraintsException):
189186
raise e
190187
case Failure(e):
@@ -224,7 +221,7 @@ async def create_order(
224221
request,
225222
):
226223
case Success(order):
227-
self.root_router.add_order_links(order, request)
224+
order.links.extend(self.root_router.order_links(order, request))
228225
location = str(self.root_router.generate_order_href(request, order.id))
229226
response.headers["Location"] = location
230227
return order
@@ -242,7 +239,7 @@ async def create_order(
242239
case x:
243240
raise AssertionError(f"Expected code to be unreachable {x}")
244241

245-
def order_link(self, request: Request):
242+
def order_link(self, request: Request, opp_req: OpportunityPayload):
246243
return Link(
247244
href=str(
248245
request.url_for(
@@ -252,11 +249,16 @@ def order_link(self, request: Request):
252249
rel="create-order",
253250
type=TYPE_JSON,
254251
method="POST",
252+
body=opp_req.search_body(),
255253
)
256254

257-
def pagination_link(self, request: Request, body: dict[str, str | dict]):
255+
def pagination_link(
256+
self, request: Request, opp_req: OpportunityPayload, pagination_token: str
257+
):
258+
body = opp_req.body()
259+
body["next"] = pagination_token
258260
return Link(
259-
href=str(request.url.remove_query_params(keys=["next", "limit"])),
261+
href=str(request.url),
260262
rel="next",
261263
type=TYPE_JSON,
262264
method="POST",

src/stapi_fastapi/routers/root_router.py

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def get_products(
168168
),
169169
]
170170
if end > 0 and end < len(self.product_ids):
171-
links.append(self.pagination_link(request, self.product_ids[end]))
171+
links.append(self.pagination_link(request, self.product_ids[end], limit))
172172
return ProductsCollection(
173173
products=[
174174
self.product_routers[product_id].get_product(request)
@@ -182,13 +182,14 @@ async def get_orders(
182182
) -> OrderCollection:
183183
links: list[Link] = []
184184
match await self._get_orders(next, limit, request):
185-
case Success((orders, Some(pagination_token))):
185+
case Success((orders, maybe_pagination_token)):
186186
for order in orders:
187-
order.links.append(self.order_link(request, order))
188-
links.append(self.pagination_link(request, pagination_token))
189-
case Success((orders, Nothing)): # noqa: F841
190-
for order in orders:
191-
order.links.append(self.order_link(request, order))
187+
order.links.extend(self.order_links(order, request))
188+
match maybe_pagination_token:
189+
case Some(x):
190+
links.append(self.pagination_link(request, x, limit))
191+
case Maybe.empty:
192+
pass
192193
case Failure(ValueError()):
193194
raise NotFoundException(detail="Error finding pagination token")
194195
case Failure(e):
@@ -210,7 +211,7 @@ async def get_order(self: Self, order_id: str, request: Request) -> Order:
210211
"""
211212
match await self._get_order(order_id, request):
212213
case Success(Some(order)):
213-
self.add_order_links(order, request)
214+
order.links.extend(self.order_links(order, request))
214215
return order
215216
case Success(Maybe.empty):
216217
raise NotFoundException("Order not found")
@@ -238,7 +239,7 @@ async def get_order_statuses(
238239
match await self._get_order_statuses(order_id, next, limit, request):
239240
case Success((statuses, Some(pagination_token))):
240241
links.append(self.order_statuses_link(request, order_id))
241-
links.append(self.pagination_link(request, pagination_token))
242+
links.append(self.pagination_link(request, pagination_token, limit))
242243
case Success((statuses, Nothing)): # noqa: F841
243244
links.append(self.order_statuses_link(request, order_id))
244245
case Failure(KeyError()):
@@ -271,28 +272,19 @@ def generate_order_statuses_href(
271272
) -> URL:
272273
return request.url_for(f"{self.name}:list-order-statuses", order_id=order_id)
273274

274-
def add_order_links(self, order: Order, request: Request):
275-
order.links.append(
275+
def order_links(self, order: Order, request: Request) -> list[Link]:
276+
return [
276277
Link(
277278
href=str(self.generate_order_href(request, order.id)),
278279
rel="self",
279280
type=TYPE_GEOJSON,
280-
)
281-
)
282-
order.links.append(
281+
),
283282
Link(
284283
href=str(self.generate_order_statuses_href(request, order.id)),
285284
rel="monitor",
286285
type=TYPE_JSON,
287286
),
288-
)
289-
290-
def order_link(self, request: Request, order: Order):
291-
return Link(
292-
href=str(request.url_for(f"{self.name}:get-order", order_id=order.id)),
293-
rel="self",
294-
type=TYPE_JSON,
295-
)
287+
]
296288

297289
def order_statuses_link(self, request: Request, order_id: str):
298290
return Link(
@@ -306,9 +298,11 @@ def order_statuses_link(self, request: Request, order_id: str):
306298
type=TYPE_JSON,
307299
)
308300

309-
def pagination_link(self, request: Request, pagination_token: str):
301+
def pagination_link(self, request: Request, pagination_token: str, limit: int):
310302
return Link(
311-
href=str(request.url.include_query_params(next=pagination_token)),
303+
href=str(
304+
request.url.include_query_params(next=pagination_token, limit=limit)
305+
),
312306
rel="next",
313307
type=TYPE_JSON,
314308
)

tests/application.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
mock_get_order_statuses,
1616
mock_get_orders,
1717
)
18-
from tests.shared import InMemoryOrderDB, mock_product_test_spotlight
18+
from tests.shared import (
19+
InMemoryOrderDB,
20+
mock_product_test_satellite_provider,
21+
mock_product_test_spotlight,
22+
)
1923

2024

2125
@asynccontextmanager
@@ -35,5 +39,6 @@ async def lifespan(app: FastAPI) -> AsyncIterator[dict[str, Any]]:
3539
conformances=[CORE],
3640
)
3741
root_router.add_product(mock_product_test_spotlight)
42+
root_router.add_product(mock_product_test_satellite_provider)
3843
app: FastAPI = FastAPI(lifespan=lifespan)
3944
app.include_router(root_router, prefix="")

tests/backends.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from stapi_fastapi.models.opportunity import (
99
Opportunity,
10-
OpportunityRequest,
10+
OpportunityPayload,
1111
)
1212
from stapi_fastapi.models.order import (
1313
Order,
@@ -76,7 +76,7 @@ async def mock_get_order_statuses(
7676

7777
async def mock_search_opportunities(
7878
product_router: ProductRouter,
79-
search: OpportunityRequest,
79+
search: OpportunityPayload,
8080
next: str | None,
8181
limit: int,
8282
request: Request,

tests/shared.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from collections import defaultdict
22
from datetime import datetime, timedelta, timezone
33
from typing import Any, Literal, Self
4+
from urllib.parse import parse_qs, urlparse
45
from uuid import uuid4
56

67
from fastapi import status
@@ -133,7 +134,7 @@ def create_mock_opportunity() -> Opportunity:
133134

134135
def pagination_tester(
135136
stapi_client: TestClient,
136-
endpoint: str,
137+
url: str,
137138
method: str,
138139
limit: int,
139140
target: str,
@@ -142,7 +143,7 @@ def pagination_tester(
142143
) -> None:
143144
retrieved = []
144145

145-
res = make_request(stapi_client, endpoint, method, body, None, limit)
146+
res = make_request(stapi_client, url, method, body, limit)
146147
assert res.status_code == status.HTTP_200_OK
147148
resp_body = res.json()
148149

@@ -151,15 +152,16 @@ def pagination_tester(
151152
next_url = next((d["href"] for d in resp_body["links"] if d["rel"] == "next"), None)
152153

153154
while next_url:
154-
url = next_url
155155
if method == "POST":
156156
body = next(
157157
(d["body"] for d in resp_body["links"] if d["rel"] == "next"), None
158158
)
159159

160-
res = make_request(stapi_client, url, method, body, next_url, limit)
161-
assert res.status_code == status.HTTP_200_OK
160+
res = make_request(stapi_client, next_url, method, body, limit)
161+
162+
assert res.status_code == status.HTTP_200_OK, res.status_code
162163
assert len(resp_body[target]) <= limit
164+
163165
resp_body = res.json()
164166
retrieved.extend(resp_body[target])
165167

@@ -177,22 +179,25 @@ def pagination_tester(
177179

178180
def make_request(
179181
stapi_client: TestClient,
180-
endpoint: str,
182+
url: str,
181183
method: str,
182184
body: dict | None,
183-
next_token: str | None,
184185
limit: int,
185186
) -> Response:
186187
"""request wrapper for pagination tests"""
187188

188189
match method:
189190
case "GET":
190-
if next_token: # extract pagination token
191-
next_token = next_token.split("next=")[1]
192-
params = {"next": next_token, "limit": limit}
193-
res = stapi_client.get(endpoint, params=params)
191+
o = urlparse(url)
192+
base_url = f"{o.scheme}://{o.netloc}{o.path}"
193+
parsed_qs = parse_qs(o.query)
194+
params = {}
195+
if "next" in parsed_qs:
196+
params["next"] = parsed_qs["next"][0]
197+
params["limit"] = int(parsed_qs.get("limit", [None])[0] or limit)
198+
res = stapi_client.get(base_url, params=params)
194199
case "POST":
195-
res = stapi_client.post(endpoint, json=body)
200+
res = stapi_client.post(url, json=body)
196201
case _:
197202
fail(f"method {method} not supported in make request")
198203

tests/test_opportunity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def test_search_opportunities_pagination(
9696

9797
pagination_tester(
9898
stapi_client=stapi_client,
99-
endpoint=f"/products/{product_id}/opportunities",
99+
url=f"/products/{product_id}/opportunities",
100100
method="POST",
101101
limit=limit,
102102
target="features",

tests/test_order.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,16 +170,20 @@ def test_get_orders_pagination(
170170
limit, setup_orders_pagination, create_order_payloads, stapi_client: TestClient
171171
) -> None:
172172
expected_returns = []
173-
if limit != 0:
173+
if limit > 0:
174174
for order in setup_orders_pagination:
175-
json_link = copy.deepcopy(order["links"][0])
176-
json_link["type"] = "application/json"
177-
order["links"].append(json_link)
175+
self_link = copy.deepcopy(order["links"][0])
176+
order["links"].append(self_link)
177+
monitor_link = copy.deepcopy(order["links"][0])
178+
monitor_link["rel"] = "monitor"
179+
monitor_link["type"] = "application/json"
180+
monitor_link["href"] = monitor_link["href"] + "/statuses"
181+
order["links"].append(monitor_link)
178182
expected_returns.append(order)
179183

180184
pagination_tester(
181185
stapi_client=stapi_client,
182-
endpoint="/orders",
186+
url="/orders",
183187
method="GET",
184188
limit=limit,
185189
target="features",
@@ -233,7 +237,7 @@ def test_get_order_status_pagination(
233237

234238
pagination_tester(
235239
stapi_client=stapi_client,
236-
endpoint=f"/orders/{order_id}/statuses",
240+
url=f"/orders/{order_id}/statuses",
237241
method="GET",
238242
limit=limit,
239243
target="statuses",

0 commit comments

Comments
 (0)