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

Commit 2d68aed

Browse files
author
Phil Varner
committed
add support for order statuses endpoints
1 parent c523496 commit 2d68aed

File tree

10 files changed

+292
-81
lines changed

10 files changed

+292
-81
lines changed

CHANGELOG.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

88

9+
## [0.4.0] - 2024-12-11
10+
11+
### Added
12+
13+
- Endpoint `/orders/{order_id}/statuses` supporting `GET` for retrieving statuses. The entity returned by this conforms
14+
to the change proposed in [stapi-spec#239](https://github.com/stapi-spec/stapi-spec/pull/239).
15+
16+
- Endpoint `/orders/{order_id}/statuses` supporting `POST` for updating current status
17+
18+
### Changed
19+
20+
- OrderRequest renamed to OrderPayload
21+
22+
### Deprecated
23+
24+
none
25+
26+
### Removed
27+
28+
none
29+
30+
### Fixed
31+
32+
none
33+
34+
### Security
35+
36+
none
37+
938
## [0.4.0] - 2024-12-11
1039

1140
### Added
@@ -44,6 +73,8 @@ none
4473

4574
- OrderStatusCode and ProviderRole are now StrEnum instead of (str, Enum)
4675
- All types using `Result[A, Exception]` have been replace with the equivalent type `ResultE[A]`
76+
- Order and OrderCollection extend _GeoJsonBase instead of Feature and FeatureCollection, to allow for tighter
77+
constraints on fields
4778

4879
### Deprecated
4980

@@ -75,8 +106,6 @@ none
75106
- Order field `id` must be a string, instead of previously allowing int. This is because while an
76107
order ID may an integral numeric value, it is not a "number" in the sense that math will be performed
77108
order ID values, so string represents this better.
78-
- Order and OrderCollection extend _GeoJsonBase instead of Feature and FeatureCollection, to allow for tighter
79-
constraints on fields
80109

81110
### Deprecated
82111

src/stapi_fastapi/backends/product_backend.py

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

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

1212

@@ -27,7 +27,7 @@ async def search_opportunities(
2727
async def create_order(
2828
self,
2929
product_router: ProductRouter,
30-
search: OrderRequest,
30+
search: OrderPayload,
3131
request: Request,
3232
) -> ResultE[Order]:
3333
"""

src/stapi_fastapi/backends/root_backend.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
from returns.maybe import Maybe
55
from returns.result import ResultE
66

7-
from stapi_fastapi.models.order import Order, OrderCollection
7+
from stapi_fastapi.models.order import (
8+
Order,
9+
OrderCollection,
10+
OrderStatus,
11+
OrderStatusPayload,
12+
)
813

914

1015
class RootBackend(Protocol): # pragma: nocover
@@ -21,9 +26,38 @@ async def get_order(self, order_id: str, request: Request) -> ResultE[Maybe[Orde
2126
Should return returns.results.Success[Order] if order is found.
2227
2328
Should return returns.results.Failure[returns.maybe.Nothing] if the order is
24-
not found or if access is denied. If there is an Exception associated with attempting to find the order,
25-
then resturns.results.Failure[returns.maybe.Some[Exception]] should be returned.
29+
not found or if access is denied.
2630
27-
Typically, a Failure[Nothing] will result in a 404 and Failure[Some[Exception]] will resulting in a 500.
31+
A Failure[Exception] will result in a 500.
32+
"""
33+
...
34+
35+
async def get_order_statuses(
36+
self, order_id: str, request: Request
37+
) -> ResultE[list[OrderStatus]]:
38+
"""
39+
Get statuses for order with `order_id`.
40+
41+
Should return returns.results.Success[list[OrderStatus]] if order is found.
42+
43+
Should return returns.results.Failure[Exception] if the order is
44+
not found or if access is denied.
45+
46+
A Failure[Exception] will result in a 500.
47+
"""
48+
...
49+
50+
async def set_order_status(
51+
self, order_id: str, payload: OrderStatusPayload, request: Request
52+
) -> ResultE[bool]:
53+
"""
54+
Get statuses for order with `order_id`.
55+
56+
Should return returns.results.Success[list[OrderStatus]] if order is found.
57+
58+
Should return returns.results.Failure[Exception] if the order is
59+
not found or if access is denied.
60+
61+
A Failure[Exception] will result in a 500.
2862
"""
2963
...

src/stapi_fastapi/models/order.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,6 @@ class OrderParameters(BaseModel):
2929
ORP = TypeVar("ORP", bound=OrderParameters)
3030

3131

32-
class OrderRequest(BaseModel, Generic[ORP]):
33-
datetime: DatetimeInterval
34-
geometry: Geometry
35-
# TODO: validate the CQL2 filter?
36-
filter: CQL2Filter | None = None
37-
38-
order_parameters: ORP
39-
40-
model_config = ConfigDict(strict=True)
41-
42-
4332
class OrderStatusCode(StrEnum):
4433
received = "received"
4534
accepted = "accepted"
@@ -56,6 +45,11 @@ class OrderStatus(BaseModel):
5645
links: list[Link] = Field(default_factory=list)
5746

5847

48+
class OrderStatuses(BaseModel):
49+
statuses: list[OrderStatus]
50+
links: list[Link] = Field(default_factory=list)
51+
52+
5953
class OrderSearchParameters(BaseModel):
6054
datetime: DatetimeInterval
6155
geometry: Geometry
@@ -115,3 +109,22 @@ def __len__(self) -> int:
115109
def __getitem__(self, index: int) -> Order:
116110
"""get feature at a given index"""
117111
return self.features[index]
112+
113+
114+
class OrderPayload(BaseModel, Generic[ORP]):
115+
datetime: DatetimeInterval
116+
geometry: Geometry
117+
# TODO: validate the CQL2 filter?
118+
filter: CQL2Filter | None = None
119+
120+
order_parameters: ORP
121+
122+
model_config = ConfigDict(strict=True)
123+
124+
125+
class OrderStatusPayload(BaseModel):
126+
status_code: OrderStatusCode
127+
reason_code: Optional[str] = None
128+
reason_text: Optional[str] = None
129+
130+
model_config = ConfigDict(strict=True)

src/stapi_fastapi/routers/product_router.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
OpportunityCollection,
1414
OpportunityRequest,
1515
)
16-
from stapi_fastapi.models.order import Order, OrderRequest
16+
from stapi_fastapi.models.order import Order, OrderPayload
1717
from stapi_fastapi.models.product import Product
1818
from stapi_fastapi.models.shared import Link
1919
from stapi_fastapi.responses import GeoJSONResponse
@@ -85,13 +85,13 @@ def __init__(
8585
# the annotation on every `ProductRouter` instance's `create_order`, not just
8686
# this one's.
8787
async def _create_order(
88-
payload: OrderRequest,
88+
payload: OrderPayload,
8989
request: Request,
9090
response: Response,
9191
) -> Order:
9292
return await self.create_order(payload, request, response)
9393

94-
_create_order.__annotations__["payload"] = OrderRequest[
94+
_create_order.__annotations__["payload"] = OrderPayload[
9595
self.product.order_parameters # type: ignore
9696
]
9797

@@ -203,7 +203,7 @@ def get_product_order_parameters(self: Self) -> JsonSchemaModel:
203203
return self.product.order_parameters
204204

205205
async def create_order(
206-
self, payload: OrderRequest, request: Request, response: Response
206+
self, payload: OrderPayload, request: Request, response: Response
207207
) -> Order:
208208
"""
209209
Create a new order.

src/stapi_fastapi/routers/root_router.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
33

44
from fastapi import APIRouter, HTTPException, Request, status
55
from fastapi.datastructures import URL
6+
from fastapi.responses import Response
67
from returns.maybe import Maybe, Some
78
from returns.result import Failure, Success
89

910
from stapi_fastapi.backends.root_backend import RootBackend
1011
from stapi_fastapi.constants import TYPE_GEOJSON, TYPE_JSON
1112
from stapi_fastapi.exceptions import NotFoundException
1213
from stapi_fastapi.models.conformance import CORE, Conformance
13-
from stapi_fastapi.models.order import Order, OrderCollection
14+
from stapi_fastapi.models.order import (
15+
Order,
16+
OrderCollection,
17+
OrderStatuses,
18+
OrderStatusPayload,
19+
)
1420
from stapi_fastapi.models.product import Product, ProductsCollection
1521
from stapi_fastapi.models.root import RootResponse
1622
from stapi_fastapi.models.shared import Link
@@ -84,6 +90,22 @@ def __init__(
8490
tags=["Orders"],
8591
)
8692

93+
self.add_api_route(
94+
"/orders/{order_id}/statuses",
95+
self.get_order_statuses,
96+
methods=["GET"],
97+
name=f"{self.name}:list-order-statuses",
98+
tags=["Orders"],
99+
)
100+
101+
self.add_api_route(
102+
"/orders/{order_id}/statuses",
103+
self.set_order_status,
104+
methods=["POST"],
105+
name=f"{self.name}:set-order-status",
106+
tags=["Orders"],
107+
)
108+
87109
def get_root(self, request: Request) -> RootResponse:
88110
return RootResponse(
89111
id="STAPI API",
@@ -168,8 +190,20 @@ async def get_order(self: Self, order_id: str, request: Request) -> Order:
168190
"""
169191
match await self.backend.get_order(order_id, request):
170192
case Success(Some(order)):
171-
order.links.append(
172-
Link(href=str(request.url), rel="self", type=TYPE_GEOJSON)
193+
order.links.extend(
194+
[
195+
Link(href=str(request.url), rel="self", type=TYPE_GEOJSON),
196+
Link(
197+
href=str(
198+
request.url_for(
199+
f"{self.name}:list-order-statuses",
200+
order_id=order_id,
201+
)
202+
),
203+
rel="monitor",
204+
type=TYPE_JSON,
205+
),
206+
]
173207
)
174208
return order
175209
case Success(Maybe.empty):
@@ -185,6 +219,52 @@ async def get_order(self: Self, order_id: str, request: Request) -> Order:
185219
case _:
186220
raise AssertionError("Expected code to be unreachable")
187221

222+
async def get_order_statuses(
223+
self: Self, order_id: str, request: Request
224+
) -> OrderStatuses:
225+
match await self.backend.get_order_statuses(order_id, request):
226+
case Success(statuses):
227+
return OrderStatuses(
228+
statuses=statuses,
229+
links=[
230+
Link(
231+
href=str(
232+
request.url_for(
233+
f"{self.name}:list-order-statuses",
234+
order_id=order_id,
235+
)
236+
),
237+
rel="self",
238+
type=TYPE_JSON,
239+
)
240+
],
241+
)
242+
case Failure(e):
243+
logging.exception(
244+
"An error occurred while retrieving order statuses", e
245+
)
246+
raise HTTPException(
247+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
248+
detail="Error finding Order Statuses",
249+
)
250+
case _:
251+
raise AssertionError("Expected code to be unreachable")
252+
253+
async def set_order_status(
254+
self: Self, order_id: str, payload: OrderStatusPayload, request: Request
255+
) -> Response:
256+
match await self.backend.set_order_status(order_id, payload, request):
257+
case Success(_):
258+
return Response(status_code=status.HTTP_202_ACCEPTED)
259+
case Failure(e):
260+
logging.exception("An error occurred while setting order status", e)
261+
raise HTTPException(
262+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
263+
detail="Error setting Order Status",
264+
)
265+
case _:
266+
raise AssertionError("Expected code to be unreachable")
267+
188268
def add_product(self: Self, product: Product) -> None:
189269
# Give the include a prefix from the product router
190270
product_router = ProductRouter(product, self)

0 commit comments

Comments
 (0)