Skip to content
This repository was archived by the owner on Apr 2, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 5 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
12 changes: 9 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ 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).

## [Unreleased]

### Changed

- Replaced the root and product backend Protocol classes with Callable type aliases to
enable future changes to make product opportunity searching, product ordering, and/or
asynchronous (stateful) product opportunity searching optional.

## [v0.5.0] - 2025-01-08

Expand Down Expand Up @@ -64,7 +71,6 @@ none

none


## [v0.3.0] - 2024-12-6

### Added
Expand All @@ -75,7 +81,7 @@ none

- OrderStatusCode and ProviderRole are now StrEnum instead of (str, Enum)
- All types using `Result[A, Exception]` have been replace with the equivalent type `ResultE[A]`
- Order and OrderCollection extend _GeoJsonBase instead of Feature and FeatureCollection, to allow for tighter
- Order and OrderCollection extend \_GeoJsonBase instead of Feature and FeatureCollection, to allow for tighter
constraints on fields

### Deprecated
Expand Down Expand Up @@ -140,7 +146,7 @@ Initial release
- Add link `create-order` to OpportunityCollection

[unreleased]: https://github.com/stapi-spec/stapi-fastapi/compare/v0.5.0...main
[v0.4.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.5.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
Expand Down
10 changes: 7 additions & 3 deletions src/stapi_fastapi/__init__.py
Comment thread
pjhartzell marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .backends import ProductBackend, RootBackend
from .backends.product_backend import CreateOrder, SearchOpportunities
from .backends.root_backend import GetOrder, GetOrders, GetOrderStatuses
from .models import (
Link,
OpportunityProperties,
Expand All @@ -9,13 +10,16 @@
from .routers import ProductRouter, RootRouter

__all__ = [
"CreateOrder",
"GetOrder",
"GetOrders",
"GetOrderStatuses",
"Link",
"OpportunityProperties",
"Product",
"ProductBackend",
"ProductRouter",
"Provider",
"ProviderRole",
"RootBackend",
"RootRouter",
"SearchOpportunities",
]
7 changes: 0 additions & 7 deletions src/stapi_fastapi/backends/__init__.py
Comment thread
pjhartzell marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,7 +0,0 @@
from .product_backend import ProductBackend
from .root_backend import RootBackend

__all__ = [
"ProductBackend",
"RootBackend",
]
68 changes: 40 additions & 28 deletions src/stapi_fastapi/backends/product_backend.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Protocol
from typing import Any, Callable, Coroutine

from fastapi import Request
from returns.result import ResultE
Expand All @@ -9,30 +9,42 @@
from stapi_fastapi.models.order import Order, OrderPayload
from stapi_fastapi.routers.product_router import ProductRouter


class ProductBackend(Protocol): # pragma: nocover
async def search_opportunities(
self,
product_router: ProductRouter,
search: OpportunityRequest,
request: Request,
) -> ResultE[list[Opportunity]]:
"""
Search for ordering opportunities for the given search parameters.

Backends must validate search constraints and return
`stapi_fastapi.exceptions.ConstraintsException` if not valid.
"""

async def create_order(
self,
product_router: ProductRouter,
search: OrderPayload,
request: Request,
) -> ResultE[Order]:
"""
Create a new order.

Backends must validate order payload and return
`stapi_fastapi.exceptions.ConstraintsException` if not valid.
Comment thread
pjhartzell marked this conversation as resolved.
"""
SearchOpportunities = Callable[
[ProductRouter, OpportunityRequest, Request],
Coroutine[Any, Any, ResultE[list[Opportunity]]],
]
"""
Type alias for an async function that searches for ordering opportunities for the given
search parameters.

Args:
product_router (ProductRouter): The product router.
search (OpportunityRequest): The search parameters.
request (Request): FastAPI's Request object.

Returns:
- Should return returns.result.Success[list[Opportunity]]
- Returning returns.result.Failure[Exception] will result in a 500.

Backends must validate search constraints and return
returns.result.Failure[stapi_fastapi.exceptions.ConstraintsException] if not valid.
"""

CreateOrder = Callable[
[ProductRouter, OrderPayload, Request], Coroutine[Any, Any, ResultE[Order]]
]
"""
Type alias for an async function that creates a new order.

Args:
product_router (ProductRouter): The product router.
payload (OrderPayload): The order payload.
request (Request): FastAPI's Request object.

Returns:
- Should return returns.result.Success[Order]
- Returning returns.result.Failure[Exception] will result in a 500.

Backends must validate order payload and return
returns.result.Failure[stapi_fastapi.exceptions.ConstraintsException] if not valid.
"""
64 changes: 37 additions & 27 deletions src/stapi_fastapi/backends/root_backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Protocol
from typing import Any, Callable, Coroutine, TypeVar

from fastapi import Request
from returns.maybe import Maybe
Expand All @@ -10,38 +10,48 @@
OrderStatus,
)

GetOrders = Callable[[Request], Coroutine[Any, Any, ResultE[OrderCollection]]]
"""
Type alias for an async function that returns a list of existing Orders.

class RootBackend[T: OrderStatus](Protocol): # pragma: nocover
async def get_orders(self, request: Request) -> ResultE[OrderCollection]:
"""
Return a list of existing orders.
"""
...
Args:
request (Request): FastAPI's Request object.

async def get_order(self, order_id: str, request: Request) -> ResultE[Maybe[Order]]:
"""
Get details for order with `order_id`.
Returns:
- Should return returns.result.Success[OrderCollection]
- Returning returns.result.Failure[Exception] will result in a 500.
"""

Should return returns.results.Success[Order] if order is found.
GetOrder = Callable[[str, Request], Coroutine[Any, Any, ResultE[Maybe[Order]]]]
"""
Type alias for an async function that gets details for the order with `order_id`.

Should return returns.results.Failure[returns.maybe.Nothing] if the order is
not found or if access is denied.
Args:
order_id (str): The order ID.
request (Request): FastAPI's Request object.

A Failure[Exception] will result in a 500.
"""
...
Returns:
- Should return returns.result.Success[returns.maybe.Some[Order]] if order is found.
- Should return returns.result.Success[returns.maybe.Nothing] if the order is not
found or if access is denied.
- Returning returns.result.Failure[Exception] will result in a 500.
"""

async def get_order_statuses(
self, order_id: str, request: Request
) -> ResultE[list[T]]:
"""
Get statuses for order with `order_id`.

Should return returns.results.Success[list[OrderStatus]] if order is found.
T = TypeVar("T", bound=OrderStatus)

Should return returns.results.Failure[Exception] if the order is
not found or if access is denied.

A Failure[Exception] will result in a 500.
"""
...
GetOrderStatuses = Callable[[str, Request], Coroutine[Any, Any, ResultE[list[T]]]]
"""
Type alias for an async function that gets statuses for the order with `order_id`.

Args:
order_id (str): The order ID.
request (Request): FastAPI's Request object.

Returns:
- Should return returns.result.Success[list[OrderStatus]] if order is found.
- Should return returns.result.Failure[Exception] if the order is not found or if
access is denied.
- Returning returns.result.Failure[Exception] will result in a 500.
"""
22 changes: 16 additions & 6 deletions src/stapi_fastapi/models/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
from stapi_fastapi.models.shared import Link

if TYPE_CHECKING:
from stapi_fastapi.backends.product_backend import ProductBackend
from stapi_fastapi.backends.product_backend import (
CreateOrder,
SearchOpportunities,
)


type Constraints = BaseModel
Expand Down Expand Up @@ -50,26 +53,33 @@ class Product(BaseModel):
_constraints: type[Constraints]
_opportunity_properties: type[OpportunityProperties]
_order_parameters: type[OrderParameters]
_backend: ProductBackend
_create_order: CreateOrder
_search_opportunities: SearchOpportunities

def __init__(
self,
*args,
backend: ProductBackend,
create_order: CreateOrder,
search_opportunities: SearchOpportunities,
constraints: type[Constraints],
opportunity_properties: type[OpportunityProperties],
order_parameters: type[OrderParameters],
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self._backend = backend
self._create_order = create_order
self._search_opportunities = search_opportunities
self._constraints = constraints
self._opportunity_properties = opportunity_properties
self._order_parameters = order_parameters

@property
def backend(self: Self) -> ProductBackend:
return self._backend
def create_order(self: Self) -> CreateOrder:
return self._create_order

@property
def search_opportunities(self: Self) -> SearchOpportunities:
return self._search_opportunities

@property
def constraints(self: Self) -> type[Constraints]:
Expand Down
4 changes: 2 additions & 2 deletions src/stapi_fastapi/routers/product_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ async def search_opportunities(
"""
Explore the opportunities available for a particular set of constraints
"""
match await self.product.backend.search_opportunities(self, search, request):
match await self.product.search_opportunities(self, search, request):
case Success(features):
return OpportunityCollection(
features=features,
Expand Down Expand Up @@ -214,7 +214,7 @@ async def create_order(
"""
Create a new order.
"""
match await self.product.backend.create_order(
match await self.product.create_order(
self,
payload,
request,
Expand Down
16 changes: 10 additions & 6 deletions src/stapi_fastapi/routers/root_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from returns.maybe import Maybe, Some
from returns.result import Failure, Success

from stapi_fastapi.backends.root_backend import RootBackend
from stapi_fastapi.backends.root_backend import GetOrder, GetOrders, GetOrderStatuses
from stapi_fastapi.constants import TYPE_GEOJSON, TYPE_JSON
from stapi_fastapi.exceptions import NotFoundException
from stapi_fastapi.models.conformance import CORE, Conformance
Expand All @@ -28,7 +28,9 @@
class RootRouter(APIRouter):
def __init__(
self,
backend: RootBackend,
get_orders: GetOrders,
get_order: GetOrder,
get_order_statuses: GetOrderStatuses,
conformances: list[str] = [CORE],
name: str = "root",
openapi_endpoint_name: str = "openapi",
Expand All @@ -37,7 +39,9 @@ def __init__(
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.backend = backend
self._get_orders = get_orders
self._get_order = get_order
self._get_order_statuses = get_order_statuses
self.name = name
self.conformances = conformances
self.openapi_endpoint_name = openapi_endpoint_name
Expand Down Expand Up @@ -153,7 +157,7 @@ def get_products(self, request: Request) -> ProductsCollection:
)

async def get_orders(self, request: Request) -> OrderCollection:
match await self.backend.get_orders(request):
match await self._get_orders(request):
case Success(orders):
for order in orders:
order.links.append(
Expand Down Expand Up @@ -184,7 +188,7 @@ async def get_order(self: Self, order_id: str, request: Request) -> Order:
"""
Get details for order with `order_id`.
"""
match await self.backend.get_order(order_id, request):
match await self._get_order(order_id, request):
case Success(Some(order)):
self.add_order_links(order, request)
return order
Expand All @@ -206,7 +210,7 @@ async def get_order(self: Self, order_id: str, request: Request) -> Order:
async def get_order_statuses(
self: Self, order_id: str, request: Request
) -> OrderStatuses:
match await self.backend.get_order_statuses(order_id, request):
match await self._get_order_statuses(order_id, request):
case Success(statuses):
return OrderStatuses(
statuses=statuses,
Expand Down
Loading