async def item_collection(self, id: str, limit: int = 10, token: str = None, **kwargs) -> ItemCollection: """Get all items from a specific collection. Called with `GET /collections/{collectionId}/items` Args: id: id of the collection. limit: number of items to return. token: pagination token. Returns: An ItemCollection. """ # If collection does not exist, NotFoundError wil be raised await self.get_collection(id, **kwargs) req = PgstacSearch(collections=[id], limit=limit, token=token) item_collection = await self._search_base(req, **kwargs) links = await CollectionLinks(collection_id=id, request=kwargs["request"]).get_links( extra_links=item_collection["links"]) item_collection["links"] = links return item_collection
async def _search_base(self, search_request: PgstacSearch, **kwargs) -> Dict[str, Any]: """Cross catalog search (POST). Called with `POST /search`. Args: search_request: search request parameters. Returns: ItemCollection containing items which match the search criteria. """ request = kwargs["request"] pool = request.app.state.readpool # pool = kwargs["request"].app.state.readpool req = search_request.json(exclude_none=True) async with pool.acquire() as conn: q, p = render( """ SELECT * FROM search(:req::text::jsonb); """, req=req, ) items = await conn.fetchval(q, *p) next = items.pop("next", None) prev = items.pop("prev", None) collection = ItemCollection.construct(**items) cleaned_features = [] if collection.features is None or len(collection.features) == 0: raise NotFoundError("No features found") for feature in collection.features: feature = Item.construct(**feature) if "links" not in search_request.fields.exclude: links = await ItemLinks( collection_id=feature.collection, item_id=feature.id, request=request, ).get_links() feature.links = links exclude = search_request.fields.exclude if len(exclude) == 0: exclude = None include = search_request.fields.include if len(include) == 0: include = None feature = feature.dict(exclude_none=True, ) cleaned_features.append(feature) collection.features = cleaned_features collection.links = await PagingLinks( request=request, next=next, prev=prev, ).get_links() return collection
async def get_item(self, item_id: str, collection_id: str, **kwargs) -> ORJSONResponse: """Get item by id. Called with `GET /collections/{collectionId}/items/{itemId}`. Args: id: Id of the item. Returns: Item. """ req = PgstacSearch(ids=[item_id], limit=1) collection = await self._search_base(req, **kwargs) return ORJSONResponse(collection.features[0])
async def get_item(self, item_id: str, collection_id: str, **kwargs) -> Item: """Get item by id. Called with `GET /collections/{collectionId}/items/{itemId}`. Args: id: Id of the item. Returns: Item. """ # If collection does not exist, NotFoundError wil be raised await self.get_collection(collection_id, **kwargs) req = PgstacSearch(ids=[item_id], limit=1) item_collection = await self._search_base(req, **kwargs) if not item_collection["features"]: raise NotFoundError(f"Collection {collection_id} does not exist.") return Item(**item_collection["features"][0])
async def item_collection(self, id: str, limit: int = 10, token: str = None, **kwargs) -> ORJSONResponse: """Get all items from a specific collection. Called with `GET /collections/{collectionId}/items` Args: id: id of the collection. limit: number of items to return. token: pagination token. Returns: An ItemCollection. """ req = PgstacSearch(collections=[id], limit=limit, token=token) collection = await self._search_base(req, **kwargs) links = await CollectionLinks( collection_id=id, request=kwargs["request"]).get_links(extra_links=collection.links) collection.links = links return ORJSONResponse(collection.dict(exclude_none=True))
async def _search_base(self, search_request: PgstacSearch, **kwargs: Any) -> ItemCollection: """Cross catalog search (POST). Called with `POST /search`. Args: search_request: search request parameters. Returns: ItemCollection containing items which match the search criteria. """ items: Dict[str, Any] request: Request = kwargs["request"] pool = request.app.state.readpool # pool = kwargs["request"].app.state.readpool req = search_request.json(exclude_none=True) try: async with pool.acquire() as conn: q, p = render( """ SELECT * FROM search(:req::text::jsonb); """, req=req, ) items = await conn.fetchval(q, *p) except InvalidDatetimeFormatError: raise InvalidQueryParameter( f"Datetime parameter {search_request.datetime} is invalid.") next: Optional[str] = items.pop("next", None) prev: Optional[str] = items.pop("prev", None) collection = ItemCollection(**items) cleaned_features: List[Item] = [] for feature in collection.get("features") or []: feature = Item(**feature) if (search_request.fields.exclude is None or "links" not in search_request.fields.exclude): # TODO: feature.collection is not always included # This code fails if it's left outside of the fields expression # I've fields extension updated test cases to always include feature.collection feature["links"] = await ItemLinks( collection_id=feature["collection"], item_id=feature["id"], request=request, ).get_links(extra_links=feature.get("links")) exclude = search_request.fields.exclude if exclude and len(exclude) == 0: exclude = None include = search_request.fields.include if include and len(include) == 0: include = None cleaned_features.append(feature) collection["features"] = cleaned_features collection["links"] = await PagingLinks( request=request, next=next, prev=prev, ).get_links() return collection
async def get_search( self, collections: Optional[List[str]] = None, ids: Optional[List[str]] = None, bbox: Optional[List[NumType]] = None, datetime: Optional[Union[str, datetime]] = None, limit: Optional[int] = 10, query: Optional[str] = None, token: Optional[str] = None, fields: Optional[List[str]] = None, sortby: Optional[str] = None, **kwargs, ) -> ItemCollection: """Cross catalog search (GET). Called with `GET /search`. Returns: ItemCollection containing items which match the search criteria. """ # Parse request parameters base_args = { "collections": collections, "ids": ids, "bbox": bbox, "limit": limit, "token": token, "query": orjson.loads(query) if query else query, } if datetime: base_args["datetime"] = datetime if sortby: # https://github.com/radiantearth/stac-spec/tree/master/api-spec/extensions/sort#http-get-or-post-form sort_param = [] for sort in sortby: sortparts = re.match(r"^([+-]?)(.*)$", sort) if sortparts: sort_param.append({ "field": sortparts.group(2).strip(), "direction": "desc" if sortparts.group(1) == "-" else "asc", }) base_args["sortby"] = sort_param if fields: includes = set() excludes = set() for field in fields: if field[0] == "-": excludes.add(field[1:]) elif field[0] == "+": includes.add(field[1:]) else: includes.add(field) base_args["fields"] = {"include": includes, "exclude": excludes} # Do the request try: search_request = PgstacSearch(**base_args) except ValidationError: raise HTTPException(status_code=400, detail="Invalid parameters provided") return await self.post_search(search_request, request=kwargs["request"])