This repository was archived by the owner on Apr 2, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathproduct_router.py
More file actions
264 lines (243 loc) · 9.2 KB
/
product_router.py
File metadata and controls
264 lines (243 loc) · 9.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
from __future__ import annotations
import logging
import traceback
from typing import TYPE_CHECKING, Self
from fastapi import APIRouter, HTTPException, Request, Response, status
from geojson_pydantic.geometries import Geometry
from returns.maybe import Some
from returns.result import Failure, Success
from stapi_fastapi.constants import TYPE_JSON
from stapi_fastapi.exceptions import ConstraintsException
from stapi_fastapi.models.opportunity import (
OpportunityCollection,
OpportunityRequest,
)
from stapi_fastapi.models.order import Order, OrderPayload
from stapi_fastapi.models.product import Product
from stapi_fastapi.models.shared import Link
from stapi_fastapi.responses import GeoJSONResponse
from stapi_fastapi.types.json_schema_model import JsonSchemaModel
if TYPE_CHECKING:
from stapi_fastapi.routers import RootRouter
logger = logging.getLogger(__name__)
class ProductRouter(APIRouter):
def __init__(
self,
product: Product,
root_router: RootRouter,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.product = product
self.root_router = root_router
self.add_api_route(
path="",
endpoint=self.get_product,
name=f"{self.root_router.name}:{self.product.id}:get-product",
methods=["GET"],
summary="Retrieve this product",
tags=["Products"],
)
self.add_api_route(
path="/opportunities",
endpoint=self.search_opportunities,
name=f"{self.root_router.name}:{self.product.id}:search-opportunities",
methods=["POST"],
response_class=GeoJSONResponse,
# unknown why mypy can't see the constraints property on Product, ignoring
response_model=OpportunityCollection[
Geometry,
self.product.opportunity_properties, # type: ignore
],
summary="Search Opportunities for the product",
tags=["Products"],
)
self.add_api_route(
path="/constraints",
endpoint=self.get_product_constraints,
name=f"{self.root_router.name}:{self.product.id}:get-constraints",
methods=["GET"],
summary="Get constraints for the product",
tags=["Products"],
)
self.add_api_route(
path="/order-parameters",
endpoint=self.get_product_order_parameters,
name=f"{self.root_router.name}:{self.product.id}:get-order-parameters",
methods=["GET"],
summary="Get order parameters for the product",
tags=["Products"],
)
# This wraps `self.create_order` to explicitly parameterize `OrderRequest`
# for this Product. This must be done programmatically instead of with a type
# annotation because it's setting the type dynamically instead of statically, and
# pydantic needs this type annotation when doing object conversion. This cannot be done
# directly to `self.create_order` because doing it there changes
# the annotation on every `ProductRouter` instance's `create_order`, not just
# this one's.
async def _create_order(
payload: OrderPayload,
request: Request,
response: Response,
) -> Order:
return await self.create_order(payload, request, response)
_create_order.__annotations__["payload"] = OrderPayload[
self.product.order_parameters # type: ignore
]
self.add_api_route(
path="/orders",
endpoint=_create_order,
name=f"{self.root_router.name}:{self.product.id}:create-order",
methods=["POST"],
response_class=GeoJSONResponse,
status_code=status.HTTP_201_CREATED,
summary="Create an order for the product",
tags=["Products"],
)
def get_product(self, request: Request) -> Product:
return self.product.with_links(
links=[
Link(
href=str(
request.url_for(
f"{self.root_router.name}:{self.product.id}:get-product",
),
),
rel="self",
type=TYPE_JSON,
),
Link(
href=str(
request.url_for(
f"{self.root_router.name}:{self.product.id}:get-constraints",
),
),
rel="constraints",
type=TYPE_JSON,
),
Link(
href=str(
request.url_for(
f"{self.root_router.name}:{self.product.id}:get-order-parameters",
),
),
rel="order-parameters",
type=TYPE_JSON,
),
Link(
href=str(
request.url_for(
f"{self.root_router.name}:{self.product.id}:search-opportunities",
),
),
rel="opportunities",
type=TYPE_JSON,
),
Link(
href=str(
request.url_for(
f"{self.root_router.name}:{self.product.id}:create-order",
),
),
rel="create-order",
type=TYPE_JSON,
),
],
)
async def search_opportunities(
self,
search: OpportunityRequest,
request: Request,
) -> OpportunityCollection:
"""
Explore the opportunities available for a particular set of constraints
"""
links: list[Link] = []
match await self.product._search_opportunities(
self,
search,
search.next,
search.limit,
request,
):
case Success((features, Some(pagination_token))):
links.append(self.order_link(request))
search.next = pagination_token
links.append(
self.pagination_link(request, search.model_dump(mode="json"))
)
case Success((features, Nothing)): # noqa: F841
links.append(self.order_link(request))
case Failure(e) if isinstance(e, ConstraintsException):
raise e
case Failure(e):
logger.error(
"An error occurred while searching opportunities: %s",
traceback.format_exception(e),
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error searching opportunities",
)
case x:
raise AssertionError(f"Expected code to be unreachable {x}")
return OpportunityCollection(features=features, links=links)
def get_product_constraints(self: Self) -> JsonSchemaModel:
"""
Return supported constraints of a specific product
"""
return self.product.constraints
def get_product_order_parameters(self: Self) -> JsonSchemaModel:
"""
Return supported constraints of a specific product
"""
return self.product.order_parameters
async def create_order(
self, payload: OrderPayload, request: Request, response: Response
) -> Order:
"""
Create a new order.
"""
match await self.product.create_order(
self,
payload,
request,
):
case Success(order):
self.root_router.add_order_links(order, request)
location = str(self.root_router.generate_order_href(request, order.id))
response.headers["Location"] = location
return order
case Failure(e) if isinstance(e, ConstraintsException):
raise e
case Failure(e):
logger.error(
"An error occurred while creating order: %s",
traceback.format_exception(e),
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error creating order",
)
case x:
raise AssertionError(f"Expected code to be unreachable {x}")
def order_link(self, request: Request):
return Link(
href=str(
request.url_for(
f"{self.root_router.name}:{self.product.id}:create-order",
),
),
rel="create-order",
type=TYPE_JSON,
method="POST",
)
def pagination_link(self, request: Request, body: dict[str, str | dict]):
return Link(
href=str(request.url.remove_query_params(keys=["next", "limit"])),
rel="next",
type=TYPE_JSON,
method="POST",
body=body,
)