Skip to content

Commit 3f2fe01

Browse files
authored
Add pagination (#9)
* bulk ingest test data * use pgstac 0.9.0 * add tests for pagination * add pagination
1 parent ac096f2 commit 3f2fe01

File tree

6 files changed

+152
-48
lines changed

6 files changed

+152
-48
lines changed

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ services:
2929
command: bash -c "./scripts/wait-for-it.sh database:5432 && python -m stac_fastapi_pgstac_pair_search.app"
3030

3131
database:
32-
image: ghcr.io/stac-utils/pgstac:v0.9.2
32+
image: ghcr.io/stac-utils/pgstac:v0.9.8
3333
environment:
3434
- POSTGRES_USER=username
3535
- POSTGRES_PASSWORD=password

scripts/ingest_test_data.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,21 @@ def ingest_data(
8989
post_or_put(urljoin(app_host, "/collections"), json.loads(src.read()))
9090

9191
# ingest downloaded items
92-
for item_json in tqdm.tqdm(
93-
os.listdir(collection_dir + "/items"), desc="ingesting items", unit=" items"
94-
):
95-
with open(f"{collection_dir}/items/{item_json}", "r") as src:
96-
post_or_put(
97-
urljoin(app_host, f"/collections/{collection_name}/items"),
98-
json.loads(src.read()),
92+
item_collection = {
93+
"type": "FeatureCollection",
94+
"features": [
95+
json.loads(open(f"{collection_dir}/items/{item_json}").read())
96+
for item_json in tqdm.tqdm(
97+
os.listdir(collection_dir + "/items"),
98+
desc="ingesting items",
99+
unit=" items",
99100
)
101+
],
102+
}
103+
post_or_put(
104+
urljoin(app_host, f"/collections/{collection_name}/items"),
105+
item_collection,
106+
)
100107

101108
# load queryables
102109
print("Loading queryables")

src/stac_fastapi_pgstac_pair_search/client.py

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import annotations
2+
import json
23
import logging
34
from pathlib import Path
45
from typing import Dict, Any, Optional, Set, List, Tuple
@@ -12,7 +13,7 @@
1213
from stac_fastapi.pgstac.app import StacApi
1314
from stac_fastapi.pgstac.config import Settings
1415
from stac_fastapi.pgstac.core import CoreCrudClient
15-
from stac_fastapi.pgstac.models.links import ItemLinks, PagingLinks
16+
from stac_fastapi.pgstac.models.links import ItemLinks
1617
from stac_fastapi.pgstac.utils import filter_fields
1718
from stac_fastapi.types.errors import InvalidQueryParameter
1819
from stac_fastapi.types.stac import ItemCollection
@@ -122,17 +123,6 @@ async def _pair_search_base(
122123
raise InvalidQueryParameter(
123124
f"Datetime parameter {search_request.datetime} is invalid."
124125
) from e
125-
# Starting in pgstac 0.9.0, the `next` and `prev` tokens are returned in spec-compliant links with method GET
126-
next_from_link: Optional[str] = None
127-
prev_from_link: Optional[str] = None
128-
for link in items.get("links", []):
129-
if link.get("rel") == "next":
130-
next_from_link = link.get("href").split("token=next:")[1]
131-
if link.get("rel") == "prev":
132-
prev_from_link = link.get("href").split("token=prev:")[1]
133-
134-
next: Optional[str] = items.pop("next", next_from_link)
135-
prev: Optional[str] = items.pop("prev", prev_from_link)
136126
collection = ItemCollection(**items)
137127

138128
fields = getattr(search_request, "fields", None)
@@ -191,13 +181,70 @@ async def _get_base_item(collection_id: str) -> Dict[str, Any]:
191181
cleaned_features.append(feature)
192182

193183
collection["features"] = cleaned_features
194-
collection["links"] = await PagingLinks(
195-
request=request,
196-
next=next,
197-
prev=prev,
198-
).get_links()
184+
if cleaned_features:
185+
collection["links"] = await self._get_search_links(
186+
search_request=search_request, request=request
187+
)
188+
199189
return collection
200190

191+
async def _get_search_links(
192+
self, search_request: PairSearchRequest, request: Request
193+
) -> List[Dict[str, str]]:
194+
"""Take existing request and edit offset."""
195+
links = []
196+
next_page_offset = search_request.offset + search_request.limit
197+
prev_page_offset = max(search_request.offset - search_request.limit, 0)
198+
if request.method == "GET":
199+
query_params = request.query_params.multi_items()
200+
links.append(
201+
{
202+
"rel": "next",
203+
"href": request.url.replace_query_params(
204+
**dict(query_params, offset=next_page_offset)
205+
),
206+
"type": "application/geo+json",
207+
}
208+
)
209+
# add link to previous page
210+
if search_request.offset:
211+
links.append(
212+
{
213+
"rel": "prev",
214+
"href": request.url.replace_query_params(
215+
**dict(
216+
query_params,
217+
offset=prev_page_offset,
218+
)
219+
),
220+
"type": "application/geo+json",
221+
}
222+
)
223+
elif request.method == "POST":
224+
body = await request.body()
225+
links.append(
226+
{
227+
"rel": "next",
228+
"href": request.url,
229+
"type": "application/geo+json",
230+
"method": "POST",
231+
"body": dict(json.loads(body), offset=next_page_offset),
232+
}
233+
)
234+
# add link to previous page
235+
if search_request.offset:
236+
links.append(
237+
{
238+
"rel": "prev",
239+
"href": request.url,
240+
"type": "application/geo+json",
241+
"method": "POST",
242+
"body": dict(json.loads(body), offset=prev_page_offset),
243+
}
244+
)
245+
246+
return links
247+
201248

202249
def render_sql(pair_search_request: PairSearchRequest) -> Tuple[str, List[Any]]:
203250
filter_query, filter_params = pair_search_request.filter_sql
@@ -212,6 +259,7 @@ def render_sql(pair_search_request: PairSearchRequest) -> Tuple[str, List[Any]]:
212259
exclude_none=True, by_alias=True
213260
),
214261
limit=pair_search_request.limit or 10,
262+
offset=pair_search_request.offset or 0,
215263
response_type=pair_search_request.response_type,
216264
**filter_params,
217265
)

src/stac_fastapi_pgstac_pair_search/models.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import cql2
66
from fastapi import Query
7-
from pydantic import Field, AfterValidator, BaseModel, model_validator
7+
from pydantic import Field, AfterValidator, BaseModel, model_validator, NonNegativeInt
88
from datetime import datetime as dt
99
from stac_fastapi.extensions.core.filter.request import FilterLang
1010
from stac_fastapi.types.search import Limit, APIRequest, BaseSearchPostRequest
@@ -42,6 +42,11 @@ class PairSearchRequest(BaseModel, APIRequest):
4242
description="Limits the number of results that are included in each page of the response (capped to 10_000).", # noqa: E501
4343
)
4444

45+
offset: Optional[NonNegativeInt] = Field(
46+
0,
47+
description="Offset from the first record. Can be used to paginate results.",
48+
)
49+
4550
first_bbox: Annotated[Optional[BBox], AfterValidator(validate_bbox)] = Field(
4651
alias="first-bbox", default=None
4752
)

src/stac_fastapi_pgstac_pair_search/sql/pair_search.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ all_pairs AS (
1717
limited_pairs AS (
1818
SELECT id1, id2, first, second
1919
FROM all_pairs
20-
OFFSET 0
20+
OFFSET :offset
2121
LIMIT :limit
2222
),
2323
all_features AS (

0 commit comments

Comments
 (0)