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

Commit df6d1f0

Browse files
austin-sidekickparksjrjkeifertylanderson
authored
ProductRouter split from RootRouter (nee StapiRouter) (#80)
This changeset includes a number of fixes to better align behavior with the spec and fix bugs, and a whole bunch of clean up. The main feature is moving the `GET /opportunities` and `POST /orders` endpoints under the product path, such as `/products/{product_id}/opportunities`, as the spec has determined that opportunities and placing an order are product-specific (see stapi-spec/stapi-spec#198 and stapi-spec/stapi-spec#200). This changeset also adds the missing `/products/{product_id}/constraints` endpoint (formerly parameters). Another key feature is the introduction of the `bin/server.py` script that can be used with uvicorn to run a minimal implementation for local browsing. See the README for instructions. * initial implementation of router composition for products * make OpportunityProperties and Opportunity models generics * overhaul the main router as APIRouter, product router not a factory, add product router classmethod to main router * resolve merge conflicts * sort out the conftest a bit more * the /products/{product_id}/opportunities endpoint is accessible * adds product router and backend * wip: splits backend and router for root and product. Co-authored-by: Jarrett Keifer <[email protected]> * adds test for get_constraints * remove unused modules * various cleanup and refinements * no longer a problem with the root router / route * rename tests/backend{,s}.py * get tests passing * more cleanup * fixes StapiExceptions Co-authored-by: Tyler <[email protected]> Co-authored-by: Jarrett Keifer <[email protected]> * removes dupe HTTPException in models * passes product router instead of product to backend Co-authored-by: Tyler <[email protected]> Co-authored-by: Jarrett Keifer <[email protected]> * Adds OrderCollection type and returns for get_orders Co-authored-by: Tyler <[email protected]> Co-authored-by: Jarrett Keifer <[email protected]> * type the opporunities to the product constraints model * upgrade fastapi * fix GeoJSONResponse model bug per openapi docs * minimal test server to allow accessing openapi docs * remove nulls from serialized links * tests should fail not warn on spec incompatibility * add nocover to protocol implementations * standardize links on models --------- Co-authored-by: Mike Parks <[email protected]> Co-authored-by: Jarrett Keifer <[email protected]> Co-authored-by: Tyler <[email protected]>
1 parent f59e634 commit df6d1f0

29 files changed

+855
-498
lines changed

README.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# STAPI FastAPI - Sensor Tasking API with FastAPI
22

3-
WARNING: The whole [STAPI spec](https://github.com/stapi-spec/stapi-spec) is
4-
very much work in progress, so things are guaranteed to be not correct.
3+
WARNING: The whole [STAPI spec] is very much work in progress, so things are
4+
guaranteed to be not correct.
55

66
## Usage
77

@@ -11,7 +11,7 @@ STAPI FastAPI provides an `fastapi.APIRouter` which must be included in
1111
## Development
1212

1313
It's 2024 and we still need to pick our poison for a 2024 dependency management
14-
solution. This project picks [poetry][poetry] for now.
14+
solution. This project picks [poetry] for now.
1515

1616
### Dev Setup
1717

@@ -35,15 +35,22 @@ command `pytest`.
3535
### Dev Server
3636

3737
This project cannot be run on its own because it does not have any backend
38-
implementations. If a development server is desired, run one of the
39-
implementations such as
40-
[stapi-fastapi-tle](https://github.com/stapi-spec/stapi-fastapi-tle). To test
41-
something like stapi-fastapi-tle with your development version of
42-
stapi-fastapi, install them both into the same virtual environment.
38+
implementations. However, a minimal test implementation is provided in
39+
[`./bin/server.py`](./bin/server.py). It can be run with `uvicorn` as a way to
40+
interact with the API and to view the OpenAPI documentation. Run it like so
41+
from the project root:
42+
43+
```commandline
44+
uvicorn server:app --app-dir ./bin --reload
45+
```
46+
47+
With the `uvicorn` defaults the app should be accessible at
48+
`http://localhost:8000`.
4349

4450
### Implementing a backend
4551

4652
- The test suite assumes the backend can be instantiated without any parameters
4753
required by the constructor.
4854

55+
[STAPI spec]: https://github.com/stapi-spec/stapi-spec
4956
[poetry]: https://python-poetry.org/

bin/server.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from uuid import uuid4
2+
3+
from fastapi import FastAPI, Request
4+
from stapi_fastapi.backends.product_backend import ProductBackend
5+
from stapi_fastapi.exceptions import ConstraintsException, NotFoundException
6+
from stapi_fastapi.models.opportunity import (
7+
Opportunity,
8+
OpportunityPropertiesBase,
9+
OpportunityRequest,
10+
)
11+
from stapi_fastapi.models.order import Order
12+
from stapi_fastapi.models.product import (
13+
Product,
14+
Provider,
15+
ProviderRole,
16+
)
17+
from stapi_fastapi.routers.root_router import RootRouter
18+
19+
20+
class MockOrderDB(dict[int | str, Order]):
21+
pass
22+
23+
24+
class MockRootBackend:
25+
def __init__(self, orders: MockOrderDB) -> None:
26+
self._orders: MockOrderDB = orders
27+
28+
async def get_orders(self, request: Request) -> list[Order]:
29+
"""
30+
Show all orders.
31+
"""
32+
return list(self._orders.values())
33+
34+
async def get_order(self, order_id: str, request: Request) -> Order:
35+
"""
36+
Show details for order with `order_id`.
37+
"""
38+
try:
39+
return self._orders[order_id]
40+
except KeyError:
41+
raise NotFoundException()
42+
43+
44+
class MockProductBackend(ProductBackend):
45+
def __init__(self, orders: MockOrderDB) -> None:
46+
self._opportunities: list[Opportunity] = []
47+
self._allowed_payloads: list[OpportunityRequest] = []
48+
self._orders: MockOrderDB = orders
49+
50+
async def search_opportunities(
51+
self, product: Product, search: OpportunityRequest, request: Request
52+
) -> list[Opportunity]:
53+
return [o.model_copy(update=search.model_dump()) for o in self._opportunities]
54+
55+
async def create_order(
56+
self, product: Product, payload: OpportunityRequest, request: Request
57+
) -> Order:
58+
"""
59+
Create a new order.
60+
"""
61+
allowed: bool = any(allowed == payload for allowed in self._allowed_payloads)
62+
if allowed:
63+
order = Order(
64+
id=str(uuid4()),
65+
geometry=payload.geometry,
66+
properties={
67+
"filter": payload.filter,
68+
"datetime": payload.datetime,
69+
"product_id": product.id,
70+
},
71+
links=[],
72+
)
73+
self._orders[order.id] = order
74+
return order
75+
raise ConstraintsException("not allowed")
76+
77+
78+
class TestSpotlightProperties(OpportunityPropertiesBase):
79+
off_nadir: int
80+
81+
82+
order_db = MockOrderDB()
83+
product_backend = MockProductBackend(order_db)
84+
root_backend = MockRootBackend(order_db)
85+
86+
provider = Provider(
87+
name="Test Provider",
88+
description="A provider for Test data",
89+
roles=[ProviderRole.producer], # Example role
90+
url="https://test-provider.example.com", # Must be a valid URL
91+
)
92+
93+
product = Product(
94+
id="test-spotlight",
95+
title="Test Spotlight Product",
96+
description="Test product for test spotlight",
97+
license="CC-BY-4.0",
98+
keywords=["test", "satellite"],
99+
providers=[provider],
100+
links=[],
101+
constraints=TestSpotlightProperties,
102+
backend=product_backend,
103+
)
104+
105+
root_router = RootRouter(root_backend)
106+
root_router.add_product(product)
107+
app: FastAPI = FastAPI()
108+
app.include_router(root_router, prefix="")

poetry.lock

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

src/stapi_fastapi/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from .backends import ProductBackend, RootBackend
2+
from .models import (
3+
Link,
4+
OpportunityPropertiesBase,
5+
Product,
6+
Provider,
7+
ProviderRole,
8+
)
9+
from .routers import ProductRouter, RootRouter
10+
11+
__all__ = [
12+
"Link",
13+
"OpportunityPropertiesBase",
14+
"Product",
15+
"ProductBackend",
16+
"ProductRouter",
17+
"Provider",
18+
"ProviderRole",
19+
"RootBackend",
20+
"RootRouter",
21+
]

0 commit comments

Comments
 (0)