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

Commit cd6fcd2

Browse files
philvarnerPhil Varner
andauthored
add product order-parameters endpoint and links (#104)
* add product order-parameters endpoint and links * CHANGELOG * add links to product and opportunity-collection * fix mypy warning on unused var --------- Co-authored-by: Phil Varner <[email protected]>
1 parent e1f5d79 commit cd6fcd2

File tree

7 files changed

+132
-17
lines changed

7 files changed

+132
-17
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
### Added
1111

1212
- Conformance endpoint `/conformance` and root body `conformsTo` attribute.
13+
- Endpoint /product/{productId}/order-parameters.
14+
- Links in Product entity to order-parameters and constraints endpoints for that product.
15+
- Add links `opportunities` and `create-order` to Product
16+
- Add link `create-order` to OpportunityCollection
1317

1418
### Changed
1519

bin/server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414
from stapi_fastapi.models.order import Order, OrderCollection
1515
from stapi_fastapi.models.product import (
16+
OrderParameters,
1617
Product,
1718
Provider,
1819
ProviderRole,
@@ -82,6 +83,10 @@ class TestSpotlightProperties(OpportunityPropertiesBase):
8283
off_nadir: int
8384

8485

86+
class TestSpotlightOrderParameters(OrderParameters):
87+
delivery_mechanism: str | None = None
88+
89+
8590
order_db = MockOrderDB()
8691
product_backend = MockProductBackend(order_db)
8792
root_backend = MockRootBackend(order_db)
@@ -102,6 +107,7 @@ class TestSpotlightProperties(OpportunityPropertiesBase):
102107
providers=[provider],
103108
links=[],
104109
constraints=TestSpotlightProperties,
110+
order_parameters=TestSpotlightOrderParameters,
105111
backend=product_backend,
106112
)
107113

src/stapi_fastapi/models/product.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from enum import Enum
55
from typing import TYPE_CHECKING, Literal, Optional, Self
66

7-
from pydantic import AnyHttpUrl, BaseModel, Field
7+
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field
88

99
from stapi_fastapi.models.opportunity import OpportunityPropertiesBase
1010
from stapi_fastapi.models.shared import Link
@@ -32,6 +32,10 @@ def __init__(self, url: AnyHttpUrl | str, **kwargs):
3232
super().__init__(url=url, **kwargs)
3333

3434

35+
class OrderParameters(BaseModel):
36+
model_config = ConfigDict(extra="allow")
37+
38+
3539
class Product(BaseModel):
3640
type_: Literal["Product"] = Field(default="Product", alias="type")
3741
conformsTo: list[str] = Field(default_factory=list)
@@ -45,18 +49,21 @@ class Product(BaseModel):
4549

4650
# we don't want to include these in the model fields
4751
_constraints: type[OpportunityPropertiesBase]
52+
_order_parameters: type[OrderParameters]
4853
_backend: ProductBackend
4954

5055
def __init__(
5156
self,
5257
*args,
5358
backend: ProductBackend,
5459
constraints: type[OpportunityPropertiesBase],
60+
order_parameters: type[OrderParameters],
5561
**kwargs,
5662
) -> None:
5763
super().__init__(*args, **kwargs)
5864
self._backend = backend
5965
self._constraints = constraints
66+
self._order_parameters = order_parameters
6067

6168
@property
6269
def backend(self: Self) -> ProductBackend:
@@ -66,6 +73,10 @@ def backend(self: Self) -> ProductBackend:
6673
def constraints(self: Self) -> type[OpportunityPropertiesBase]:
6774
return self._constraints
6875

76+
@property
77+
def order_parameters(self: Self) -> type[OrderParameters]:
78+
return self._order_parameters
79+
6980
def with_links(self: Self, links: list[Link] | None = None) -> Self:
7081
if not links:
7182
return self

src/stapi_fastapi/routers/product_router.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ def __init__(
6363
tags=["Products"],
6464
)
6565

66+
self.add_api_route(
67+
path="/order-parameters",
68+
endpoint=self.get_product_order_parameters,
69+
name=f"{self.root_router.name}:{self.product.id}:get-order-parameters",
70+
methods=["GET"],
71+
summary="Get order parameters for the product",
72+
tags=["Products"],
73+
)
74+
6675
self.add_api_route(
6776
path="/order",
6877
endpoint=self.create_order,
@@ -86,6 +95,42 @@ def get_product(self, request: Request) -> Product:
8695
rel="self",
8796
type=TYPE_JSON,
8897
),
98+
Link(
99+
href=str(
100+
request.url_for(
101+
f"{self.root_router.name}:{self.product.id}:get-constraints",
102+
),
103+
),
104+
rel="constraints",
105+
type=TYPE_JSON,
106+
),
107+
Link(
108+
href=str(
109+
request.url_for(
110+
f"{self.root_router.name}:{self.product.id}:get-order-parameters",
111+
),
112+
),
113+
rel="order-parameters",
114+
type=TYPE_JSON,
115+
),
116+
Link(
117+
href=str(
118+
request.url_for(
119+
f"{self.root_router.name}:{self.product.id}:search-opportunities",
120+
),
121+
),
122+
rel="opportunities",
123+
type=TYPE_JSON,
124+
),
125+
Link(
126+
href=str(
127+
request.url_for(
128+
f"{self.root_router.name}:{self.product.id}:create-order",
129+
),
130+
),
131+
rel="create-order",
132+
type=TYPE_JSON,
133+
),
89134
],
90135
)
91136

@@ -102,14 +147,33 @@ async def search_opportunities(
102147
except ConstraintsException as exc:
103148
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.detail)
104149

105-
return OpportunityCollection(features=opportunities)
150+
return OpportunityCollection(
151+
features=opportunities,
152+
links=[
153+
Link(
154+
href=str(
155+
request.url_for(
156+
f"{self.root_router.name}:{self.product.id}:create-order",
157+
),
158+
),
159+
rel="create-order",
160+
type=TYPE_JSON,
161+
),
162+
],
163+
)
106164

107165
async def get_product_constraints(self: Self) -> JsonSchemaModel:
108166
"""
109167
Return supported constraints of a specific product
110168
"""
111169
return self.product.constraints
112170

171+
async def get_product_order_parameters(self: Self) -> JsonSchemaModel:
172+
"""
173+
Return supported constraints of a specific product
174+
"""
175+
return self.product.order_parameters
176+
113177
async def create_order(
114178
self, payload: OpportunityRequest, request: Request, response: Response
115179
) -> Order:

tests/conftest.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
OpportunityPropertiesBase,
1616
OpportunityRequest,
1717
)
18-
from stapi_fastapi.models.product import Product, Provider, ProviderRole
18+
from stapi_fastapi.models.product import (
19+
OrderParameters,
20+
Product,
21+
Provider,
22+
ProviderRole,
23+
)
1924
from stapi_fastapi.routers.root_router import RootRouter
2025

2126
from .backends import MockOrderDB, MockProductBackend, MockRootBackend
@@ -26,6 +31,10 @@ class TestSpotlightProperties(OpportunityPropertiesBase):
2631
off_nadir: int
2732

2833

34+
class TestSpotlightOrderParameters(OrderParameters):
35+
delivery_mechanism: str | None = None
36+
37+
2938
@pytest.fixture(scope="session")
3039
def base_url() -> Iterator[str]:
3140
yield "http://stapiserver"
@@ -60,6 +69,7 @@ def mock_product_test_spotlight(
6069
providers=[mock_provider],
6170
links=[],
6271
constraints=TestSpotlightProperties,
72+
order_parameters=TestSpotlightOrderParameters,
6373
backend=product_backend,
6474
)
6575

tests/opportunity_test.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def test_search_opportunities_response(
1616
mock_test_spotlight_opportunities: List[Opportunity],
1717
product_backend: MockProductBackend,
1818
stapi_client: TestClient,
19+
assert_link,
1920
) -> None:
2021
product_backend._opportunities = mock_test_spotlight_opportunities
2122

@@ -50,9 +51,11 @@ def test_search_opportunities_response(
5051

5152
# Validate response status and structure
5253
assert response.status_code == 200, f"Failed for product: {product_id}"
53-
_json = response.json()
54+
body = response.json()
5455

5556
try:
56-
OpportunityCollection(**_json)
57+
_ = OpportunityCollection(**body)
5758
except Exception as _:
5859
pytest.fail("response is not an opportunity collection")
60+
61+
assert_link(f"POST {url}", body, "create-order", f"/products/{product_id}/order")

tests/product_test.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
from fastapi import status
33
from fastapi.testclient import TestClient
44

5-
from .utils import find_link
6-
75

86
def test_products_response(stapi_client: TestClient):
97
res = stapi_client.get("/products")
@@ -21,17 +19,22 @@ def test_products_response(stapi_client: TestClient):
2119
def test_product_response_self_link(
2220
product_id: str,
2321
stapi_client: TestClient,
24-
url_for,
22+
assert_link,
2523
):
2624
res = stapi_client.get(f"/products/{product_id}")
2725
assert res.status_code == status.HTTP_200_OK
2826
assert res.headers["Content-Type"] == "application/json"
2927

30-
data = res.json()
31-
link = find_link(data["links"], "self")
32-
assert link, "GET /products Link[rel=self] should exist"
33-
assert link["type"] == "application/json"
34-
assert link["href"] == url_for(f"/products/{product_id}")
28+
body = res.json()
29+
30+
url = "GET /products"
31+
assert_link(url, body, "self", f"/products/{product_id}")
32+
assert_link(url, body, "constraints", f"/products/{product_id}/constraints")
33+
assert_link(
34+
url, body, "order-parameters", f"/products/{product_id}/order-parameters"
35+
)
36+
assert_link(url, body, "opportunities", f"/products/{product_id}/opportunities")
37+
assert_link(url, body, "create-order", f"/products/{product_id}/order")
3538

3639

3740
@pytest.mark.parametrize("product_id", ["test-spotlight"])
@@ -43,7 +46,21 @@ def test_product_constraints_response(
4346
assert res.status_code == status.HTTP_200_OK
4447
assert res.headers["Content-Type"] == "application/json"
4548

46-
data = res.json()
47-
assert "properties" in data
48-
assert "datetime" in data["properties"]
49-
assert "off_nadir" in data["properties"]
49+
json_schema = res.json()
50+
assert "properties" in json_schema
51+
assert "datetime" in json_schema["properties"]
52+
assert "off_nadir" in json_schema["properties"]
53+
54+
55+
@pytest.mark.parametrize("product_id", ["test-spotlight"])
56+
def test_product_order_parameters_response(
57+
product_id: str,
58+
stapi_client: TestClient,
59+
):
60+
res = stapi_client.get(f"/products/{product_id}/order-parameters")
61+
assert res.status_code == status.HTTP_200_OK
62+
assert res.headers["Content-Type"] == "application/json"
63+
64+
json_schema = res.json()
65+
assert "properties" in json_schema
66+
assert "delivery_mechanism" in json_schema["properties"]

0 commit comments

Comments
 (0)