Skip to content

Commit 12644af

Browse files
committed
proposed rework of conformances
1 parent f7db6af commit 12644af

5 files changed

Lines changed: 59 additions & 40 deletions

File tree

stapi-fastapi/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
2222
- stapi-fastapi is now using stapi-pydantic models, deduplicating code
2323
- Product in stapi-fastapi is now subclass of Product from stapi-pydantic
2424
- How conformances work ([#90](https://github.com/stapi-spec/pystapi/pull/90))
25+
- Async behaviors align with spec changes ([#90](https://github.com/stapi-spec/pystapi/pull/90))
2526

2627
## [0.6.0] - 2025-02-11
2728

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

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
Product as ProductPydantic,
3434
)
3535

36+
from stapi_fastapi.conformance import PRODUCT as PRODUCT_CONFORMACES
3637
from stapi_fastapi.constants import TYPE_JSON
3738
from stapi_fastapi.errors import NotFoundError, QueryablesError
3839
from stapi_fastapi.models.product import Product
@@ -66,6 +67,23 @@ def get_prefer(prefer: str | None = Header(None)) -> str | None:
6667
return Prefer(prefer)
6768

6869

70+
def build_conformances(product: Product, root_router: RootRouter) -> list[str]:
71+
# FIXME we can make this check more robust
72+
if not any(conformance.startswith("https://geojson.org/schema/") for conformance in product.conformsTo):
73+
raise ValueError("product conformance does not contain at least one geojson conformance")
74+
75+
conformances = set(product.conformsTo)
76+
77+
if product.supports_opportunity_search:
78+
conformances.add(PRODUCT_CONFORMACES.opportunities)
79+
80+
if product.supports_async_opportunity_search and root_router.supports_async_opportunity_search:
81+
conformances.add(PRODUCT_CONFORMACES.opportunities)
82+
conformances.add(PRODUCT_CONFORMACES.opportunities_async)
83+
84+
return list(conformances)
85+
86+
6987
class ProductRouter(APIRouter):
7088
# FIXME ruff is complaining that the init is too complex
7189
def __init__( # noqa
@@ -77,17 +95,9 @@ def __init__( # noqa
7795
) -> None:
7896
super().__init__(*args, **kwargs)
7997

80-
if root_router.supports_async_opportunity_search and not product.supports_async_opportunity_search:
81-
raise ValueError(
82-
f"Product '{product.id}' must support async opportunity search since the root router does",
83-
)
84-
85-
# FIXME we can make this check more robust
86-
if not any(conformance.startswith("https://geojson.org/schema/") for conformance in product.conformsTo):
87-
raise ValueError("product conformance does not contain at least one geojson conformance")
88-
8998
self.product = product
9099
self.root_router = root_router
100+
self.conformances = build_conformances(product, root_router)
91101

92102
self.add_api_route(
93103
path="",
@@ -154,7 +164,9 @@ async def _create_order(
154164
tags=["Products"],
155165
)
156166

157-
if product.supports_opportunity_search or root_router.supports_async_opportunity_search:
167+
if product.supports_opportunity_search or (
168+
self.product.supports_async_opportunity_search and self.root_router.supports_async_opportunity_search
169+
):
158170
self.add_api_route(
159171
path="/opportunities",
160172
endpoint=self.search_opportunities,
@@ -176,7 +188,7 @@ async def _create_order(
176188
tags=["Products"],
177189
)
178190

179-
if root_router.supports_async_opportunity_search:
191+
if product.supports_async_opportunity_search and root_router.supports_async_opportunity_search:
180192
self.add_api_route(
181193
path="/opportunities/{opportunity_collection_id}",
182194
endpoint=self.get_opportunity_collection,
@@ -237,7 +249,9 @@ def get_product(self, request: Request) -> ProductPydantic:
237249
),
238250
]
239251

240-
if self.product.supports_opportunity_search or self.root_router.supports_async_opportunity_search:
252+
if self.product.supports_opportunity_search or (
253+
self.product.supports_async_opportunity_search and self.root_router.supports_async_opportunity_search
254+
):
241255
links.append(
242256
Link(
243257
href=str(
@@ -263,9 +277,9 @@ async def search_opportunities(
263277
Explore the opportunities available for a particular set of queryables
264278
"""
265279
# sync
266-
if not self.root_router.supports_async_opportunity_search or (
267-
prefer is Prefer.wait and self.product.supports_opportunity_search
268-
):
280+
if not (
281+
self.root_router.supports_async_opportunity_search and self.product.supports_async_opportunity_search
282+
) or (prefer is Prefer.wait and self.product.supports_opportunity_search):
269283
return await self.search_opportunities_sync(
270284
search,
271285
request,
@@ -362,7 +376,7 @@ def get_product_conformance(self) -> Conformance:
362376
"""
363377
Return conformance urls of a specific product
364378
"""
365-
return Conformance.model_validate({"conforms_to": self.product.conformsTo})
379+
return Conformance.model_validate({"conforms_to": self.conformances})
366380

367381
def get_product_queryables(self) -> JsonSchemaModel:
368382
"""

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

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __init__(
5454
self,
5555
get_orders: GetOrders,
5656
get_order: GetOrder,
57-
get_order_statuses: GetOrderStatuses, # type: ignore
57+
get_order_statuses: GetOrderStatuses | None = None, # type: ignore
5858
get_opportunity_search_records: GetOpportunitySearchRecords | None = None,
5959
get_opportunity_search_record: GetOpportunitySearchRecord | None = None,
6060
get_opportunity_search_record_statuses: GetOpportunitySearchRecordStatuses | None = None,
@@ -67,18 +67,14 @@ def __init__(
6767
) -> None:
6868
super().__init__(*args, **kwargs)
6969

70-
api_conformances = API_CONFORMANCE.all()
71-
for conformance in conformances:
72-
if conformance not in api_conformances:
73-
raise ValueError(f"{conformance} is not a valid API conformance")
70+
_conformances = set(conformances)
7471

7572
self._get_orders = get_orders
7673
self._get_order = get_order
77-
self._get_order_statuses = get_order_statuses
74+
self.__get_order_statuses = get_order_statuses
7875
self.__get_opportunity_search_records = get_opportunity_search_records
7976
self.__get_opportunity_search_record = get_opportunity_search_record
8077
self.__get_opportunity_search_record_statuses = get_opportunity_search_record_statuses
81-
self.conformances = conformances
8278
self.name = name
8379
self.openapi_endpoint_name = openapi_endpoint_name
8480
self.docs_endpoint_name = docs_endpoint_name
@@ -132,15 +128,18 @@ def __init__(
132128
tags=["Orders"],
133129
)
134130

135-
self.add_api_route(
136-
"/orders/{order_id}/statuses",
137-
self.get_order_statuses,
138-
methods=["GET"],
139-
name=f"{self.name}:{LIST_ORDER_STATUSES}",
140-
tags=["Orders"],
141-
)
131+
if self.get_order_statuses is not None:
132+
_conformances.add(API_CONFORMANCE.order_statuses)
133+
self.add_api_route(
134+
"/orders/{order_id}/statuses",
135+
self.get_order_statuses,
136+
methods=["GET"],
137+
name=f"{self.name}:{LIST_ORDER_STATUSES}",
138+
tags=["Orders"],
139+
)
142140

143-
if API_CONFORMANCE.searches_opportunity in conformances:
141+
if self.supports_async_opportunity_search:
142+
_conformances.add(API_CONFORMANCE.searches_opportunity)
144143
self.add_api_route(
145144
"/searches/opportunities",
146145
self.get_opportunity_search_records,
@@ -159,7 +158,8 @@ def __init__(
159158
tags=["Opportunities"],
160159
)
161160

162-
if API_CONFORMANCE.searches_opportunity_statuses in conformances:
161+
if self.__get_opportunity_search_record_statuses is not None:
162+
_conformances.add(API_CONFORMANCE.searches_opportunity_statuses)
163163
self.add_api_route(
164164
"/searches/opportunities/{search_record_id}/statuses",
165165
self.get_opportunity_search_record_statuses,
@@ -169,6 +169,8 @@ def __init__(
169169
tags=["Opportunities"],
170170
)
171171

172+
self.conformances = list(_conformances)
173+
172174
def get_root(self, request: Request) -> RootResponse:
173175
links = [
174176
Link(
@@ -466,6 +468,12 @@ def opportunity_search_record_self_link(
466468
type=TYPE_JSON,
467469
)
468470

471+
@property
472+
def _get_order_statuses(self) -> GetOrderStatuses: # type: ignore
473+
if not self.__get_order_statuses:
474+
raise AttributeError("Root router does not support order status history")
475+
return self.__get_order_statuses
476+
469477
@property
470478
def _get_opportunity_search_records(self) -> GetOpportunitySearchRecords:
471479
if not self.__get_opportunity_search_records:
@@ -481,13 +489,9 @@ def _get_opportunity_search_record(self) -> GetOpportunitySearchRecord:
481489
@property
482490
def _get_opportunity_search_record_statuses(self) -> GetOpportunitySearchRecordStatuses:
483491
if not self.__get_opportunity_search_record_statuses:
484-
raise AttributeError("Root router does not support async opportunity search")
492+
raise AttributeError("Root router does not support async opportunity search status history")
485493
return self.__get_opportunity_search_record_statuses
486494

487495
@property
488496
def supports_async_opportunity_search(self) -> bool:
489-
return (
490-
API_CONFORMANCE.searches_opportunity in self.conformances
491-
and self._get_opportunity_search_records is not None
492-
and self._get_opportunity_search_record is not None
493-
)
497+
return self.__get_opportunity_search_records is not None and self.__get_opportunity_search_record is not None

stapi-fastapi/tests/test_conformance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def test_conformance(stapi_client: TestClient) -> None:
1111

1212
body = res.json()
1313

14-
assert body["conformsTo"] == [API.core]
14+
assert body["conformsTo"] == [API.core, API.order_statuses]
1515

1616

1717
def test_all() -> None:

stapi-fastapi/tests/test_root.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def test_root(stapi_client: TestClient, assert_link) -> None:
1111

1212
body = res.json()
1313

14-
assert body["conformsTo"] == [API.core]
14+
assert body["conformsTo"] == [API.core, API.order_statuses]
1515

1616
assert_link("GET /", body, "self", "/")
1717
assert_link("GET /", body, "service-description", "/openapi.json")

0 commit comments

Comments
 (0)