Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 15 additions & 84 deletions stapi-fastapi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -14,14 +13,15 @@ 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

- 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

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

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

<!-- [unreleased]: https://github.com/stapi-spec/stapi-fastapi/compare/v0.5.0...main -->
[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
21 changes: 21 additions & 0 deletions stapi-fastapi/src/stapi_fastapi/backends/root_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from returns.result import ResultE
from stapi_pydantic import (
OpportunitySearchRecord,
OpportunitySearchStatus,
Order,
OrderStatus,
)
Expand Down Expand Up @@ -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.
"""
44 changes: 44 additions & 0 deletions stapi-fastapi/src/stapi_fastapi/routers/root_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Link,
OpportunitySearchRecord,
OpportunitySearchRecords,
OpportunitySearchStatus,
Order,
OrderCollection,
OrderStatus,
Expand All @@ -22,6 +23,7 @@
from stapi_fastapi.backends.root_backend import (
GetOpportunitySearchRecord,
GetOpportunitySearchRecords,
GetOpportunitySearchRecordStatuses,
GetOrder,
GetOrders,
GetOrderStatuses,
Expand All @@ -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,
Expand All @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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}",
Expand Down Expand Up @@ -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 (
Expand Down
1 change: 1 addition & 0 deletions stapi-fastapi/src/stapi_fastapi/routers/route_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
11 changes: 11 additions & 0 deletions stapi-fastapi/tests/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 2 additions & 0 deletions stapi-fastapi/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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],
)

Expand Down
7 changes: 7 additions & 0 deletions stapi-fastapi/tests/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
OpportunityCollection,
OpportunityProperties,
OpportunitySearchRecord,
OpportunitySearchStatus,
Order,
OrderParameters,
OrderStatus,
Expand Down Expand Up @@ -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()))

Expand Down
7 changes: 7 additions & 0 deletions stapi-fastapi/tests/test_opportunity_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down