Skip to content

Commit 49c1e3d

Browse files
author
Phil Varner
committed
better types
1 parent 9a35025 commit 49c1e3d

4 files changed

Lines changed: 153 additions & 110 deletions

File tree

stapi-fastapi/src/stapi_fastapi/backends/root_backend.py

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Callable, Coroutine
2-
from typing import Any, TypeVar
2+
from typing import Any, Generic, Protocol, TypeVar
33

44
from fastapi import Request
55
from returns.maybe import Maybe
@@ -11,68 +11,75 @@
1111
OrderStatus,
1212
)
1313

14-
T = TypeVar("T", bound=OrderStatus)
14+
OrderStatusBound = TypeVar("OrderStatusBound", bound=OrderStatus)
1515

16-
GetOrders = Callable[
17-
[str | None, int, Request],
18-
Coroutine[Any, Any, ResultE[tuple[list[Order[T]], Maybe[str], Maybe[int]]]],
19-
]
20-
"""
21-
Type alias for an async function that returns a list of existing Orders.
2216

23-
Args:
24-
next (str | None): A pagination token.
25-
limit (int): The maximum number of orders to return in a page.
26-
request (Request): FastAPI's Request object.
17+
class GetOrders(Protocol, Generic[OrderStatusBound]):
18+
"""Type alias for an async function that returns a list of existing Orders.
2719
28-
Returns:
29-
A tuple containing a list of orders and a pagination token.
20+
Args:
21+
next (str | None): A pagination token.
22+
limit (int): The maximum number of orders to return in a page.
23+
request (Request): FastAPI's Request object.
3024
31-
- Should return returns.result.Success[tuple[list[Order], returns.maybe.Some[str]]]
32-
if including a pagination token
33-
- Should return returns.result.Success[tuple[list[Order], returns.maybe.Nothing]]
34-
if not including a pagination token
35-
- Returning returns.result.Failure[Exception] will result in a 500.
36-
"""
25+
Returns:
26+
A tuple containing a list of orders and a pagination token.
3727
38-
GetOrder = Callable[[str, Request], Coroutine[Any, Any, ResultE[Maybe[Order[OrderStatus]]]]]
39-
"""
40-
Type alias for an async function that gets details for the order with `order_id`.
28+
- Should return returns.result.Success[tuple[list[Order], returns.maybe.Some[str]]]
29+
if including a pagination token
30+
- Should return returns.result.Success[tuple[list[Order], returns.maybe.Nothing]]
31+
if not including a pagination token
32+
- Returning returns.result.Failure[Exception] will result in a 500.
33+
"""
4134

42-
Args:
43-
order_id (str): The order ID.
44-
request (Request): FastAPI's Request object.
35+
async def __call__(
36+
self,
37+
next: str | None,
38+
limit: int,
39+
request: Request,
40+
) -> ResultE[tuple[list[Order[OrderStatusBound]], Maybe[str], Maybe[int]]]: ...
4541

46-
Returns:
47-
- Should return returns.result.Success[returns.maybe.Some[Order]] if order is found.
48-
- Should return returns.result.Success[returns.maybe.Nothing] if the order is not found or if access is denied.
49-
- Returning returns.result.Failure[Exception] will result in a 500.
50-
"""
5142

43+
class GetOrder(Protocol, Generic[OrderStatusBound]):
44+
"""Type alias for an async function that gets details for the order with `order_id`.
5245
53-
GetOrderStatuses = Callable[
54-
[str, str | None, int, Request],
55-
Coroutine[Any, Any, ResultE[Maybe[tuple[list[T], Maybe[str]]]]],
56-
]
57-
"""
58-
Type alias for an async function that gets statuses for the order with `order_id`.
46+
Args:
47+
order_id (str): The order ID.
48+
request (Request): FastAPI's Request object.
5949
60-
Args:
61-
order_id (str): The order ID.
62-
next (str | None): A pagination token.
63-
limit (int): The maximum number of statuses to return in a page.
64-
request (Request): FastAPI's Request object.
50+
Returns:
51+
- Should return returns.result.Success[returns.maybe.Some[Order]] if order is found.
52+
- Should return returns.result.Success[returns.maybe.Nothing] if the order is not found or if access is denied.
53+
- Returning returns.result.Failure[Exception] will result in a 500.
54+
"""
6555

66-
Returns:
67-
A tuple containing a list of order statuses and a pagination token.
56+
async def __call__(self, order_id: str, request: Request) -> ResultE[Maybe[Order[OrderStatusBound]]]: ...
57+
58+
59+
class GetOrderStatuses(Protocol, Generic[OrderStatusBound]):
60+
"""Type alias for an async function that gets statuses for the order with `order_id`.
61+
62+
Args:
63+
order_id (str): The order ID.
64+
next (str | None): A pagination token.
65+
limit (int): The maximum number of statuses to return in a page.
66+
request (Request): FastAPI's Request object.
67+
68+
Returns:
69+
A tuple containing a list of order statuses and a pagination token.
70+
71+
- Should return returns.result.Success[returns.maybe.Some[tuple[list[OrderStatus], returns.maybe.Some[str]]]
72+
if order is found and including a pagination token.
73+
- Should return returns.result.Success[returns.maybe.Some[tuple[list[OrderStatus], returns.maybe.Nothing]]]
74+
if order is found and not including a pagination token.
75+
- Should return returns.result.Success[returns.maybe.Nothing] if the order is not found or if access is denied.
76+
- Returning returns.result.Failure[Exception] will result in a 500.
77+
"""
78+
79+
async def __call__(
80+
self, order_id: str, _next: str | None, limit: int, request: Request
81+
) -> ResultE[Maybe[tuple[list[OrderStatusBound], Maybe[str]]]]: ...
6882

69-
- Should return returns.result.Success[returns.maybe.Some[tuple[list[OrderStatus], returns.maybe.Some[str]]]
70-
if order is found and including a pagination token.
71-
- Should return returns.result.Success[returns.maybe.Some[tuple[list[OrderStatus], returns.maybe.Nothing]]]
72-
if order is found and not including a pagination token.
73-
- Should return returns.result.Success[returns.maybe.Nothing] if the order is not found or if access is denied.
74-
- Returning returns.result.Failure[Exception] will result in a 500.
75-
"""
7683

7784
GetOpportunitySearchRecords = Callable[
7885
[str | None, int, Request],

stapi-fastapi/src/stapi_fastapi/routers/product_router.py

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

33
import logging
44
import traceback
5-
from typing import TYPE_CHECKING, Any, TypeVar
5+
from typing import TYPE_CHECKING, Any
66

77
from fastapi import (
88
Depends,
@@ -50,7 +50,8 @@
5050
from stapi_fastapi.routers.utils import json_link
5151

5252
if TYPE_CHECKING:
53-
from stapi_fastapi.routers import RootRouter
53+
from stapi_fastapi.routers.root_router import ConformancesSupport, RootProvider
54+
5455

5556
logger = logging.getLogger(__name__)
5657

@@ -68,10 +69,7 @@ def get_prefer(prefer: str | None = Header(None)) -> str | None:
6869
return Prefer(prefer)
6970

7071

71-
T = TypeVar("T", bound=OrderStatus)
72-
73-
74-
def build_conformances(product: Product, root_router: RootRouter[T]) -> list[str]:
72+
def build_conformances(product: Product, conformances_support: ConformancesSupport) -> list[str]:
7573
# FIXME we can make this check more robust
7674
if not any(conformance.startswith("https://geojson.org/schema/") for conformance in product.conformsTo):
7775
raise ValueError("product conformance does not contain at least one geojson conformance")
@@ -81,7 +79,7 @@ def build_conformances(product: Product, root_router: RootRouter[T]) -> list[str
8179
if product.supports_opportunity_search:
8280
conformances.add(PRODUCT_CONFORMACES.opportunities)
8381

84-
if product.supports_async_opportunity_search and root_router.supports_async_opportunity_search:
82+
if product.supports_async_opportunity_search and conformances_support.supports_async_opportunity_search:
8583
conformances.add(PRODUCT_CONFORMACES.opportunities)
8684
conformances.add(PRODUCT_CONFORMACES.opportunities_async)
8785

@@ -93,20 +91,21 @@ class ProductRouter(StapiFastapiBaseRouter):
9391
def __init__( # noqa
9492
self,
9593
product: Product,
96-
root_router: RootRouter[T],
94+
root_provider: RootProvider,
9795
*args: Any,
9896
**kwargs: Any,
9997
) -> None:
10098
super().__init__(*args, **kwargs)
10199

102100
self.product = product
103-
self.root_router = root_router
104-
self.conformances = build_conformances(product, root_router)
101+
self.root_provider = root_provider
102+
self.conformances_support: ConformancesSupport = root_provider
103+
self.conformances = build_conformances(product, root_provider)
105104

106105
self.add_api_route(
107106
path="",
108107
endpoint=self.get_product,
109-
name=f"{self.root_router.name}:{self.product.id}:{GET_PRODUCT}",
108+
name=f"{self.root_provider.name}:{self.product.id}:{GET_PRODUCT}",
110109
methods=["GET"],
111110
summary="Retrieve this product",
112111
tags=["Products"],
@@ -115,7 +114,7 @@ def __init__( # noqa
115114
self.add_api_route(
116115
path="/conformance",
117116
endpoint=self.get_product_conformance,
118-
name=f"{self.root_router.name}:{self.product.id}:{CONFORMANCE}",
117+
name=f"{self.root_provider.name}:{self.product.id}:{CONFORMANCE}",
119118
methods=["GET"],
120119
summary="Get conformance urls for the product",
121120
tags=["Products"],
@@ -124,7 +123,7 @@ def __init__( # noqa
124123
self.add_api_route(
125124
path="/queryables",
126125
endpoint=self.get_product_queryables,
127-
name=f"{self.root_router.name}:{self.product.id}:{GET_QUERYABLES}",
126+
name=f"{self.root_provider.name}:{self.product.id}:{GET_QUERYABLES}",
128127
methods=["GET"],
129128
summary="Get queryables for the product",
130129
tags=["Products"],
@@ -133,7 +132,7 @@ def __init__( # noqa
133132
self.add_api_route(
134133
path="/order-parameters",
135134
endpoint=self.get_product_order_parameters,
136-
name=f"{self.root_router.name}:{self.product.id}:{GET_ORDER_PARAMETERS}",
135+
name=f"{self.root_provider.name}:{self.product.id}:{GET_ORDER_PARAMETERS}",
137136
methods=["GET"],
138137
summary="Get order parameters for the product",
139138
tags=["Products"],
@@ -160,7 +159,7 @@ async def _create_order(
160159
self.add_api_route(
161160
path="/orders",
162161
endpoint=_create_order,
163-
name=f"{self.root_router.name}:{self.product.id}:{CREATE_ORDER}",
162+
name=f"{self.root_provider.name}:{self.product.id}:{CREATE_ORDER}",
164163
methods=["POST"],
165164
response_class=GeoJSONResponse,
166165
status_code=status.HTTP_201_CREATED,
@@ -169,12 +168,13 @@ async def _create_order(
169168
)
170169

171170
if product.supports_opportunity_search or (
172-
self.product.supports_async_opportunity_search and self.root_router.supports_async_opportunity_search
171+
self.product.supports_async_opportunity_search
172+
and self.conformances_support.supports_async_opportunity_search
173173
):
174174
self.add_api_route(
175175
path="/opportunities",
176176
endpoint=self.search_opportunities,
177-
name=f"{self.root_router.name}:{self.product.id}:{SEARCH_OPPORTUNITIES}",
177+
name=f"{self.root_provider.name}:{self.product.id}:{SEARCH_OPPORTUNITIES}",
178178
methods=["POST"],
179179
response_class=GeoJSONResponse,
180180
# unknown why mypy can't see the queryables property on Product, ignoring
@@ -192,11 +192,11 @@ async def _create_order(
192192
tags=["Products"],
193193
)
194194

195-
if product.supports_async_opportunity_search and root_router.supports_async_opportunity_search:
195+
if product.supports_async_opportunity_search and self.conformances_support.supports_async_opportunity_search:
196196
self.add_api_route(
197197
path="/opportunities/{opportunity_collection_id}",
198198
endpoint=self.get_opportunity_collection,
199-
name=f"{self.root_router.name}:{self.product.id}:{GET_OPPORTUNITY_COLLECTION}",
199+
name=f"{self.root_provider.name}:{self.product.id}:{GET_OPPORTUNITY_COLLECTION}",
200200
methods=["GET"],
201201
response_class=GeoJSONResponse,
202202
summary="Get an Opportunity Collection by ID",
@@ -205,30 +205,34 @@ async def _create_order(
205205

206206
def get_product(self, request: Request) -> ProductPydantic:
207207
links = [
208-
json_link("self", self.url_for(request, f"{self.root_router.name}:{self.product.id}:{GET_PRODUCT}")),
209-
json_link("conformance", self.url_for(request, f"{self.root_router.name}:{self.product.id}:{CONFORMANCE}")),
208+
json_link("self", self.url_for(request, f"{self.root_provider.name}:{self.product.id}:{GET_PRODUCT}")),
210209
json_link(
211-
"queryables", self.url_for(request, f"{self.root_router.name}:{self.product.id}:{GET_QUERYABLES}")
210+
"conformance", self.url_for(request, f"{self.root_provider.name}:{self.product.id}:{CONFORMANCE}")
211+
),
212+
json_link(
213+
"queryables",
214+
self.url_for(request, f"{self.root_provider.name}:{self.product.id}:{GET_QUERYABLES}"),
212215
),
213216
json_link(
214217
"order-parameters",
215-
self.url_for(request, f"{self.root_router.name}:{self.product.id}:{GET_ORDER_PARAMETERS}"),
218+
self.url_for(request, f"{self.root_provider.name}:{self.product.id}:{GET_ORDER_PARAMETERS}"),
216219
),
217220
Link(
218-
href=self.url_for(request, f"{self.root_router.name}:{self.product.id}:{CREATE_ORDER}"),
221+
href=self.url_for(request, f"{self.root_provider.name}:{self.product.id}:{CREATE_ORDER}"),
219222
rel="create-order",
220223
type=TYPE_JSON,
221224
method="POST",
222225
),
223226
]
224227

225228
if self.product.supports_opportunity_search or (
226-
self.product.supports_async_opportunity_search and self.root_router.supports_async_opportunity_search
229+
self.product.supports_async_opportunity_search
230+
and self.conformances_support.supports_async_opportunity_search
227231
):
228232
links.append(
229233
json_link(
230234
"opportunities",
231-
self.url_for(request, f"{self.root_router.name}:{self.product.id}:{SEARCH_OPPORTUNITIES}"),
235+
self.url_for(request, f"{self.root_provider.name}:{self.product.id}:{SEARCH_OPPORTUNITIES}"),
232236
),
233237
)
234238

@@ -246,7 +250,8 @@ async def search_opportunities(
246250
"""
247251
# sync
248252
if not (
249-
self.root_router.supports_async_opportunity_search and self.product.supports_async_opportunity_search
253+
self.product.supports_async_opportunity_search
254+
and self.conformances_support.supports_async_opportunity_search
250255
) or (prefer is Prefer.wait and self.product.supports_opportunity_search):
251256
return await self.search_opportunities_sync(
252257
search,
@@ -301,7 +306,7 @@ async def search_opportunities_sync(
301306
case x:
302307
raise AssertionError(f"Expected code to be unreachable {x}")
303308

304-
if prefer is Prefer.wait and self.root_router.supports_async_opportunity_search:
309+
if prefer is Prefer.wait and self.conformances_support.supports_async_opportunity_search:
305310
response.headers["Preference-Applied"] = "wait"
306311

307312
return OpportunityCollection(features=features, links=links)
@@ -314,10 +319,12 @@ async def search_opportunities_async(
314319
) -> JSONResponse:
315320
match await self.product.search_opportunities_async(self, search, request):
316321
case Success(search_record):
317-
search_record.links.append(self.root_router.opportunity_search_record_self_link(search_record, request))
322+
search_record.links.append(
323+
self.root_provider.opportunity_search_record_self_link(search_record, request)
324+
)
318325
headers = {}
319326
headers["Location"] = str(
320-
self.root_router.generate_opportunity_search_record_href(request, search_record.id)
327+
self.root_provider.generate_opportunity_search_record_href(request, search_record.id)
321328
)
322329
if prefer is not None:
323330
headers["Preference-Applied"] = "respond-async"
@@ -368,8 +375,8 @@ async def create_order(self, payload: OrderPayload, request: Request, response:
368375
request,
369376
):
370377
case Success(order):
371-
order.links.extend(self.root_router.order_links(order, request))
372-
location = str(self.root_router.generate_order_href(request, order.id))
378+
order.links.extend(self.root_provider.order_links(order, request))
379+
location = str(self.root_provider.generate_order_href(request, order.id))
373380
response.headers["Location"] = location
374381
return order # type: ignore
375382
case Failure(e) if isinstance(e, QueryablesError):
@@ -388,7 +395,7 @@ async def create_order(self, payload: OrderPayload, request: Request, response:
388395

389396
def order_link(self, request: Request, opp_req: OpportunityPayload) -> Link:
390397
return Link(
391-
href=self.url_for(request, f"{self.root_router.name}:{self.product.id}:{CREATE_ORDER}"),
398+
href=self.url_for(request, f"{self.root_provider.name}:{self.product.id}:{CREATE_ORDER}"),
392399
rel="create-order",
393400
type=TYPE_JSON,
394401
method="POST",
@@ -423,7 +430,7 @@ async def get_opportunity_collection(
423430
"self",
424431
self.url_for(
425432
request,
426-
f"{self.root_router.name}:{self.product.id}:{GET_OPPORTUNITY_COLLECTION}",
433+
f"{self.root_provider.name}:{self.product.id}:{GET_OPPORTUNITY_COLLECTION}",
427434
opportunity_collection_id=opportunity_collection_id,
428435
),
429436
),

0 commit comments

Comments
 (0)