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

Commit 494f952

Browse files
initial implementation of router composition for products
1 parent 8899ad6 commit 494f952

File tree

7 files changed

+92
-18
lines changed

7 files changed

+92
-18
lines changed

conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from fastapi.testclient import TestClient
66
from pytest import Parser, fixture
77

8-
from stapi_fastapi.api import StapiRouter
8+
from stapi_fastapi.main_router import MainRouter
99
from stapi_fastapi_test_backend import TestBackend
1010

1111
T = TypeVar("T")
@@ -38,7 +38,7 @@ def stapi_client(stapi_backend, base_url: str) -> YieldFixture[TestClient]:
3838
app = FastAPI()
3939

4040
app.include_router(
41-
StapiRouter(backend=stapi_backend).router,
41+
MainRouter(backend=stapi_backend).router,
4242
prefix="",
4343
)
4444

stapi_fastapi/__dev__.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
print("install uvicorn and pydantic-settings to use the dev server", file=stderr)
1414
exit(1)
1515

16-
from stapi_fastapi.api import StapiRouter
17-
16+
from stapi_fastapi.main_router import MainRouter
17+
from stapi_fastapi.routers.umbra_spotlight_router import umbra_spotlight_router
1818

1919
class DevSettings(BaseSettings):
2020
port: int = 8000
@@ -34,8 +34,19 @@ def validate_backend(cls, value: str):
3434

3535
settings = DevSettings()
3636
backend = settings.backend_name()
37+
38+
# Compose the router
39+
main_router = MainRouter()
40+
main_router.include_router(umbra_spotlight_router, prefix="/umbra_spotlight")
41+
3742
app = FastAPI(debug=True)
38-
app.include_router(StapiRouter(backend).router)
43+
app.include_router(main_router)
44+
45+
# Define a root endpoint
46+
# TODO: do I need this?
47+
@app.get("/")
48+
async def read_root():
49+
return {"message": "Welcome to STAPI AKA STAC to the Future AKA STAC-ish Order API"}
3950

4051

4152
def cli():

stapi_fastapi/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from typing import Any, Mapping
2+
from fastapi import HTTPException
23

4+
class StapiException(HTTPException):
5+
def __init__(self, status_code: int, detail: str) -> None:
6+
super().__init__(status_code, detail)
37

48
class ConstraintsException(Exception):
59
detail: Mapping[str, Any] | None
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from stapi_fastapi.backend import StapiBackend
66
from stapi_fastapi.constants import TYPE_GEOJSON, TYPE_JSON
7-
from stapi_fastapi.exceptions import ConstraintsException, NotFoundException
7+
from stapi_fastapi.exceptions import StapiException, ConstraintsException, NotFoundException
88
from stapi_fastapi.models.opportunity import (
99
OpportunityCollection,
1010
OpportunityRequest,
@@ -15,14 +15,12 @@
1515
from stapi_fastapi.models.shared import HTTPException as HTTPExceptionModel
1616
from stapi_fastapi.models.shared import Link
1717

18-
19-
class StapiException(HTTPException):
20-
def __init__(self, status_code: int, detail: str) -> None:
21-
super().__init__(status_code, detail)
22-
23-
24-
class StapiRouter:
25-
NAME_PREFIX = "stapi"
18+
"""
19+
/products/{component router} # router for each product added to main router
20+
/orders # list all orders
21+
"""
22+
class MainRouter:
23+
NAME_PREFIX = "main"
2624
backend: StapiBackend
2725
openapi_endpoint_name: str
2826
docs_endpoint_name: str
@@ -56,6 +54,7 @@ def __init__(
5654
name=f"{self.NAME_PREFIX}:list-products",
5755
tags=["Product"],
5856
)
57+
5958
self.router.add_api_route(
6059
"/products/{product_id}",
6160
self.product,
@@ -81,6 +80,7 @@ def __init__(
8180
tags=["Orders"],
8281
response_model=Order,
8382
)
83+
8484
self.router.add_api_route(
8585
"/orders/{order_id}",
8686
self.get_order,

stapi_fastapi/models/opportunity.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@
88
from stapi_fastapi.types.filter import CQL2Filter
99

1010

11+
# GENERIC Python
1112
# Copied and modified from https://github.com/stac-utils/stac-pydantic/blob/main/stac_pydantic/item.py#L11
1213
class OpportunityProperties(BaseModel):
1314
datetime: DatetimeInterval
14-
product_id: str
1515
model_config = ConfigDict(extra="allow")
1616

17-
18-
class OpportunityRequest(OpportunityProperties):
17+
# NOT GENERIC
18+
class OpportunityRequest(BaseModel):
19+
datetime: DatetimeInterval
1920
geometry: Geometry
21+
# TODO: validate the CQL2 filter?
2022
filter: Optional[CQL2Filter] = None
23+
# PHILOSOPH: strict?
2124

22-
25+
# GENERIC: Each product needs an opportunity model (constraints/parameters)
2326
class Opportunity(Feature[Geometry, OpportunityProperties]):
2427
type: Literal["Feature"] = "Feature"
2528

stapi_fastapi/products_router.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Generic product router factory
2+
from fastapi import APIRouter, HTTPException, status, Request
3+
from typing import Dict, Any
4+
from stapi_fastapi.models.opportunity import OpportunityRequest
5+
from stapi_fastapi.backend import StapiBackend
6+
from stapi_fastapi.exceptions import ConstraintsException
7+
8+
"""
9+
/products[MainRouter]/opportunities
10+
/products[MainRouter]/parameters
11+
/products[MainRouter]/order
12+
"""
13+
14+
def create_products_router(product_id: str, backend: StapiBackend) -> APIRouter:
15+
# TODO: map product names to product IDs
16+
"""
17+
Creates a new APIRouter for a specific product type with standardized routes.
18+
19+
Args:
20+
product_id (str): The name of the product type (e.g., 'electronics', 'furniture').
21+
backend (StapiBackend): Backend instance implementing the StapiBackend protocol.
22+
23+
Returns:
24+
APIRouter: A FastAPI APIRouter configured for the product type.
25+
"""
26+
# Create a new router for the given product type
27+
router = APIRouter(prefix=f"/{product_id}", tags=[product_id.capitalize()])
28+
29+
@router.get("/opportunities", summary="Get Opportunities for the product")
30+
async def get_opportunities(request: Request, search: OpportunityRequest):
31+
try:
32+
opportunities = await backend.search_opportunities(search, request)
33+
return opportunities
34+
except ConstraintsException as exc:
35+
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.detail)
36+
37+
@router.get("/parameters", summary="Get parameters for the product")
38+
async def get_product_parameters(request: Request):
39+
product = backend.product(product_id, request)
40+
if not product:
41+
raise HTTPException(status_code=404, detail="Product not found")
42+
return {"product_id": product.id, "parameters": product.parameters}
43+
44+
@router.post("/order", summary="Create an order for the product")
45+
async def create_order(request: Request, payload: OpportunityRequest):
46+
try:
47+
order = await backend.create_order(payload, request)
48+
return order
49+
except ConstraintsException as exc:
50+
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.detail)
51+
52+
return router
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from routers.products_router import create_products_router
2+
3+
# Create a router for electronics using the base factory function
4+
umbra_spotlight_router = create_products_router("umbra_spotlight")

0 commit comments

Comments
 (0)