Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat subclass router issue 73 #80

Merged
merged 30 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
494f952
initial implementation of router composition for products
austin-sidekick Oct 9, 2024
c0d7a23
make OpportunityProperties and Opportunity models generics
austin-sidekick Oct 9, 2024
3139ec4
overhaul the main router as APIRouter, product router not a factory, …
austin-sidekick Oct 9, 2024
2d2ce71
Merge branch 'main' into feat-subclass-router-issue-73
austin-sidekick Oct 9, 2024
1ac2cc8
resolve merge conflicts
austin-sidekick Oct 9, 2024
73b9c03
sort out the conftest a bit more
austin-sidekick Oct 9, 2024
6fa822b
the /products/{product_id}/opportunities endpoint is accessible
austin-sidekick Oct 9, 2024
1c4126f
Merge commit '3d8e618cc1419d42805048e0dd69a6efa19d1a2b' into mp/feat-…
parksjr Oct 23, 2024
571f86a
adds product router and backend
parksjr Oct 23, 2024
91f8b65
wip: splits backend and router for root and product.
parksjr Oct 30, 2024
1adf323
Merge branch 'mp/feat-subclass-router-issue-73' into feat-subclass-ro…
parksjr Oct 30, 2024
db815d1
adds test for get_constraints
parksjr Oct 31, 2024
f79a589
remove unused modules
jkeifer Oct 31, 2024
6e0285d
various cleanup and refinements
jkeifer Oct 31, 2024
c9e3aa3
no longer a problem with the root router / route
jkeifer Nov 1, 2024
6a2f4d7
rename tests/backend{,s}.py
jkeifer Nov 1, 2024
fba52ed
get tests passing
jkeifer Nov 1, 2024
1bde9f5
more cleanup
jkeifer Nov 1, 2024
375c51c
fixes StapiExceptions
parksjr Nov 1, 2024
07b2597
removes dupe HTTPException in models
parksjr Nov 1, 2024
9355fdf
passes product router instead of product to backend
parksjr Nov 1, 2024
f965f99
Adds OrderCollection type and returns for get_orders
parksjr Nov 1, 2024
18c7be5
type the opporunities to the product constraints model
jkeifer Nov 1, 2024
19fdf34
upgrade fastapi
jkeifer Nov 1, 2024
2939a3e
fix GeoJSONResponse model bug per openapi docs
jkeifer Nov 1, 2024
6919ab5
minimal test server to allow accessing openapi docs
jkeifer Nov 1, 2024
671d66a
remove nulls from serialized links
jkeifer Nov 1, 2024
f5f3dd2
tests should fail not warn on spec incompatibility
jkeifer Nov 1, 2024
5948e22
add nocover to protocol implementations
jkeifer Nov 1, 2024
ba130c4
standardize links on models
jkeifer Nov 1, 2024
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
4 changes: 2 additions & 2 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from fastapi.testclient import TestClient
from pytest import Parser, fixture

from stapi_fastapi.api import StapiRouter
from stapi_fastapi.main_router import MainRouter
jkeifer marked this conversation as resolved.
Show resolved Hide resolved
from stapi_fastapi_test_backend import TestBackend

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

app.include_router(
StapiRouter(backend=stapi_backend).router,
MainRouter(backend=stapi_backend).router,
prefix="",
)

Expand Down
17 changes: 14 additions & 3 deletions stapi_fastapi/__dev__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
print("install uvicorn and pydantic-settings to use the dev server", file=stderr)
exit(1)

from stapi_fastapi.api import StapiRouter

from stapi_fastapi.main_router import MainRouter
from stapi_fastapi.routers.umbra_spotlight_router import umbra_spotlight_router

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

settings = DevSettings()
backend = settings.backend_name()

# Compose the router
main_router = MainRouter()
main_router.include_router(umbra_spotlight_router, prefix="/umbra_spotlight")
jkeifer marked this conversation as resolved.
Show resolved Hide resolved

app = FastAPI(debug=True)
app.include_router(StapiRouter(backend).router)
app.include_router(main_router)

# Define a root endpoint
# TODO: do I need this?
jkeifer marked this conversation as resolved.
Show resolved Hide resolved
@app.get("/")
async def read_root():
return {"message": "Welcome to STAPI AKA STAC to the Future AKA STAC-ish Order API"}
jkeifer marked this conversation as resolved.
Show resolved Hide resolved


def cli():
Expand Down
1 change: 0 additions & 1 deletion stapi_fastapi/backend.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Protocol

from fastapi import Request

from stapi_fastapi.models.opportunity import Opportunity, OpportunityRequest
Expand Down
4 changes: 4 additions & 0 deletions stapi_fastapi/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from typing import Any, Mapping
from fastapi import HTTPException

class StapiException(HTTPException):
def __init__(self, status_code: int, detail: str) -> None:
super().__init__(status_code, detail)

class ConstraintsException(Exception):
detail: Mapping[str, Any] | None
Expand Down
18 changes: 9 additions & 9 deletions stapi_fastapi/api.py → stapi_fastapi/main_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from stapi_fastapi.backend import StapiBackend
from stapi_fastapi.constants import TYPE_GEOJSON, TYPE_JSON
from stapi_fastapi.exceptions import ConstraintsException, NotFoundException
from stapi_fastapi.exceptions import StapiException, ConstraintsException, NotFoundException
from stapi_fastapi.models.opportunity import (
OpportunityCollection,
OpportunityRequest,
Expand All @@ -15,14 +15,12 @@
from stapi_fastapi.models.shared import HTTPException as HTTPExceptionModel
from stapi_fastapi.models.shared import Link


class StapiException(HTTPException):
def __init__(self, status_code: int, detail: str) -> None:
super().__init__(status_code, detail)


class StapiRouter:
NAME_PREFIX = "stapi"
"""
/products/{component router} # router for each product added to main router
/orders # list all orders
"""
class MainRouter:
NAME_PREFIX = "main"
backend: StapiBackend
jkeifer marked this conversation as resolved.
Show resolved Hide resolved
openapi_endpoint_name: str
docs_endpoint_name: str
Expand Down Expand Up @@ -56,6 +54,7 @@ def __init__(
name=f"{self.NAME_PREFIX}:list-products",
tags=["Product"],
)

self.router.add_api_route(
"/products/{product_id}",
self.product,
Expand All @@ -81,6 +80,7 @@ def __init__(
tags=["Orders"],
response_model=Order,
)
jkeifer marked this conversation as resolved.
Show resolved Hide resolved

self.router.add_api_route(
"/orders/{order_id}",
self.get_order,
Expand Down
19 changes: 13 additions & 6 deletions stapi_fastapi/models/opportunity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Literal, Optional
from typing import Literal, Optional, TypeVar, Generic

from geojson_pydantic import Feature, FeatureCollection
from geojson_pydantic.geometries import Geometry
Expand All @@ -7,20 +7,27 @@
from stapi_fastapi.types.datetime_interval import DatetimeInterval
from stapi_fastapi.types.filter import CQL2Filter

# Generic type definition for Opportunity Properties
T = TypeVar("T")

# Copied and modified from https://github.com/stac-utils/stac-pydantic/blob/main/stac_pydantic/item.py#L11
class OpportunityProperties(BaseModel):
class OpportunityProperties(BaseModel, Generic[T]):
datetime: DatetimeInterval
product_id: str
model_config = ConfigDict(extra="allow")


class OpportunityRequest(OpportunityProperties):
class OpportunityRequest(BaseModel):
datetime: DatetimeInterval
geometry: Geometry
# TODO: validate the CQL2 filter?
filter: Optional[CQL2Filter] = None
# PHILOSOPH: strict?

# Generic type definition for Opportunity
P = TypeVar("P", bound=OpportunityProperties)
K = TypeVar("K", bound=Geometry)

class Opportunity(Feature[Geometry, OpportunityProperties]):
# Each product implements its own opportunity model
class Opportunity(Feature[K, P], Generic[K, P]):
type: Literal["Feature"] = "Feature"


Expand Down
50 changes: 50 additions & 0 deletions stapi_fastapi/products_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generic product router factory
from fastapi import APIRouter, HTTPException, status, Request
from stapi_fastapi.models.opportunity import OpportunityRequest
from stapi_fastapi.backend import StapiBackend
from stapi_fastapi.exceptions import ConstraintsException

"""
/products[MainRouter]/opportunities
/products[MainRouter]/parameters
/products[MainRouter]/order
"""

def create_products_router(product_id: str, backend: StapiBackend) -> APIRouter:
jkeifer marked this conversation as resolved.
Show resolved Hide resolved
"""
Creates a new APIRouter for a specific product type with standardized routes.

Args:
product_id (str): The name of the product type (e.g., 'electronics', 'furniture').
backend (StapiBackend): Backend instance implementing the StapiBackend protocol.

Returns:
APIRouter: A FastAPI APIRouter configured for the product type.
"""
# Create a new router for the given product type
router = APIRouter(prefix=f"/{product_id}", tags=[product_id.capitalize()])

@router.get("/opportunities", summary="Get Opportunities for the product")
async def get_opportunities(request: Request, search: OpportunityRequest):
try:
opportunities = await backend.search_opportunities(search, request)
return opportunities
except ConstraintsException as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.detail)

@router.get("/parameters", summary="Get parameters for the product")
async def get_product_parameters(request: Request):
product = backend.product(product_id, request)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return {"product_id": product.id, "parameters": product.parameters}

@router.post("/order", summary="Create an order for the product")
async def create_order(request: Request, payload: OpportunityRequest):
try:
order = await backend.create_order(payload, request)
return order
except ConstraintsException as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.detail)

return router
4 changes: 4 additions & 0 deletions stapi_fastapi/umbra_spotlight_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from routers.products_router import create_products_router

# Create a router for electronics using the base factory function
umbra_spotlight_router = create_products_router("umbra-spotlight-1")