diff --git a/stapi-fastapi/CHANGELOG.md b/stapi-fastapi/CHANGELOG.md index 45022b1..0c7517c 100644 --- a/stapi-fastapi/CHANGELOG.md +++ b/stapi-fastapi/CHANGELOG.md @@ -2,10 +2,9 @@ All notable changes to this project will be documented in this file. -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). +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). -## unreleased +## Unreleased ### Fixed @@ -14,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Added - Add constants for route names to be used in link href generation +- Opportunity search statuses ([#78](https://github.com/stapi-spec/pystapi/pull/78)) - Conformance url to product ([#85](https://github.com/stapi-spec/pystapi/pull/85)) ## Changed @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - stapi-fastapi is now using stapi-pydantic models, deduplicating code - Product in stapi-fastapi is now subclass of Product from stapi-pydantic -## [v0.6.0] - 2025-02-11 +## [0.6.0] - 2025-02-11 ### Added @@ -53,7 +53,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Opportunities Search result now has the search body in the `create-order` link. -## [v0.5.0] - 2025-01-08 +## [0.5.0] - 2025-01-08 ### Added @@ -66,10 +66,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - OrderRequest renamed to OrderPayload -### Deprecated - -none - ### Removed - Endpoint `/orders/{order_id}/statuses` supporting `POST` for updating current status was added and then @@ -80,42 +76,14 @@ none - Exception logging -### Security - -none - -## [v0.4.0] - 2024-12-11 - -### Added - -none +## [0.4.0] - 2024-12-11 ### Changed - The concepts of Opportunity search Constraint and Opportunity search result Opportunity Properties are now separate, recognizing that they have related attributes, but not neither the same attributes or the same values for those attributes. -### Deprecated - -none - -### Removed - -none - -### Fixed - -none - -### Security - -none - -## [v0.3.0] - 2024-12-6 - -### Added - -none +## [0.3.0] - 2024-12-6 ### Changed @@ -124,27 +92,7 @@ none - Order and OrderCollection extend \_GeoJsonBase instead of Feature and FeatureCollection, to allow for tighter constraints on fields -### Deprecated - -none - -### Removed - -none - -### Fixed - -none - -### Security - -none - -## [v0.2.0] - 2024-11-23 - -### Added - -none +## [0.2.0] - 2024-11-23 ### Changed @@ -155,23 +103,7 @@ none order ID may an integral numeric value, it is not a "number" in the sense that math will be performed order ID values, so string represents this better. -### Deprecated - -none - -### Removed - -none - -### Fixed - -none - -### Security - -none - -## [v0.1.0] - 2024-11-15 +## [0.1.0] - 2024-11-15 Initial release @@ -185,10 +117,9 @@ Initial release - Add links `opportunities` and `create-order` to Product - Add link `create-order` to OpportunityCollection - -[v0.6.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.6.0 -[v0.5.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.5.0 -[v0.4.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.4.0 -[v0.3.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.3.0 -[v0.2.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.2.0 -[v0.1.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.1.0 +[0.6.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.6.0 +[0.5.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.5.0 +[0.4.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.4.0 +[0.3.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.3.0 +[0.2.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.2.0 +[0.1.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.1.0 diff --git a/stapi-fastapi/src/stapi_fastapi/backends/root_backend.py b/stapi-fastapi/src/stapi_fastapi/backends/root_backend.py index 6661145..000248d 100644 --- a/stapi-fastapi/src/stapi_fastapi/backends/root_backend.py +++ b/stapi-fastapi/src/stapi_fastapi/backends/root_backend.py @@ -6,6 +6,7 @@ from returns.result import ResultE from stapi_pydantic import ( OpportunitySearchRecord, + OpportunitySearchStatus, Order, OrderStatus, ) @@ -109,3 +110,23 @@ if access is denied. - Returning returns.result.Failure[Exception] will result in a 500. """ + +GetOpportunitySearchRecordStatuses = Callable[ + [str, Request], Coroutine[Any, Any, ResultE[Maybe[list[OpportunitySearchStatus]]]] +] +""" +Type alias for an async function that gets the statuses of a OpportunitySearchRecord with +`search_record_id`. + +Args: + search_record_id (str): The ID of the OpportunitySearchRecord. + request (Request): FastAPI's Request object. + +Returns: + - Should return + returns.result.Success[returns.maybe.Some[list[OpportunitySearchStatus]]] if + the search record is found. + - Should return returns.result.Success[returns.maybe.Nothing] if the search record is not found or + if access is denied. + - Returning returns.result.Failure[Exception] will result in a 500. +""" diff --git a/stapi-fastapi/src/stapi_fastapi/routers/root_router.py b/stapi-fastapi/src/stapi_fastapi/routers/root_router.py index d428e24..b7c71be 100644 --- a/stapi-fastapi/src/stapi_fastapi/routers/root_router.py +++ b/stapi-fastapi/src/stapi_fastapi/routers/root_router.py @@ -11,6 +11,7 @@ Link, OpportunitySearchRecord, OpportunitySearchRecords, + OpportunitySearchStatus, Order, OrderCollection, OrderStatus, @@ -22,6 +23,7 @@ from stapi_fastapi.backends.root_backend import ( GetOpportunitySearchRecord, GetOpportunitySearchRecords, + GetOpportunitySearchRecordStatuses, GetOrder, GetOrders, GetOrderStatuses, @@ -35,6 +37,7 @@ from stapi_fastapi.routers.route_names import ( CONFORMANCE, GET_OPPORTUNITY_SEARCH_RECORD, + GET_OPPORTUNITY_SEARCH_RECORD_STATUSES, GET_ORDER, LIST_OPPORTUNITY_SEARCH_RECORDS, LIST_ORDER_STATUSES, @@ -54,6 +57,7 @@ def __init__( get_order_statuses: GetOrderStatuses, # type: ignore get_opportunity_search_records: GetOpportunitySearchRecords | None = None, get_opportunity_search_record: GetOpportunitySearchRecord | None = None, + get_opportunity_search_record_statuses: GetOpportunitySearchRecordStatuses | None = None, conformances: list[str] = [CORE], name: str = "root", openapi_endpoint_name: str = "openapi", @@ -76,6 +80,7 @@ def __init__( self._get_order_statuses = get_order_statuses self.__get_opportunity_search_records = get_opportunity_search_records self.__get_opportunity_search_record = get_opportunity_search_record + self.__get_opportunity_search_record_statuses = get_opportunity_search_record_statuses self.conformances = conformances self.name = name self.openapi_endpoint_name = openapi_endpoint_name @@ -157,6 +162,15 @@ def __init__( tags=["Opportunities"], ) + self.add_api_route( + "/searches/opportunities/{search_record_id}/statuses", + self.get_opportunity_search_record_statuses, + methods=["GET"], + name=f"{self.name}:{GET_OPPORTUNITY_SEARCH_RECORD_STATUSES}", + summary="Get an Opportunity Search Record statuses by ID", + tags=["Opportunities"], + ) + def get_root(self, request: Request) -> RootResponse: links = [ Link( @@ -415,6 +429,30 @@ async def get_opportunity_search_record(self, search_record_id: str, request: Re case _: raise AssertionError("Expected code to be unreachable") + async def get_opportunity_search_record_statuses( + self, search_record_id: str, request: Request + ) -> list[OpportunitySearchStatus]: + """ + Get the Opportunity Search Record statuses with `search_record_id`. + """ + match await self._get_opportunity_search_record_statuses(search_record_id, request): + case Success(Some(search_record_statuses)): + return search_record_statuses # type: ignore + case Success(Maybe.empty): + raise NotFoundException("Opportunity Search Record not found") + case Failure(e): + logger.error( + "An error occurred while retrieving opportunity search record statuses '%s': %s", + search_record_id, + traceback.format_exception(e), + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error finding Opportunity Search Record statuses", + ) + case _: + raise AssertionError("Expected code to be unreachable") + def generate_opportunity_search_record_href(self, request: Request, search_record_id: str) -> URL: return request.url_for( f"{self.name}:{GET_OPPORTUNITY_SEARCH_RECORD}", @@ -442,6 +480,12 @@ def _get_opportunity_search_record(self) -> GetOpportunitySearchRecord: raise AttributeError("Root router does not support async opportunity search") return self.__get_opportunity_search_record + @property + def _get_opportunity_search_record_statuses(self) -> GetOpportunitySearchRecordStatuses: + if not self.__get_opportunity_search_record_statuses: + raise AttributeError("Root router does not support async opportunity search") + return self.__get_opportunity_search_record_statuses + @property def supports_async_opportunity_search(self) -> bool: return ( diff --git a/stapi-fastapi/src/stapi_fastapi/routers/route_names.py b/stapi-fastapi/src/stapi_fastapi/routers/route_names.py index 292bc54..cbde946 100644 --- a/stapi-fastapi/src/stapi_fastapi/routers/route_names.py +++ b/stapi-fastapi/src/stapi_fastapi/routers/route_names.py @@ -12,6 +12,7 @@ # Opportunity LIST_OPPORTUNITY_SEARCH_RECORDS = "list-opportunity-search-records" GET_OPPORTUNITY_SEARCH_RECORD = "get-opportunity-search-record" +GET_OPPORTUNITY_SEARCH_RECORD_STATUSES = "get-opportunity-search-record-statuses" SEARCH_OPPORTUNITIES = "search-opportunities" GET_OPPORTUNITY_COLLECTION = "get-opportunity-collection" diff --git a/stapi-fastapi/tests/backends.py b/stapi-fastapi/tests/backends.py index 11088ea..0bedc8d 100644 --- a/stapi-fastapi/tests/backends.py +++ b/stapi-fastapi/tests/backends.py @@ -196,3 +196,14 @@ async def mock_get_opportunity_search_record( return Success(Maybe.from_optional(request.state._opportunities_db.get_search_record(search_record_id))) except Exception as e: return Failure(e) + + +async def mock_get_opportunity_search_record_statuses( + search_record_id: str, request: Request +) -> ResultE[Maybe[list[OpportunitySearchStatus]]]: + try: + return Success( + Maybe.from_optional(request.state._opportunities_db.get_search_record_statuses(search_record_id)) + ) + except Exception as e: + return Failure(e) diff --git a/stapi-fastapi/tests/conftest.py b/stapi-fastapi/tests/conftest.py index eb59af7..e583f5e 100644 --- a/stapi-fastapi/tests/conftest.py +++ b/stapi-fastapi/tests/conftest.py @@ -18,6 +18,7 @@ from .backends import ( mock_get_opportunity_search_record, + mock_get_opportunity_search_record_statuses, mock_get_opportunity_search_records, mock_get_order, mock_get_order_statuses, @@ -110,6 +111,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[dict[str, Any]]: get_order_statuses=mock_get_order_statuses, get_opportunity_search_records=mock_get_opportunity_search_records, get_opportunity_search_record=mock_get_opportunity_search_record, + get_opportunity_search_record_statuses=mock_get_opportunity_search_record_statuses, conformances=[CORE, OPPORTUNITIES, ASYNC_OPPORTUNITIES], ) diff --git a/stapi-fastapi/tests/shared.py b/stapi-fastapi/tests/shared.py index c639b4b..58aa897 100644 --- a/stapi-fastapi/tests/shared.py +++ b/stapi-fastapi/tests/shared.py @@ -18,6 +18,7 @@ OpportunityCollection, OpportunityProperties, OpportunitySearchRecord, + OpportunitySearchStatus, Order, OrderParameters, OrderStatus, @@ -68,6 +69,12 @@ def __init__(self) -> None: def get_search_record(self, search_id: str) -> OpportunitySearchRecord | None: return deepcopy(self._search_records.get(search_id)) + def get_search_record_statuses(self, search_id: str) -> list[OpportunitySearchStatus] | None: + if search_record := self.get_search_record(search_id): + return [deepcopy(search_record.status)] + else: + return None + def get_search_records(self) -> list[OpportunitySearchRecord]: return deepcopy(list(self._search_records.values())) diff --git a/stapi-fastapi/tests/test_opportunity_async.py b/stapi-fastapi/tests/test_opportunity_async.py index 19f2a36..ea34eb1 100644 --- a/stapi-fastapi/tests/test_opportunity_async.py +++ b/stapi-fastapi/tests/test_opportunity_async.py @@ -231,6 +231,13 @@ def test_async_opportunity_search_to_completion( retrieved_search_record = OpportunitySearchRecord(**retrieved_search_response.json()) assert retrieved_search_record.status.status_code == OpportunitySearchStatusCode.completed + url = f"/searches/opportunities/{search_record.id}/statuses" + retrieved_statuses_response = stapi_client_async_opportunity.get(url) + assert retrieved_statuses_response.status_code == 200 + retrieved_statuses = [OpportunitySearchStatus(**d) for d in retrieved_statuses_response.json()] + assert len(retrieved_statuses) >= 1 + assert retrieved_statuses[-1].status_code == OpportunitySearchStatusCode.completed + # Verify we can retrieve the OpportunityCollection from the # OpportunitySearchRecord's `opportunities` link; verify the retrieved # OpportunityCollection contains an order link and a link pointing back to the