From b47b89acde1177baa8853864a560fb49af92c431 Mon Sep 17 00:00:00 2001 From: Sanjog Thapa Date: Fri, 9 May 2025 14:28:33 -0500 Subject: [PATCH 1/2] add extraProperties field in collection that holds dictionary, purposed to store metadata related to the collection --- tipg/factory.py | 115 ++++++++++++++++++++++++++++++------------------ tipg/model.py | 3 +- 2 files changed, 74 insertions(+), 44 deletions(-) diff --git a/tipg/factory.py b/tipg/factory.py index 46dd0d8..0665a59 100644 --- a/tipg/factory.py +++ b/tipg/factory.py @@ -503,7 +503,7 @@ def _collections_route(self): # noqa: C901 }, tags=["OGC Features API"], ) - def collections( + async def collections( request: Request, collection_list: Annotated[ CollectionList, @@ -555,51 +555,65 @@ def collections( ), ) + collections=[] + for collection in collection_list["collections"]: + # First come first serve to get the collection properties + extra_properties_dict={} + if (callable(collection.features)): + try: + item_list = await collection.features( + request + ) + extra_properties_prefix = "_fid" + 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.append(model.Collection( + id=collection.id, + title=collection.id, + description=collection.description, + extent=collection.extent, + extraProperties=extra_properties_dict, + links=[ + model.Link( + href=self.url_for( + request, + "collection", + collectionId=collection.id, + ), + rel="collection", + type=MediaType.json, + ), + model.Link( + href=self.url_for( + request, + "items", + collectionId=collection.id, + ), + rel="items", + type=MediaType.geojson, + ), + model.Link( + href=self.url_for( + request, + "queryables", + collectionId=collection.id, + ), + rel="queryables", + type=MediaType.schemajson, + ), + *self._additional_collection_tiles_links( + request, collection + ), + ] + )) + data = model.Collections( links=links, numberMatched=collection_list["matched"], numberReturned=len(collection_list["collections"]), - collections=[ - model.Collection( - id=collection.id, - title=collection.id, - description=collection.description, - extent=collection.extent, - links=[ - model.Link( - href=self.url_for( - request, - "collection", - collectionId=collection.id, - ), - rel="collection", - type=MediaType.json, - ), - model.Link( - href=self.url_for( - request, - "items", - collectionId=collection.id, - ), - rel="items", - type=MediaType.geojson, - ), - model.Link( - href=self.url_for( - request, - "queryables", - collectionId=collection.id, - ), - rel="queryables", - type=MediaType.schemajson, - ), - *self._additional_collection_tiles_links( - request, collection - ), - ], - ) - for collection in collection_list["collections"] - ], + collections=collections ).model_dump(exclude_none=True, mode="json") if output_type == MediaType.html: @@ -628,17 +642,32 @@ def _collection_route(self): }, tags=["OGC Features API"], ) - def collection( + async def collection( request: Request, collection: Annotated[Collection, Depends(self.collection_dependency)], output_type: Annotated[Optional[MediaType], Depends(OutputType)] = None, ): """Metadata for a feature collection.""" + + # First come first serve to get the collection properties + extra_properties_dict={} + if (callable(collection.features)): + try: + item_list = await collection.features( + request + ) + extra_properties_prefix = "_fid" + 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) + data = model.Collection( id=collection.id, title=collection.title, description=collection.description, extent=collection.extent, + extraProperties=extra_properties_dict, 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"} From a536eeda411b02193377adcfe33615ea3db4f863 Mon Sep 17 00:00:00 2001 From: Sanjog Thapa Date: Fri, 9 May 2025 16:22:51 -0500 Subject: [PATCH 2/2] add extra properties to collection and collection list via dependency injection --- tipg/dependencies.py | 91 +++++++++++++++++++++++++++++++ tipg/factory.py | 127 ++++++++++++++++++++----------------------- 2 files changed, 149 insertions(+), 69 deletions(-) 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 0665a59..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.""" @@ -509,6 +519,10 @@ async def collections( CollectionList, Depends(self.collections_dependency), ], + collections_extra_properties_dictionary: Annotated[ + CollectionsExtraPropertiesDict, + Depends(self.collections_extra_properties) + ], output_type: Annotated[ Optional[MediaType], Depends(OutputType), @@ -555,65 +569,52 @@ async def collections( ), ) - collections=[] - for collection in collection_list["collections"]: - # First come first serve to get the collection properties - extra_properties_dict={} - if (callable(collection.features)): - try: - item_list = await collection.features( - request - ) - extra_properties_prefix = "_fid" - 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.append(model.Collection( - id=collection.id, - title=collection.id, - description=collection.description, - extent=collection.extent, - extraProperties=extra_properties_dict, - links=[ - model.Link( - href=self.url_for( - request, - "collection", - collectionId=collection.id, - ), - rel="collection", - type=MediaType.json, - ), - model.Link( - href=self.url_for( - request, - "items", - collectionId=collection.id, - ), - rel="items", - type=MediaType.geojson, - ), - model.Link( - href=self.url_for( - request, - "queryables", - collectionId=collection.id, - ), - rel="queryables", - type=MediaType.schemajson, - ), - *self._additional_collection_tiles_links( - request, collection - ), - ] - )) - data = model.Collections( links=links, numberMatched=collection_list["matched"], numberReturned=len(collection_list["collections"]), - collections=collections + collections=[ + model.Collection( + id=collection.id, + title=collection.id, + description=collection.description, + extent=collection.extent, + extraProperties=collections_extra_properties_dictionary[collection.id], + links=[ + model.Link( + href=self.url_for( + request, + "collection", + collectionId=collection.id, + ), + rel="collection", + type=MediaType.json, + ), + model.Link( + href=self.url_for( + request, + "items", + collectionId=collection.id, + ), + rel="items", + type=MediaType.geojson, + ), + model.Link( + href=self.url_for( + request, + "queryables", + collectionId=collection.id, + ), + rel="queryables", + type=MediaType.schemajson, + ), + *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: @@ -645,29 +646,17 @@ def _collection_route(self): 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.""" - # First come first serve to get the collection properties - extra_properties_dict={} - if (callable(collection.features)): - try: - item_list = await collection.features( - request - ) - extra_properties_prefix = "_fid" - 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) - data = model.Collection( id=collection.id, title=collection.title, description=collection.description, extent=collection.extent, - extraProperties=extra_properties_dict, + extraProperties=extraProperties, links=[ model.Link( title="Collection",