diff --git a/tipg/dependencies.py b/tipg/dependencies.py index a82feeb..f94251b 100644 --- a/tipg/dependencies.py +++ b/tipg/dependencies.py @@ -502,3 +502,94 @@ def CollectionsParams( next=offset + returned if matched - returned > offset else None, prev=max(offset - limit, 0) if offset else None, ) + +ExtraProperties = Dict[str, any] + +async def CollectionExtraProperties( + request: Request, + collection: Annotated[Collection, Depends(CollectionParams)], +) -> ExtraProperties: + """ + Extracts extra properties from the first feature of a collection. (First Item has the highest priority) + - This is done because there is no separate table to store the collection specific information. + To elaborate, a schema table represents the whole collection and a rows represents items. + So, If we add any columns with prefix "collection_properties_", we use that as extra properties. + - This is needed to store metadata information about the collection. + - This is named extraProperties as there was another key named properties defined already for the Collection. + + This function attempts to retrieve features from the provided collection. + If successful, it inspects the properties of the first feature and + filters them to include only those whose keys contain the prefix "collection_properties_". + + Args: + request: The incoming Starlette/FastAPI request object. + collection: The collection object, typically resolved by FastAPI's + dependency injection system using `CollectionParams`. + + Returns: + A dictionary containing the extra properties (keys containing "collection_properties_") + from the first feature of the collection. Returns an empty dictionary + if the collection features are not callable, if no features are found, + or if an error occurs during processing. + """ + # First come first serve to get the collection properties + extra_properties_dict={} + extra_properties_prefix = "collection_properties_" + + if (callable(collection.features)): + try: + item_list = await collection.features( + request + ) + extra_properties = item_list['items'][0]['properties'] + extra_properties_dict = dict(map(lambda key: (key, extra_properties[key]), filter(lambda key: extra_properties_prefix in key, extra_properties))) + except Exception as err: + print(err) + return extra_properties_dict + +CollectionsExtraPropertiesDict = Dict[str, ExtraProperties] + +async def CollectionsExtraProperties( + request: Request, + collections: Annotated[CollectionList, Depends(CollectionsParams)], +) -> CollectionsExtraPropertiesDict: + """ + For all the list of available collection, + Extracts extra properties from the first feature of a collection. (First Item has the highest priority) + - This is done because there is no separate table to store the collection specific information. + To elaborate, a schema table represents the whole collection and a rows represents items. + So, If we add any columns with prefix "collection_properties_", we use that as extra properties. + - This is needed to store metadata information about the collection. + - This is named extraProperties as there was another key named properties defined already for the Collection. + + This function attempts to retrieve features from the provided collection. + If successful, it inspects the properties of the first feature and + filters them to include only those whose keys contain the prefix "collection_properties_". + + Args: + request: The incoming Starlette/FastAPI request object. + collections: The collection list, typically resolved by FastAPI's + dependency injection system using `CollectionsParams`. + + Returns: + A dictionary containing the collection.id as key and extra properties (keys containing "collection_properties_") as value + """ + collections_extra_properties = {} + extra_properties_prefix = "collection_properties_" + + for collection in collections["collections"]: + collection_id = collection.id + extra_properties_dict={} + + if (callable(collection.features)): + try: + item_list = await collection.features( + request + ) + extra_properties = item_list['items'][0]['properties'] + extra_properties_dict = dict(map(lambda key: (key, extra_properties[key]), filter(lambda key: extra_properties_prefix in key, extra_properties))) + except Exception as err: + print(err) + collections_extra_properties[collection_id] = extra_properties_dict + + return collections_extra_properties diff --git a/tipg/factory.py b/tipg/factory.py index 46dd0d8..51fafcc 100644 --- a/tipg/factory.py +++ b/tipg/factory.py @@ -28,6 +28,10 @@ from tipg.collections import Collection, CollectionList from tipg.dependencies import ( CollectionParams, + ExtraProperties, + CollectionExtraProperties, + CollectionsExtraPropertiesDict, + CollectionsExtraProperties, CollectionsParams, ItemsOutputType, OutputType, @@ -187,6 +191,9 @@ class EndpointsFactory(metaclass=abc.ABCMeta): # collection dependency collection_dependency: Callable[..., Collection] = CollectionParams + # collection extra-properties dependency needed for collection metadata + collection_extra_properties: Callable[..., ExtraProperties] = CollectionExtraProperties + # Router Prefix is needed to find the path for routes when prefixed # e.g if you mount the route with `/foo` prefix, set router_prefix to foo router_prefix: str = "" @@ -363,6 +370,9 @@ class OGCFeaturesFactory(EndpointsFactory): # collections dependency collections_dependency: Callable[..., CollectionList] = CollectionsParams + # collections extra-properties dependency needed for collection metadata list + collections_extra_properties: Callable[..., CollectionsExtraPropertiesDict] = CollectionsExtraProperties + @property def conforms_to(self) -> List[str]: """Factory conformances.""" @@ -503,12 +513,16 @@ def _collections_route(self): # noqa: C901 }, tags=["OGC Features API"], ) - def collections( + async def collections( request: Request, collection_list: Annotated[ CollectionList, Depends(self.collections_dependency), ], + collections_extra_properties_dictionary: Annotated[ + CollectionsExtraPropertiesDict, + Depends(self.collections_extra_properties) + ], output_type: Annotated[ Optional[MediaType], Depends(OutputType), @@ -565,6 +579,7 @@ def collections( title=collection.id, description=collection.description, extent=collection.extent, + extraProperties=collections_extra_properties_dictionary[collection.id], links=[ model.Link( href=self.url_for( @@ -596,10 +611,10 @@ def collections( *self._additional_collection_tiles_links( request, collection ), - ], + ] ) for collection in collection_list["collections"] - ], + ] ).model_dump(exclude_none=True, mode="json") if output_type == MediaType.html: @@ -628,17 +643,20 @@ def _collection_route(self): }, tags=["OGC Features API"], ) - def collection( + async def collection( request: Request, collection: Annotated[Collection, Depends(self.collection_dependency)], + extraProperties: Annotated[Dict, Depends(self.collection_extra_properties)], output_type: Annotated[Optional[MediaType], Depends(OutputType)] = None, ): """Metadata for a feature collection.""" + data = model.Collection( id=collection.id, title=collection.title, description=collection.description, extent=collection.extent, + extraProperties=extraProperties, links=[ model.Link( title="Collection", diff --git a/tipg/model.py b/tipg/model.py index 0028e97..1cd5434 100644 --- a/tipg/model.py +++ b/tipg/model.py @@ -1,7 +1,7 @@ """tipg models.""" from datetime import datetime -from typing import Annotated, Dict, List, Literal, Optional, Set, Tuple, Union +from typing import Annotated, Any, Dict, List, Literal, Optional, Set, Tuple, Union from geojson_pydantic.features import Feature, FeatureCollection from morecantile.models import CRSType @@ -145,6 +145,7 @@ class Collection(BaseModel): extent: Optional[Extent] = None itemType: str = "feature" crs: List[str] = ["http://www.opengis.net/def/crs/OGC/1.3/CRS84"] + extraProperties: Optional[Dict[str, Any]] = None model_config = {"extra": "ignore"}