Skip to content

Make API customization easier #380

@mcucchi9

Description

@mcucchi9

Description

As an enthusiast user of the stac-fastapi-pgstac, I find it a little cumbersome to implement little customizations on top of it. I came to believe that this mainly stems from the structure of the app.py module, and that customizability could be improved by implementing a small (and mainly backward-compatible) refactoring of it.

Use cases

A couple of (real and personal) use cases which motivates this issue:

Each one of the above, even taken independently, required copying a quite a few lines of boilerplate code from the app.py module.

Possible solution

One possible solution which I applied in another project of mine essentially relies on wrapping these lines

search_extensions_map: dict[str, ApiExtension] = {
"query": QueryExtension(),
"sort": SortExtension(),
"fields": FieldsExtension(),
"filter": SearchFilterExtension(client=FiltersClient()),
"pagination": TokenPaginationExtension(),
}
# collection_search extensions
cs_extensions_map: dict[str, ApiExtension] = {
"query": QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
"sort": SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
"fields": FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
"filter": CollectionSearchFilterExtension(client=FiltersClient()),
"free_text": FreeTextExtension(
conformance_classes=[FreeTextConformanceClasses.COLLECTIONS],
),
"pagination": OffsetPaginationExtension(),
}
# item_collection extensions
itm_col_extensions_map: dict[str, ApiExtension] = {
"query": QueryExtension(
conformance_classes=[QueryConformanceClasses.ITEMS],
),
"sort": SortExtension(
conformance_classes=[SortConformanceClasses.ITEMS],
),
"fields": FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
"filter": ItemCollectionFilterExtension(client=FiltersClient()),
"pagination": TokenPaginationExtension(),
}
enabled_extensions: set[str] = {
*search_extensions_map.keys(),
*cs_extensions_map.keys(),
*itm_col_extensions_map.keys(),
"collection_search",
}
if ext := settings.enabled_extensions:
enabled_extensions = set(ext.split(","))
application_extensions: list[ApiExtension] = []
with_transactions = settings.enable_transactions_extensions
if with_transactions:
application_extensions.append(
TransactionExtension(
client=TransactionsClient(),
settings=settings,
response_class=JSONResponse,
),
)
application_extensions.append(
BulkTransactionExtension(client=BulkTransactionsClient()),
)
# /search models
search_extensions = [
extension
for key, extension in search_extensions_map.items()
if key in enabled_extensions
]
post_request_model = create_post_request_model(search_extensions, base_model=PgstacSearch)
get_request_model = create_get_request_model(search_extensions)
application_extensions.extend(search_extensions)
# /collections/{collectionId}/items model
items_get_request_model: type[APIRequest] = ItemCollectionUri
itm_col_extensions = [
extension
for key, extension in itm_col_extensions_map.items()
if key in enabled_extensions
]
if itm_col_extensions:
items_get_request_model = cast(
type[APIRequest],
create_request_model(
model_name="ItemCollectionUri",
base_model=ItemCollectionUri,
extensions=itm_col_extensions,
request_type="GET",
),
)
application_extensions.extend(itm_col_extensions)
# /collections model
collections_get_request_model: type[APIRequest] = EmptyRequest
if "collection_search" in enabled_extensions:
cs_extensions = [
extension
for key, extension in cs_extensions_map.items()
if key in enabled_extensions
]
collection_search_extension = CollectionSearchExtension.from_extensions(cs_extensions)
collections_get_request_model = collection_search_extension.GET
application_extensions.append(collection_search_extension)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""FastAPI Lifespan."""
await connect_to_db(app, add_write_connection_pool=with_transactions)
yield
await close_db_connection(app)
api = StacApi(
app=FastAPI(
openapi_url=settings.openapi_url,
docs_url=settings.docs_url,
redoc_url=None,
root_path=settings.root_path,
title=settings.stac_fastapi_title,
version=settings.stac_fastapi_version,
description=settings.stac_fastapi_description,
lifespan=lifespan,
),
router=APIRouter(prefix=settings.prefix_path),
settings=settings,
extensions=application_extensions,
client=CoreCrudClient(pgstac_search_model=post_request_model), # type: ignore [arg-type]
response_class=JSONResponse,
items_get_request_model=items_get_request_model,
search_get_request_model=get_request_model,
search_post_request_model=post_request_model,
collections_get_request_model=collections_get_request_model,
middlewares=[
Middleware(BrotliMiddleware),
Middleware(ProxyHeaderMiddleware),
Middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_origin_regex=settings.cors_origin_regex,
allow_methods=settings.cors_methods,
allow_credentials=settings.cors_credentials,
allow_headers=settings.cors_headers,
max_age=600,
),
],
health_check=health_check, # type: ignore [arg-type]
)
into a function, such as instantiate_api, with arguments giving the possibility to inject e.g. a customized client and customized extensions.

The function defaults would leave current behavior unchanged. The only possible source of non-backward-compatibility I can foresee stems from the eventuality that some user currently imports objects from the app.py module's namespace which will then live inside the instantiate_api function.

I can work on a draft PR on this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions