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

Commit b50795b

Browse files
author
Phil Varner
committed
add Result and Maybe to return types of ProductBackend
1 parent f343726 commit b50795b

File tree

7 files changed

+114
-78
lines changed

7 files changed

+114
-78
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ none
1313

1414
### Changed
1515

16-
- RootBackend protocol uses `returns` library types Result and Maybe instead of exceptions.
16+
- RootBackend and ProductBackend protocols use `returns` library types Result and Maybe instead of
17+
raising exceptions.
1718
- Create Order endpoint from `.../order` to `.../orders`
1819
- Order field `id` must be a string, instead of previously allowing int. This is because while an
1920
order ID may an integral numeric value, it is not a "number" in the sense that math will be performed

bin/server.py

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ class MockRootBackend(RootBackend):
3838
def __init__(self, orders: MockOrderDB) -> None:
3939
self._orders: MockOrderDB = orders
4040

41-
async def get_orders(self, request: Request) -> OrderCollection:
41+
async def get_orders(
42+
self, request: Request
43+
) -> Result[OrderCollection, Maybe[Exception]]:
4244
"""
4345
Show all orders.
4446
"""
45-
return OrderCollection(features=list(self._orders.values()))
47+
return Success(OrderCollection(features=list(self._orders.values())))
4648

4749
async def get_order(
4850
self, order_id: str, request: Request
@@ -67,41 +69,49 @@ async def search_opportunities(
6769
product_router: ProductRouter,
6870
search: OpportunityRequest,
6971
request: Request,
70-
) -> list[Opportunity]:
71-
return [o.model_copy(update=search.model_dump()) for o in self._opportunities]
72+
) -> Result[list[Opportunity], Maybe[Exception]]:
73+
try:
74+
return Success(
75+
[o.model_copy(update=search.model_dump()) for o in self._opportunities]
76+
)
77+
except Exception as e:
78+
return Failure(e)
7279

7380
async def create_order(
7481
self, product_router: ProductRouter, payload: OrderRequest, request: Request
75-
) -> Order:
82+
) -> Result[Order, Maybe[Exception]]:
7683
"""
7784
Create a new order.
7885
"""
79-
order = Order(
80-
id=str(uuid4()),
81-
geometry=payload.geometry,
82-
properties={
83-
"product_id": product_router.product.id,
84-
"created": datetime.now(timezone.utc),
85-
"status": OrderStatus(
86-
timestamp=datetime.now(timezone.utc),
87-
status_code=OrderStatusCode.accepted,
88-
),
89-
"search_parameters": {
90-
"geometry": payload.geometry,
91-
"datetime": payload.datetime,
92-
"filter": payload.filter,
93-
},
94-
"order_parameters": payload.order_parameters.model_dump(),
95-
"opportunity_properties": {
96-
"datetime": "2024-01-29T12:00:00Z/2024-01-30T12:00:00Z",
97-
"off_nadir": 10,
86+
try:
87+
order = Order(
88+
id=str(uuid4()),
89+
geometry=payload.geometry,
90+
properties={
91+
"product_id": product_router.product.id,
92+
"created": datetime.now(timezone.utc),
93+
"status": OrderStatus(
94+
timestamp=datetime.now(timezone.utc),
95+
status_code=OrderStatusCode.accepted,
96+
),
97+
"search_parameters": {
98+
"geometry": payload.geometry,
99+
"datetime": payload.datetime,
100+
"filter": payload.filter,
101+
},
102+
"order_parameters": payload.order_parameters.model_dump(),
103+
"opportunity_properties": {
104+
"datetime": "2024-01-29T12:00:00Z/2024-01-30T12:00:00Z",
105+
"off_nadir": 10,
106+
},
98107
},
99-
},
100-
links=[],
101-
)
108+
links=[],
109+
)
102110

103-
self._orders[order.id] = order
104-
return order
111+
self._orders[order.id] = order
112+
return Success(order)
113+
except Exception as e:
114+
return Failure(e)
105115

106116

107117
class MyOpportunityProperties(OpportunityProperties):

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ dev = "stapi_fastapi.__dev__:cli"
3636
[tool.ruff]
3737
line-length = 88
3838

39+
[tool.ruff.format]
40+
quote-style = 'double'
41+
3942
[tool.ruff.lint]
4043
extend-ignore = ["E501", "UP007", "UP034"]
4144
select = [

src/stapi_fastapi/backends/product_backend.py

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

55
from fastapi import Request
6+
from returns.maybe import Maybe
7+
from returns.result import Result
68

79
from stapi_fastapi.models.opportunity import Opportunity, OpportunityRequest
810
from stapi_fastapi.models.order import Order, OrderRequest
@@ -15,25 +17,23 @@ async def search_opportunities(
1517
product_router: ProductRouter,
1618
search: OpportunityRequest,
1719
request: Request,
18-
) -> list[Opportunity]:
20+
) -> Result[list[Opportunity], Maybe[Exception]]:
1921
"""
2022
Search for ordering opportunities for the given search parameters.
2123
22-
Backends must validate search constraints and raise
24+
Backends must validate search constraints and return
2325
`stapi_fastapi.exceptions.ConstraintsException` if not valid.
2426
"""
25-
...
2627

2728
async def create_order(
2829
self,
2930
product_router: ProductRouter,
3031
search: OrderRequest,
3132
request: Request,
32-
) -> Order:
33+
) -> Result[Order, Maybe[Exception]]:
3334
"""
3435
Create a new order.
3536
36-
Backends must validate order payload and raise
37+
Backends must validate order payload and return
3738
`stapi_fastapi.exceptions.ConstraintsException` if not valid.
3839
"""
39-
...

src/stapi_fastapi/backends/root_backend.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99

1010
class RootBackend(Protocol): # pragma: nocover
11-
async def get_orders(self, request: Request) -> OrderCollection:
11+
async def get_orders(
12+
self, request: Request
13+
) -> Result[OrderCollection, Maybe[Exception]]:
1214
"""
1315
Return a list of existing orders.
1416
"""

src/stapi_fastapi/routers/product_router.py

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from fastapi import APIRouter, HTTPException, Request, Response, status
66
from geojson_pydantic.geometries import Geometry
7+
from returns.maybe import Maybe, Some
8+
from returns.result import Failure, Success
79

810
from stapi_fastapi.constants import TYPE_GEOJSON, TYPE_JSON
911
from stapi_fastapi.exceptions import ConstraintsException
@@ -158,27 +160,32 @@ async def search_opportunities(
158160
"""
159161
Explore the opportunities available for a particular set of constraints
160162
"""
161-
try:
162-
opportunities = await self.product.backend.search_opportunities(
163-
self, search, request
164-
)
165-
except ConstraintsException as exc:
166-
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.detail)
167-
168-
return OpportunityCollection(
169-
features=opportunities,
170-
links=[
171-
Link(
172-
href=str(
173-
request.url_for(
174-
f"{self.root_router.name}:{self.product.id}:create-order",
163+
match await self.product.backend.search_opportunities(self, search, request):
164+
case Success(features):
165+
return OpportunityCollection(
166+
features=features,
167+
links=[
168+
Link(
169+
href=str(
170+
request.url_for(
171+
f"{self.root_router.name}:{self.product.id}:create-order",
172+
),
173+
),
174+
rel="create-order",
175+
type=TYPE_JSON,
175176
),
176-
),
177-
rel="create-order",
178-
type=TYPE_JSON,
179-
),
180-
],
181-
)
177+
],
178+
)
179+
case Failure(Some(e)) if isinstance(e, ConstraintsException):
180+
raise e
181+
case Failure(Some(e)):
182+
raise HTTPException(
183+
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail
184+
)
185+
case Failure(Maybe.empty):
186+
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
187+
case x:
188+
raise AssertionError(f"Expected code to be unreachable {x}")
182189

183190
def get_product_constraints(self: Self) -> JsonSchemaModel:
184191
"""
@@ -198,16 +205,23 @@ async def create_order(
198205
"""
199206
Create a new order.
200207
"""
201-
try:
202-
order = await self.product.backend.create_order(
203-
self,
204-
payload,
205-
request,
206-
)
207-
except ConstraintsException as exc:
208-
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.detail)
209-
210-
location = str(self.root_router.generate_order_href(request, order.id))
211-
order.links.append(Link(href=location, rel="self", type=TYPE_GEOJSON))
212-
response.headers["Location"] = location
213-
return order
208+
match await self.product.backend.create_order(
209+
self,
210+
payload,
211+
request,
212+
):
213+
case Success(order):
214+
location = str(self.root_router.generate_order_href(request, order.id))
215+
order.links.append(Link(href=location, rel="self", type=TYPE_GEOJSON))
216+
response.headers["Location"] = location
217+
return order
218+
case Failure(Some(e)) if isinstance(e, ConstraintsException):
219+
raise e
220+
case Failure(Some(e)):
221+
raise HTTPException(
222+
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail
223+
)
224+
case Failure(Maybe.empty):
225+
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
226+
case x:
227+
raise AssertionError(f"Expected code to be unreachable {x}")

tests/backends.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ class MockRootBackend(RootBackend):
2727
def __init__(self, orders: MockOrderDB) -> None:
2828
self._orders = orders
2929

30-
async def get_orders(self, request: Request) -> OrderCollection:
30+
async def get_orders(
31+
self, request: Request
32+
) -> Result[OrderCollection, Maybe[Exception]]:
3133
"""
3234
Show all orders.
3335
"""
34-
return OrderCollection(features=list(self._orders.values()))
36+
return Success(OrderCollection(features=list(self._orders.values())))
3537

3638
async def get_order(
3739
self, order_id: str, request: Request
@@ -56,15 +58,17 @@ async def search_opportunities(
5658
product_router: ProductRouter,
5759
search: OpportunityRequest,
5860
request: Request,
59-
) -> list[Opportunity]:
60-
return [o.model_copy(update=search.model_dump()) for o in self._opportunities]
61+
) -> Result[list[Opportunity], Maybe[Exception]]:
62+
return Success(
63+
[o.model_copy(update=search.model_dump()) for o in self._opportunities]
64+
)
6165

6266
async def create_order(
6367
self,
6468
product_router: ProductRouter,
6569
payload: OrderRequest,
6670
request: Request,
67-
) -> Order:
71+
) -> Result[Order, Maybe[Exception]]:
6872
"""
6973
Create a new order.
7074
"""
@@ -93,8 +97,10 @@ async def create_order(
9397
links=[],
9498
)
9599
self._orders[order.id] = order
96-
return order
100+
return Success(order)
97101
else:
98-
raise ConstraintsException(
99-
f"not allowed: payload {payload.model_dump_json()} not in {[p.model_dump_json() for p in self._allowed_payloads]}"
102+
return Failure(
103+
ConstraintsException(
104+
f"not allowed: payload {payload.model_dump_json()} not in {[p.model_dump_json() for p in self._allowed_payloads]}"
105+
)
100106
)

0 commit comments

Comments
 (0)