Skip to content

Commit ccbf496

Browse files
add OffsetPaginationExtension extension (#757)
1 parent 0ce70ba commit ccbf496

File tree

6 files changed

+195
-2
lines changed

6 files changed

+195
-2
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
* Removed `cql2-text` in supported `filter-lang` for `FilterExtensionPostRequest` model (as per specification)
88

9+
### Added
10+
11+
* Add `OffsetPaginationExtension` extension to add `offset` query/body parameter to endpoints
12+
913
## [3.0.2] - 2024-09-20
1014

1115
### Added

stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
from .fields import FieldsExtension
66
from .filter import FilterExtension
77
from .free_text import FreeTextAdvancedExtension, FreeTextExtension
8-
from .pagination import PaginationExtension, TokenPaginationExtension
8+
from .pagination import (
9+
OffsetPaginationExtension,
10+
PaginationExtension,
11+
TokenPaginationExtension,
12+
)
913
from .query import QueryExtension
1014
from .sort import SortExtension
1115
from .transaction import TransactionExtension
@@ -16,6 +20,7 @@
1620
"FilterExtension",
1721
"FreeTextExtension",
1822
"FreeTextAdvancedExtension",
23+
"OffsetPaginationExtension",
1924
"PaginationExtension",
2025
"QueryExtension",
2126
"SortExtension",
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Pagination classes as extensions."""
22

3+
from .offset_pagination import OffsetPaginationExtension
34
from .pagination import PaginationExtension
45
from .token_pagination import TokenPaginationExtension
56

6-
__all__ = ["PaginationExtension", "TokenPaginationExtension"]
7+
__all__ = ["OffsetPaginationExtension", "PaginationExtension", "TokenPaginationExtension"]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Pagination API extension."""
2+
3+
from typing import List, Optional
4+
5+
import attr
6+
from fastapi import FastAPI
7+
8+
from stac_fastapi.types.extension import ApiExtension
9+
10+
from .request import GETOffsetPagination, POSTOffsetPagination
11+
12+
13+
@attr.s
14+
class OffsetPaginationExtension(ApiExtension):
15+
"""Offset Pagination.
16+
17+
Though not strictly an extension, the chosen pagination will modify the form of the
18+
request object. By making pagination an extension class, we can use
19+
create_request_model to dynamically add the correct pagination parameter to the
20+
request model for OpenAPI generation.
21+
"""
22+
23+
GET = GETOffsetPagination
24+
POST = POSTOffsetPagination
25+
26+
conformance_classes: List[str] = attr.ib(factory=list)
27+
schema_href: Optional[str] = attr.ib(default=None)
28+
29+
def register(self, app: FastAPI) -> None:
30+
"""Register the extension with a FastAPI application.
31+
32+
Args:
33+
app: target FastAPI application.
34+
35+
Returns:
36+
None
37+
"""
38+
pass

stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/request.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,16 @@ class POSTPagination(BaseModel):
3434
"""Page based pagination for POST requests."""
3535

3636
page: Optional[str] = None
37+
38+
39+
@attr.s
40+
class GETOffsetPagination(APIRequest):
41+
"""Offset pagination for GET requests."""
42+
43+
offset: Annotated[Optional[int], Query()] = attr.ib(default=None)
44+
45+
46+
class POSTOffsetPagination(BaseModel):
47+
"""Offset pagination model for POST requests."""
48+
49+
offset: Optional[int] = None
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from typing import Iterator
2+
3+
import pytest
4+
from starlette.testclient import TestClient
5+
6+
from stac_fastapi.api.app import StacApi
7+
from stac_fastapi.api.models import (
8+
EmptyRequest,
9+
create_post_request_model,
10+
create_request_model,
11+
)
12+
from stac_fastapi.extensions.core import (
13+
OffsetPaginationExtension,
14+
PaginationExtension,
15+
TokenPaginationExtension,
16+
)
17+
from stac_fastapi.types.config import ApiSettings
18+
from stac_fastapi.types.core import BaseCoreClient
19+
from stac_fastapi.types.search import BaseSearchGetRequest
20+
21+
22+
class DummyCoreClient(BaseCoreClient):
23+
def all_collections(self, *args, **kwargs):
24+
_ = kwargs.pop("request", None)
25+
return args, kwargs
26+
27+
def get_collection(self, *args, **kwargs):
28+
_ = kwargs.pop("request", None)
29+
return args, kwargs
30+
31+
def get_item(self, *args, **kwargs):
32+
_ = kwargs.pop("request", None)
33+
return args, kwargs
34+
35+
def get_search(self, *args, **kwargs):
36+
_ = kwargs.pop("request", None)
37+
return args, kwargs
38+
39+
def post_search(self, *args, **kwargs):
40+
_ = kwargs.pop("request", None)
41+
return args[0].model_dump(), kwargs
42+
43+
def item_collection(self, *args, **kwargs):
44+
_ = kwargs.pop("request", None)
45+
return args, kwargs
46+
47+
48+
collections_get_request_model = create_request_model(
49+
model_name="CollectionsGetRequest",
50+
base_model=EmptyRequest,
51+
mixins=[
52+
OffsetPaginationExtension().GET,
53+
],
54+
request_type="GET",
55+
)
56+
57+
items_get_request_model = create_request_model(
58+
model_name="ItemsGetRequest",
59+
base_model=EmptyRequest,
60+
mixins=[
61+
PaginationExtension().GET,
62+
],
63+
request_type="GET",
64+
)
65+
66+
search_get_request_model = create_request_model(
67+
model_name="SearchGetRequest",
68+
base_model=BaseSearchGetRequest,
69+
mixins=[
70+
TokenPaginationExtension().GET,
71+
],
72+
request_type="GET",
73+
)
74+
75+
76+
@pytest.fixture
77+
def client() -> Iterator[TestClient]:
78+
settings = ApiSettings()
79+
80+
api = StacApi(
81+
settings=settings,
82+
client=DummyCoreClient(),
83+
extensions=[],
84+
collections_get_request_model=collections_get_request_model,
85+
items_get_request_model=items_get_request_model,
86+
search_get_request_model=search_get_request_model,
87+
search_post_request_model=create_post_request_model([]),
88+
)
89+
with TestClient(api.app) as client:
90+
yield client
91+
92+
93+
def test_pagination_extension(client: TestClient):
94+
"""Test endpoints with pagination extensions."""
95+
# OffsetPaginationExtension
96+
response = client.get("/collections")
97+
assert response.is_success, response.json()
98+
arg, kwargs = response.json()
99+
assert "offset" in kwargs
100+
assert kwargs["offset"] is None
101+
102+
response = client.get("/collections", params={"offset": 1})
103+
assert response.is_success, response.json()
104+
arg, kwargs = response.json()
105+
assert "offset" in kwargs
106+
assert kwargs["offset"] == 1
107+
108+
# PaginationExtension
109+
response = client.get("/collections/a_collection/items")
110+
assert response.is_success, response.json()
111+
arg, kwargs = response.json()
112+
assert "page" in kwargs
113+
assert kwargs["page"] is None
114+
115+
response = client.get("/collections/a_collection/items", params={"page": "1"})
116+
assert response.is_success, response.json()
117+
arg, kwargs = response.json()
118+
assert "page" in kwargs
119+
assert kwargs["page"] == "1"
120+
121+
# TokenPaginationExtension
122+
response = client.get("/search")
123+
assert response.is_success, response.json()
124+
arg, kwargs = response.json()
125+
assert "token" in kwargs
126+
assert kwargs["token"] is None
127+
128+
response = client.get("/search", params={"token": "atoken"})
129+
assert response.is_success, response.json()
130+
arg, kwargs = response.json()
131+
assert "token" in kwargs
132+
assert kwargs["token"] == "atoken"

0 commit comments

Comments
 (0)