Skip to content

Commit b9d7951

Browse files
gadomskiTrevor Skaggs
andauthored
feat: add opportunity search statuses (#78)
## What I'm changing - Closes #72 ## How I did it - Checked out https://github.com/stapi-spec/stapi-spec/blob/main/README.md#endpoints for the async endpoints - Saw that the only one we were missing was `GET /searches/opportunities/{searchRecordId}/statuses` - Implemented it ## Checklist - [x] Tests pass: `uv run pytest` - [x] Checks pass: `uv run pre-commit --all-files` - [x] CHANGELOG is updated (if necessary) --------- Co-authored-by: Trevor Skaggs <[email protected]>
1 parent 1efe04f commit b9d7951

File tree

8 files changed

+108
-84
lines changed

8 files changed

+108
-84
lines changed

stapi-fastapi/CHANGELOG.md

Lines changed: 15 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6-
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
5+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
76

8-
## unreleased
7+
## Unreleased
98

109
### Fixed
1110

@@ -14,14 +13,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1413
## Added
1514

1615
- Add constants for route names to be used in link href generation
16+
- Opportunity search statuses ([#78](https://github.com/stapi-spec/pystapi/pull/78))
1717
- Conformance url to product ([#85](https://github.com/stapi-spec/pystapi/pull/85))
1818

1919
## Changed
2020

2121
- stapi-fastapi is now using stapi-pydantic models, deduplicating code
2222
- Product in stapi-fastapi is now subclass of Product from stapi-pydantic
2323

24-
## [v0.6.0] - 2025-02-11
24+
## [0.6.0] - 2025-02-11
2525

2626
### Added
2727

@@ -53,7 +53,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
5353

5454
- Opportunities Search result now has the search body in the `create-order` link.
5555

56-
## [v0.5.0] - 2025-01-08
56+
## [0.5.0] - 2025-01-08
5757

5858
### Added
5959

@@ -66,10 +66,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
6666

6767
- OrderRequest renamed to OrderPayload
6868

69-
### Deprecated
70-
71-
none
72-
7369
### Removed
7470

7571
- Endpoint `/orders/{order_id}/statuses` supporting `POST` for updating current status was added and then
@@ -80,42 +76,14 @@ none
8076

8177
- Exception logging
8278

83-
### Security
84-
85-
none
86-
87-
## [v0.4.0] - 2024-12-11
88-
89-
### Added
90-
91-
none
79+
## [0.4.0] - 2024-12-11
9280

9381
### Changed
9482

9583
- The concepts of Opportunity search Constraint and Opportunity search result Opportunity Properties are now separate,
9684
recognizing that they have related attributes, but not neither the same attributes or the same values for those attributes.
9785

98-
### Deprecated
99-
100-
none
101-
102-
### Removed
103-
104-
none
105-
106-
### Fixed
107-
108-
none
109-
110-
### Security
111-
112-
none
113-
114-
## [v0.3.0] - 2024-12-6
115-
116-
### Added
117-
118-
none
86+
## [0.3.0] - 2024-12-6
11987

12088
### Changed
12189

@@ -124,27 +92,7 @@ none
12492
- Order and OrderCollection extend \_GeoJsonBase instead of Feature and FeatureCollection, to allow for tighter
12593
constraints on fields
12694

127-
### Deprecated
128-
129-
none
130-
131-
### Removed
132-
133-
none
134-
135-
### Fixed
136-
137-
none
138-
139-
### Security
140-
141-
none
142-
143-
## [v0.2.0] - 2024-11-23
144-
145-
### Added
146-
147-
none
95+
## [0.2.0] - 2024-11-23
14896

14997
### Changed
15098

@@ -155,23 +103,7 @@ none
155103
order ID may an integral numeric value, it is not a "number" in the sense that math will be performed
156104
order ID values, so string represents this better.
157105

158-
### Deprecated
159-
160-
none
161-
162-
### Removed
163-
164-
none
165-
166-
### Fixed
167-
168-
none
169-
170-
### Security
171-
172-
none
173-
174-
## [v0.1.0] - 2024-11-15
106+
## [0.1.0] - 2024-11-15
175107

176108
Initial release
177109

@@ -185,10 +117,9 @@ Initial release
185117
- Add links `opportunities` and `create-order` to Product
186118
- Add link `create-order` to OpportunityCollection
187119

188-
<!-- [unreleased]: https://github.com/stapi-spec/stapi-fastapi/compare/v0.5.0...main -->
189-
[v0.6.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.6.0
190-
[v0.5.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.5.0
191-
[v0.4.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.4.0
192-
[v0.3.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.3.0
193-
[v0.2.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.2.0
194-
[v0.1.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.1.0
120+
[0.6.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.6.0
121+
[0.5.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.5.0
122+
[0.4.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.4.0
123+
[0.3.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.3.0
124+
[0.2.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.2.0
125+
[0.1.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.1.0

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from returns.result import ResultE
77
from stapi_pydantic import (
88
OpportunitySearchRecord,
9+
OpportunitySearchStatus,
910
Order,
1011
OrderStatus,
1112
)
@@ -109,3 +110,23 @@
109110
if access is denied.
110111
- Returning returns.result.Failure[Exception] will result in a 500.
111112
"""
113+
114+
GetOpportunitySearchRecordStatuses = Callable[
115+
[str, Request], Coroutine[Any, Any, ResultE[Maybe[list[OpportunitySearchStatus]]]]
116+
]
117+
"""
118+
Type alias for an async function that gets the statuses of a OpportunitySearchRecord with
119+
`search_record_id`.
120+
121+
Args:
122+
search_record_id (str): The ID of the OpportunitySearchRecord.
123+
request (Request): FastAPI's Request object.
124+
125+
Returns:
126+
- Should return
127+
returns.result.Success[returns.maybe.Some[list[OpportunitySearchStatus]]] if
128+
the search record is found.
129+
- Should return returns.result.Success[returns.maybe.Nothing] if the search record is not found or
130+
if access is denied.
131+
- Returning returns.result.Failure[Exception] will result in a 500.
132+
"""

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
Link,
1212
OpportunitySearchRecord,
1313
OpportunitySearchRecords,
14+
OpportunitySearchStatus,
1415
Order,
1516
OrderCollection,
1617
OrderStatus,
@@ -22,6 +23,7 @@
2223
from stapi_fastapi.backends.root_backend import (
2324
GetOpportunitySearchRecord,
2425
GetOpportunitySearchRecords,
26+
GetOpportunitySearchRecordStatuses,
2527
GetOrder,
2628
GetOrders,
2729
GetOrderStatuses,
@@ -35,6 +37,7 @@
3537
from stapi_fastapi.routers.route_names import (
3638
CONFORMANCE,
3739
GET_OPPORTUNITY_SEARCH_RECORD,
40+
GET_OPPORTUNITY_SEARCH_RECORD_STATUSES,
3841
GET_ORDER,
3942
LIST_OPPORTUNITY_SEARCH_RECORDS,
4043
LIST_ORDER_STATUSES,
@@ -54,6 +57,7 @@ def __init__(
5457
get_order_statuses: GetOrderStatuses, # type: ignore
5558
get_opportunity_search_records: GetOpportunitySearchRecords | None = None,
5659
get_opportunity_search_record: GetOpportunitySearchRecord | None = None,
60+
get_opportunity_search_record_statuses: GetOpportunitySearchRecordStatuses | None = None,
5761
conformances: list[str] = [CORE],
5862
name: str = "root",
5963
openapi_endpoint_name: str = "openapi",
@@ -76,6 +80,7 @@ def __init__(
7680
self._get_order_statuses = get_order_statuses
7781
self.__get_opportunity_search_records = get_opportunity_search_records
7882
self.__get_opportunity_search_record = get_opportunity_search_record
83+
self.__get_opportunity_search_record_statuses = get_opportunity_search_record_statuses
7984
self.conformances = conformances
8085
self.name = name
8186
self.openapi_endpoint_name = openapi_endpoint_name
@@ -157,6 +162,15 @@ def __init__(
157162
tags=["Opportunities"],
158163
)
159164

165+
self.add_api_route(
166+
"/searches/opportunities/{search_record_id}/statuses",
167+
self.get_opportunity_search_record_statuses,
168+
methods=["GET"],
169+
name=f"{self.name}:{GET_OPPORTUNITY_SEARCH_RECORD_STATUSES}",
170+
summary="Get an Opportunity Search Record statuses by ID",
171+
tags=["Opportunities"],
172+
)
173+
160174
def get_root(self, request: Request) -> RootResponse:
161175
links = [
162176
Link(
@@ -415,6 +429,30 @@ async def get_opportunity_search_record(self, search_record_id: str, request: Re
415429
case _:
416430
raise AssertionError("Expected code to be unreachable")
417431

432+
async def get_opportunity_search_record_statuses(
433+
self, search_record_id: str, request: Request
434+
) -> list[OpportunitySearchStatus]:
435+
"""
436+
Get the Opportunity Search Record statuses with `search_record_id`.
437+
"""
438+
match await self._get_opportunity_search_record_statuses(search_record_id, request):
439+
case Success(Some(search_record_statuses)):
440+
return search_record_statuses # type: ignore
441+
case Success(Maybe.empty):
442+
raise NotFoundException("Opportunity Search Record not found")
443+
case Failure(e):
444+
logger.error(
445+
"An error occurred while retrieving opportunity search record statuses '%s': %s",
446+
search_record_id,
447+
traceback.format_exception(e),
448+
)
449+
raise HTTPException(
450+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
451+
detail="Error finding Opportunity Search Record statuses",
452+
)
453+
case _:
454+
raise AssertionError("Expected code to be unreachable")
455+
418456
def generate_opportunity_search_record_href(self, request: Request, search_record_id: str) -> URL:
419457
return request.url_for(
420458
f"{self.name}:{GET_OPPORTUNITY_SEARCH_RECORD}",
@@ -442,6 +480,12 @@ def _get_opportunity_search_record(self) -> GetOpportunitySearchRecord:
442480
raise AttributeError("Root router does not support async opportunity search")
443481
return self.__get_opportunity_search_record
444482

483+
@property
484+
def _get_opportunity_search_record_statuses(self) -> GetOpportunitySearchRecordStatuses:
485+
if not self.__get_opportunity_search_record_statuses:
486+
raise AttributeError("Root router does not support async opportunity search")
487+
return self.__get_opportunity_search_record_statuses
488+
445489
@property
446490
def supports_async_opportunity_search(self) -> bool:
447491
return (

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# Opportunity
1313
LIST_OPPORTUNITY_SEARCH_RECORDS = "list-opportunity-search-records"
1414
GET_OPPORTUNITY_SEARCH_RECORD = "get-opportunity-search-record"
15+
GET_OPPORTUNITY_SEARCH_RECORD_STATUSES = "get-opportunity-search-record-statuses"
1516
SEARCH_OPPORTUNITIES = "search-opportunities"
1617
GET_OPPORTUNITY_COLLECTION = "get-opportunity-collection"
1718

stapi-fastapi/tests/backends.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,14 @@ async def mock_get_opportunity_search_record(
196196
return Success(Maybe.from_optional(request.state._opportunities_db.get_search_record(search_record_id)))
197197
except Exception as e:
198198
return Failure(e)
199+
200+
201+
async def mock_get_opportunity_search_record_statuses(
202+
search_record_id: str, request: Request
203+
) -> ResultE[Maybe[list[OpportunitySearchStatus]]]:
204+
try:
205+
return Success(
206+
Maybe.from_optional(request.state._opportunities_db.get_search_record_statuses(search_record_id))
207+
)
208+
except Exception as e:
209+
return Failure(e)

stapi-fastapi/tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from .backends import (
2020
mock_get_opportunity_search_record,
21+
mock_get_opportunity_search_record_statuses,
2122
mock_get_opportunity_search_records,
2223
mock_get_order,
2324
mock_get_order_statuses,
@@ -110,6 +111,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[dict[str, Any]]:
110111
get_order_statuses=mock_get_order_statuses,
111112
get_opportunity_search_records=mock_get_opportunity_search_records,
112113
get_opportunity_search_record=mock_get_opportunity_search_record,
114+
get_opportunity_search_record_statuses=mock_get_opportunity_search_record_statuses,
113115
conformances=[CORE, OPPORTUNITIES, ASYNC_OPPORTUNITIES],
114116
)
115117

stapi-fastapi/tests/shared.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
OpportunityCollection,
1919
OpportunityProperties,
2020
OpportunitySearchRecord,
21+
OpportunitySearchStatus,
2122
Order,
2223
OrderParameters,
2324
OrderStatus,
@@ -68,6 +69,12 @@ def __init__(self) -> None:
6869
def get_search_record(self, search_id: str) -> OpportunitySearchRecord | None:
6970
return deepcopy(self._search_records.get(search_id))
7071

72+
def get_search_record_statuses(self, search_id: str) -> list[OpportunitySearchStatus] | None:
73+
if search_record := self.get_search_record(search_id):
74+
return [deepcopy(search_record.status)]
75+
else:
76+
return None
77+
7178
def get_search_records(self) -> list[OpportunitySearchRecord]:
7279
return deepcopy(list(self._search_records.values()))
7380

stapi-fastapi/tests/test_opportunity_async.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,13 @@ def test_async_opportunity_search_to_completion(
231231
retrieved_search_record = OpportunitySearchRecord(**retrieved_search_response.json())
232232
assert retrieved_search_record.status.status_code == OpportunitySearchStatusCode.completed
233233

234+
url = f"/searches/opportunities/{search_record.id}/statuses"
235+
retrieved_statuses_response = stapi_client_async_opportunity.get(url)
236+
assert retrieved_statuses_response.status_code == 200
237+
retrieved_statuses = [OpportunitySearchStatus(**d) for d in retrieved_statuses_response.json()]
238+
assert len(retrieved_statuses) >= 1
239+
assert retrieved_statuses[-1].status_code == OpportunitySearchStatusCode.completed
240+
234241
# Verify we can retrieve the OpportunityCollection from the
235242
# OpportunitySearchRecord's `opportunities` link; verify the retrieved
236243
# OpportunityCollection contains an order link and a link pointing back to the

0 commit comments

Comments
 (0)