Skip to content
This repository was archived by the owner on Apr 2, 2025. It is now read-only.

Commit 89a50e1

Browse files
authored
Convert backends from Protocol classes to sets of Callables (#132)
* feat: convert backend protocols to callables This change is in preparation for making product opportunity searching and ordering optional and for introducing optional asynchronous opportunity searching (which will require state). A few notable changes to the test suite: - The `backends` module has been removed from the `tests` directory. Its contents were never actually being used. The contents in `conftest` took precedence. - The in-memory storage that was attached to the MockProductBackend class is replaced with a FastAPI lifespan for storing state.
1 parent 67ea960 commit 89a50e1

21 files changed

+607
-620
lines changed

.github/workflows/pr.yml

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,28 @@ jobs:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
12-
python-version: ["3.12"]
12+
python-version: ["3.12", "3.13"]
1313
steps:
1414
- uses: actions/checkout@v4
15-
- run: pipx install poetry==1.7.1
1615
- uses: actions/setup-python@v5
1716
with:
1817
python-version: ${{ matrix.python-version }}
19-
cache: poetry
20-
- name: Install dependencies
21-
run: poetry install --with=dev
22-
- name: Lint code
23-
if: ${{ matrix.python-version == 3.12 }}
18+
- name: Cache dependencies
19+
uses: actions/cache@v3
20+
with:
21+
path: |
22+
~/.cache/pip
23+
.venv
24+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }}
25+
restore-keys: |
26+
${{ runner.os }}-pip-${{ matrix.python-version }}-
27+
${{ runner.os }}-pip-
28+
- name: Install
29+
run: |
30+
python -m pip install poetry==1.7.1
31+
poetry install --with=dev
32+
- name: Lint
2433
run: |
25-
python -m pip install pre-commit
26-
pre-commit run --all-files
27-
- name: Type-check code
28-
if: ${{ matrix.python-version == 3.12 }}
29-
run: poetry run mypy src
30-
- name: Run tests
31-
run: poetry run nox
34+
poetry run pre-commit run --all-files
35+
- name: Test
36+
run: poetry run pytest
Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,36 @@
1-
name: Build Python Package
1+
name: PR Checks
22

33
on:
4-
push:
5-
branches:
6-
- main
74
pull_request:
8-
branches:
9-
- main
10-
release:
11-
types:
12-
- published
5+
branches: ["main"]
136

147
jobs:
15-
build-package:
8+
test:
169
runs-on: ubuntu-latest
17-
environment:
18-
name: pypi
19-
url: https://pypi.org/p/stapi-fastapi
20-
permissions:
21-
id-token: write
10+
strategy:
11+
matrix:
12+
python-version: ["3.12", "3.13"]
2213
steps:
2314
- uses: actions/checkout@v4
24-
- name: Set up Python
25-
uses: actions/setup-python@v5
15+
- uses: actions/setup-python@v5
2616
with:
27-
python-version: "3.12.x"
28-
- name: Install dependencies
17+
python-version: ${{ matrix.python-version }}
18+
- name: Cache dependencies
19+
uses: actions/cache@v3
20+
with:
21+
path: |
22+
~/.cache/pip
23+
.venv
24+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }}
25+
restore-keys: |
26+
${{ runner.os }}-pip-${{ matrix.python-version }}-
27+
${{ runner.os }}-pip-
28+
- name: Install
29+
run: |
30+
python -m pip install poetry==1.7.1
31+
poetry install --with=dev
32+
- name: Lint
2933
run: |
30-
python -m pip install --upgrade pip
31-
pip install build
32-
pip install .
33-
- name: Build package
34-
run: python -m build
35-
- name: Publish package distributions to PyPI
36-
uses: pypa/gh-action-pypi-publish@release/v1
37-
if: startsWith(github.ref, 'refs/tags')
34+
poetry run pre-commit run --all-files
35+
- name: Test
36+
run: poetry run pytest

CHANGELOG.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- Added token-based pagination to `GET /orders`, `GET /products`,
13+
`GET /orders/{order_id}/statuses`, and `POST /products/{product_id}/opportunities`.
14+
15+
### Changed
16+
17+
- Replaced the root and product backend Protocol classes with Callable type aliases to
18+
enable future changes to make product opportunity searching, product ordering, and/or
19+
asynchronous (stateful) product opportunity searching optional.
20+
- Backend methods that support pagination now return tuples to include the pagination
21+
token.
22+
- Moved `OrderCollection` construction from the root backend to the `RootRouter`
23+
`get_orders` method.
824

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

@@ -64,7 +80,6 @@ none
6480

6581
none
6682

67-
6883
## [v0.3.0] - 2024-12-6
6984

7085
### Added
@@ -75,7 +90,7 @@ none
7590

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

8196
### Deprecated
@@ -140,7 +155,7 @@ Initial release
140155
- Add link `create-order` to OpportunityCollection
141156

142157
[unreleased]: https://github.com/stapi-spec/stapi-fastapi/compare/v0.5.0...main
143-
[v0.4.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.5.0
158+
[v0.5.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.5.0
144159
[v0.4.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.4.0
145160
[v0.3.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.3.0
146161
[v0.2.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.2.0

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ STAPI FastAPI provides an `fastapi.APIRouter` which must be included in
1414
`GET`: `'/orders`, `/products`, `/orders/{order_id}/statuses`
1515
`POST`: `/opportunities`.
1616

17-
Pagination is token based and follows recommendations in the [STAC API pagination]. Limit and token are passed in as query params for `GET` endpoints, and via the body aas separte key/value pairs for `POST` requests.
17+
Pagination is token based and follows recommendations in the [STAC API pagination].
18+
Limit and token are passed in as query params for `GET` endpoints, and via the body as
19+
separate key/value pairs for `POST` requests.
1820

19-
If pagination is available and more records remain the response object will contain a `next` link object that can be used to get the next page of results. No `next` `Link` returned indicates there are no further records available.
21+
If pagination is available and more records remain the response object will contain a
22+
`next` link object that can be used to get the next page of results. No `next` `Link`
23+
returned indicates there are no further records available.
2024

2125
`limit` defaults to 10 and maxes at 100.
2226

23-
2427
## ADRs
2528

2629
ADRs can be found in in the [adrs](./adrs/README.md) directory.

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import nox
22

33

4-
@nox.session(python=["3.12"])
4+
@nox.session(python=["3.12", "3.13"])
55
def tests(session):
66
session.run("poetry", "install", external=True)
77
session.run("poetry", "run", "pytest", external=True)

poetry.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ readme = "README.md"
99
packages = [{include = "stapi_fastapi", from="src"}]
1010

1111
[tool.poetry.dependencies]
12-
python = "3.12.*"
12+
python = "^3.12.0"
1313
fastapi = "^0.115.0"
1414
pydantic = "^2.10.1"
1515
geojson-pydantic = "^1.1.1"

src/stapi_fastapi/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from .backends import ProductBackend, RootBackend
21
from .models import (
32
Link,
43
OpportunityProperties,
@@ -12,10 +11,8 @@
1211
"Link",
1312
"OpportunityProperties",
1413
"Product",
15-
"ProductBackend",
1614
"ProductRouter",
1715
"Provider",
1816
"ProviderRole",
19-
"RootBackend",
2017
"RootRouter",
2118
]
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
from .product_backend import ProductBackend
2-
from .root_backend import RootBackend
1+
from .product_backend import CreateOrder, SearchOpportunities
2+
from .root_backend import GetOrder, GetOrders, GetOrderStatuses
33

44
__all__ = [
5-
"ProductBackend",
6-
"RootBackend",
5+
"CreateOrder",
6+
"GetOrder",
7+
"GetOrders",
8+
"GetOrderStatuses",
9+
"SearchOpportunities",
710
]
Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Protocol
3+
from typing import Any, Callable, Coroutine
44

55
from fastapi import Request
66
from returns.maybe import Maybe
@@ -10,32 +10,49 @@
1010
from stapi_fastapi.models.order import Order, OrderPayload
1111
from stapi_fastapi.routers.product_router import ProductRouter
1212

13-
14-
class ProductBackend(Protocol): # pragma: nocover
15-
async def search_opportunities(
16-
self,
17-
product_router: ProductRouter,
18-
search: OpportunityRequest,
19-
request: Request,
20-
next: str | None,
21-
limit: int,
22-
) -> ResultE[tuple[list[Opportunity], Maybe[str]]]:
23-
"""
24-
Search for ordering opportunities for the given search parameters and return pagination token if applicable.
25-
26-
Backends must validate search constraints and return
27-
`stapi_fastapi.exceptions.ConstraintsException` if not valid.
28-
"""
29-
30-
async def create_order(
31-
self,
32-
product_router: ProductRouter,
33-
search: OrderPayload,
34-
request: Request,
35-
) -> ResultE[Order]:
36-
"""
37-
Create a new order.
38-
39-
Backends must validate order payload and return
40-
`stapi_fastapi.exceptions.ConstraintsException` if not valid.
41-
"""
13+
SearchOpportunities = Callable[
14+
[ProductRouter, OpportunityRequest, Request, str | None, int],
15+
Coroutine[Any, Any, ResultE[tuple[list[Opportunity], Maybe[str]]]],
16+
]
17+
"""
18+
Type alias for an async function that searches for ordering opportunities for the given
19+
search parameters.
20+
21+
Args:
22+
product_router (ProductRouter): The product router.
23+
search (OpportunityRequest): The search parameters.
24+
request (Request): FastAPI's Request object.
25+
next (str | None): A pagination token.
26+
limit (int): The maximum number of opportunities to return in a page.
27+
28+
Returns:
29+
A tuple containing a list of opportunities and a pagination token.
30+
31+
- Should return returns.result.Success[tuple[list[Opportunity], returns.maybe.Some[str]]] if including a pagination token
32+
- Should return returns.result.Success[tuple[list[Opportunity], returns.maybe.Nothing]] if not including a pagination token
33+
- Returning returns.result.Failure[Exception] will result in a 500.
34+
35+
Note:
36+
Backends must validate search constraints and return
37+
returns.result.Failure[stapi_fastapi.exceptions.ConstraintsException] if not valid.
38+
"""
39+
40+
CreateOrder = Callable[
41+
[ProductRouter, OrderPayload, Request], Coroutine[Any, Any, ResultE[Order]]
42+
]
43+
"""
44+
Type alias for an async function that creates a new order.
45+
46+
Args:
47+
product_router (ProductRouter): The product router.
48+
payload (OrderPayload): The order payload.
49+
request (Request): FastAPI's Request object.
50+
51+
Returns:
52+
- Should return returns.result.Success[Order]
53+
- Returning returns.result.Failure[Exception] will result in a 500.
54+
55+
Note:
56+
Backends must validate order payload and return
57+
returns.result.Failure[stapi_fastapi.exceptions.ConstraintsException] if not valid.
58+
"""

0 commit comments

Comments
 (0)