def test_api_paging_extension(): item_collection = request(ITEM_COLLECTION) item_collection["links"] += [ { "title": "next page", "rel": "next", "method": "GET", "href": "http://next" }, { "title": "previous page", "rel": "previous", "method": "POST", "href": "http://prev", "body": { "key": "value" }, }, ] model = ItemCollection(**item_collection) links = model.to_dict()["links"] # Make sure we can mix links and pagination links normal_link = Link(**links[0]) assert normal_link.rel == "self" next_link = PaginationLink(**links[1]) assert next_link.rel == "next" previous_link = PaginationLink(**links[2]) assert previous_link.rel == "previous" assert previous_link.body == {"key": "value"}
def test_api_invalid_paging_link(): # Invalid rel type with pytest.raises(ValidationError): PaginationLink(rel="self", method="GET", href="http://next") # Invalid method with pytest.raises(ValidationError): PaginationLink(rel="next", method="DELETE", href="http://next")
def link_prev(self) -> PaginationLink: """Create link for previous page.""" if self.prev is not None: method = self.request.method if method == "GET": href = merge_params(self.url, {"token": f"prev:{self.prev}"}) return PaginationLink( rel=Relations.previous, type=MimeTypes.json, method=method, href=href, ) if method == "POST": body = self.request.postbody body["token"] = f"prev:{self.prev}" return PaginationLink( rel=Relations.previous, type=MimeTypes.json, method=method, href=f"{self.request.url}", body=body, )
def link_next(self) -> PaginationLink: """Create link for next page.""" if self.next is not None: method = self.request.method if method == "GET": href = merge_params(self.url, {"token": f"next:{self.next}"}) link = PaginationLink( rel=Relations.next, type=MimeTypes.json, method=method, href=href, ) return link if method == "POST": body = self.request.postbody body["token"] = f"next:{self.next}" return PaginationLink( rel=Relations.next, type=MimeTypes.json, method=method, href=f"{self.request.url}", body=body, )
def post_search(self, search_request: STACSearch, **kwargs) -> Dict[str, Any]: """POST search catalog.""" with self.session.reader.context_session() as session: token = (self.get_token(search_request.token) if search_request.token else False) query = session.query(self.item_table) # Filter by collection count = None if search_request.collections: query = query.join(self.collection_table).filter( sa.or_(*[ self.collection_table.id == col_id for col_id in search_request.collections ])) # Sort if search_request.sortby: sort_fields = [ getattr(self.item_table.get_field(sort.field), sort.direction.value)() for sort in search_request.sortby ] sort_fields.append(self.item_table.id) query = query.order_by(*sort_fields) else: # Default sort is date query = query.order_by(self.item_table.datetime.desc(), self.item_table.id) # Ignore other parameters if ID is present if search_request.ids: id_filter = sa.or_( *[self.item_table.id == i for i in search_request.ids]) items = query.filter(id_filter).order_by(self.item_table.id) page = get_page(items, per_page=search_request.limit, page=token) if self.extension_is_enabled(ContextExtension): count = len(search_request.ids) page.next = (self.insert_token( keyset=page.paging.bookmark_next) if page.paging.has_next else None) page.previous = (self.insert_token( keyset=page.paging.bookmark_previous) if page.paging.has_previous else None) else: # Spatial query poly = None if search_request.intersects is not None: poly = shape(search_request.intersects) elif search_request.bbox: poly = ShapelyPolygon.from_bounds(*search_request.bbox) if poly: filter_geom = ga.shape.from_shape(poly, srid=4326) query = query.filter( ga.func.ST_Intersects(self.item_table.geometry, filter_geom)) # Temporal query if search_request.datetime: # Two tailed query (between) if ".." not in search_request.datetime: query = query.filter( self.item_table.datetime.between( *search_request.datetime)) # All items after the start date if search_request.datetime[0] != "..": query = query.filter(self.item_table.datetime >= search_request.datetime[0]) # All items before the end date if search_request.datetime[1] != "..": query = query.filter(self.item_table.datetime <= search_request.datetime[1]) # Query fields if search_request.query: for (field_name, expr) in search_request.query.items(): field = self.item_table.get_field(field_name) for (op, value) in expr.items(): query = query.filter(op.operator(field, value)) if self.extension_is_enabled(ContextExtension): count_query = query.statement.with_only_columns( [func.count()]).order_by(None) count = query.session.execute(count_query).scalar() page = get_page(query, per_page=search_request.limit, page=token) # Create dynamic attributes for each page page.next = (self.insert_token( keyset=page.paging.bookmark_next) if page.paging.has_next else None) page.previous = (self.insert_token( keyset=page.paging.bookmark_previous) if page.paging.has_previous else None) links = [] if page.next: links.append( PaginationLink( rel=Relations.next, type="application/geo+json", href=f"{kwargs['request'].base_url}search", method="POST", body={"token": page.next}, merge=True, )) if page.previous: links.append( PaginationLink( rel=Relations.previous, type="application/geo+json", href=f"{kwargs['request'].base_url}search", method="POST", body={"token": page.previous}, merge=True, )) response_features = [] filter_kwargs = {} if self.extension_is_enabled(FieldsExtension): if search_request.query is not None: query_include: Set[str] = set([ k if k in Settings.get().indexed_fields else f"properties.{k}" for k in search_request.query.keys() ]) if not search_request.field.include: search_request.field.include = query_include else: search_request.field.include.union(query_include) filter_kwargs = search_request.field.filter_fields xvals = [] yvals = [] for item in page: item.base_url = str(kwargs["request"].base_url) item_model = schemas.Item.from_orm(item) xvals += [item_model.bbox[0], item_model.bbox[2]] yvals += [item_model.bbox[1], item_model.bbox[3]] response_features.append(item_model.to_dict(**filter_kwargs)) try: bbox = (min(xvals), min(yvals), max(xvals), max(yvals)) except ValueError: bbox = None context_obj = None if self.extension_is_enabled(ContextExtension): context_obj = { "returned": len(page), "limit": search_request.limit, "matched": count, } return { "type": "FeatureCollection", "context": context_obj, "features": response_features, "links": links, "bbox": bbox, }
def item_collection(self, id: str, limit: int = 10, token: str = None, **kwargs) -> ItemCollection: """Read an item collection from the database.""" with self.session.reader.context_session() as session: collection_children = (session.query(self.item_table).join( self.collection_table).filter( self.collection_table.id == id).order_by( self.item_table.datetime.desc(), self.item_table.id)) count = None if self.extension_is_enabled(ContextExtension): count_query = collection_children.statement.with_only_columns( [func.count()]).order_by(None) count = collection_children.session.execute( count_query).scalar() token = self.get_token(token) if token else token page = get_page(collection_children, per_page=limit, page=(token or False)) # Create dynamic attributes for each page page.next = (self.insert_token(keyset=page.paging.bookmark_next) if page.paging.has_next else None) page.previous = (self.insert_token( keyset=page.paging.bookmark_previous) if page.paging.has_previous else None) links = [] if page.next: links.append( PaginationLink( rel=Relations.next, type="application/geo+json", href= f"{kwargs['request'].base_url}collections/{id}/items?token={page.next}&limit={limit}", method="GET", )) if page.previous: links.append( PaginationLink( rel=Relations.previous, type="application/geo+json", href= f"{kwargs['request'].base_url}collections/{id}/items?token={page.previous}&limit={limit}", method="GET", )) response_features = [] for item in page: item.base_url = str(kwargs["request"].base_url) response_features.append(schemas.Item.from_orm(item)) context_obj = None if self.extension_is_enabled(ContextExtension): context_obj = { "returned": len(page), "limit": limit, "matched": count } return ItemCollection( type="FeatureCollection", context=context_obj, features=response_features, links=links, )
def item_collection( self, id: str, limit: int = 10, token: str = None, **kwargs ) -> ItemCollection: """Read an item collection from the database""" try: collection_children = ( self.reader_session.query(self.table) .join(self.collection_table) .filter(self.collection_table.id == id) .order_by(self.table.datetime.desc(), self.table.id) ) count = None if config.settings.api_extension_is_enabled(config.ApiExtensions.context): count_query = collection_children.statement.with_only_columns( [func.count()] ).order_by(None) count = collection_children.session.execute(count_query).scalar() token = self.pagination_client.get(token) if token else token page = get_page(collection_children, per_page=limit, page=(token or False)) # Create dynamic attributes for each page page.next = ( self.pagination_client.insert(keyset=page.paging.bookmark_next) if page.paging.has_next else None ) page.previous = ( self.pagination_client.insert(keyset=page.paging.bookmark_previous) if page.paging.has_previous else None ) except errors.NotFoundError: raise except Exception as e: logger.error(e, exc_info=True) raise errors.DatabaseError( "Unhandled database error when getting collection children" ) links = [] if page.next: links.append( PaginationLink( rel=Relations.next, type="application/geo+json", href=f"{kwargs['request'].base_url}collections/{id}/items?token={page.next}&limit={limit}", method="GET", ) ) if page.previous: links.append( PaginationLink( rel=Relations.previous, type="application/geo+json", href=f"{kwargs['request'].base_url}collections/{id}/items?token={page.previous}&limit={limit}", method="GET", ) ) response_features = [] for item in page: item.base_url = str(kwargs["request"].base_url) response_features.append(schemas.Item.from_orm(item)) context_obj = None if config.settings.api_extension_is_enabled(ApiExtensions.context): context_obj = {"returned": len(page), "limit": limit, "matched": count} return ItemCollection( type="FeatureCollection", context=context_obj, features=response_features, links=links, )