-
Notifications
You must be signed in to change notification settings - Fork 108
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Return "raw" API parameters in pagination (#3412)
* Return "raw" API parameters in pagination PBENCH-1133 The `GET /datasets` response is optimized for sequential pagination, providing a convenient "next_url" string that can be used directly. However if a client wants to support "random access" pagination, this requires that the client parses the URL string in order to modify the `offset` parameter. This attempts to make that a bit easier by supplementing the current response payload with a `parameters` field containing the query parameters JSON object, making it easy to update the `offset` parameter. (Making the unit tests work against the normalized parameter list proved a bit challenging and I ended up saving the original "raw" client parameters in the API `context` so they can be used directly.)
- Loading branch information
Showing
3 changed files
with
82 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import datetime | ||
from http import HTTPStatus | ||
import re | ||
from typing import Optional | ||
from typing import Any, Optional | ||
|
||
import pytest | ||
import requests | ||
|
@@ -10,7 +10,7 @@ | |
from sqlalchemy.orm import aliased, Query | ||
|
||
from pbench.server import JSON, JSONARRAY, JSONOBJECT | ||
from pbench.server.api.resources import APIAbort | ||
from pbench.server.api.resources import APIAbort, ApiParams | ||
from pbench.server.api.resources.datasets_list import DatasetsList, urlencode_json | ||
from pbench.server.database.database import Database | ||
from pbench.server.database.models.datasets import Dataset, Metadata | ||
|
@@ -129,17 +129,27 @@ def get_results(self, name_list: list[str], query: JSON, server_config) -> JSON: | |
Returns: | ||
Paginated JSON object containing list of dataset values | ||
""" | ||
|
||
def convert(k: str, v: Any) -> Any: | ||
if isinstance(v, str) and k in ("filter", "sort", "metadata"): | ||
return [v] | ||
elif isinstance(v, int): | ||
return str(v) | ||
else: | ||
return v | ||
|
||
results: list[JSON] = [] | ||
offset = query.get("offset", 0) | ||
offset = int(query.get("offset", "0")) | ||
limit = query.get("limit") | ||
|
||
if limit: | ||
next_offset = offset + limit | ||
next_offset = offset + int(limit) | ||
paginated_name_list = name_list[offset:next_offset] | ||
if next_offset >= len(name_list): | ||
query["offset"] = str(len(name_list)) | ||
next_url = "" | ||
else: | ||
query["offset"] = next_offset | ||
query["offset"] = str(next_offset) | ||
next_url = ( | ||
f"http://localhost{server_config.rest_uri}/datasets?" | ||
+ urlencode_json(query) | ||
|
@@ -161,7 +171,15 @@ def get_results(self, name_list: list[str], query: JSON, server_config) -> JSON: | |
}, | ||
} | ||
) | ||
return {"next_url": next_url, "results": results, "total": len(name_list)} | ||
q1 = {k: convert(k, v) for k, v in query.items()} | ||
if "metadata" not in q1: | ||
q1["metadata"] = ["dataset.uploaded"] | ||
return { | ||
"parameters": q1, | ||
"next_url": next_url, | ||
"results": results, | ||
"total": len(name_list), | ||
} | ||
|
||
def compare_results( | ||
self, result: JSONOBJECT, name_list: list[str], query: JSON, server_config | ||
|
@@ -190,8 +208,8 @@ def compare_results( | |
(None, {}, ["fio_1", "fio_2"]), | ||
(None, {"access": "public"}, ["fio_1", "fio_2"]), | ||
("drb", {"name": "fio"}, ["fio_1", "fio_2"]), | ||
("drb", {"name": "fio", "limit": 1}, ["fio_1", "fio_2"]), | ||
("drb", {"name": "fio", "limit": 1, "offset": 2}, ["fio_1", "fio_2"]), | ||
("drb", {"name": "fio", "limit": "1"}, ["fio_1", "fio_2"]), | ||
("drb", {"name": "fio", "limit": 1, "offset": "2"}, ["fio_1", "fio_2"]), | ||
("drb", {"name": "fio", "offset": 1}, ["fio_1", "fio_2"]), | ||
("drb", {"name": "fio", "offset": 2}, ["fio_1", "fio_2"]), | ||
("drb", {"owner": "drb"}, ["drb", "fio_1"]), | ||
|
@@ -256,6 +274,17 @@ def compare_results( | |
def test_dataset_list(self, server_config, query_as, login, query, results): | ||
"""Test `datasets/list` filters | ||
NOTE: Several of these queries use the "limit" and/or "offset" options | ||
to test how the result set is segmented during pagination. These are | ||
represented in the parametrization above interchangeably as integers or | ||
strings. This is because (1) the actual input to the Pbench Server API | ||
is always in string form as a URI query parameter but (2) the requests | ||
package understands this and stringifies integer parameters while (3) | ||
the Pbench Server API framework recognizes these are integer values and | ||
presents them to the API code as integers. Mixing integer and string | ||
representation here must have no impact on the operation of the API so | ||
it's worth testing. | ||
Args: | ||
server_config: The PbenchServerConfig object | ||
query_as: A fixture to provide a helper that executes the API call | ||
|
@@ -311,7 +340,9 @@ def test_mine_novalue(self, server_config, client, more_datasets, get_token_func | |
headers=headers, | ||
) | ||
assert response.status_code == HTTPStatus.OK | ||
self.compare_results(response.json, ["drb", "fio_1"], {}, server_config) | ||
self.compare_results( | ||
response.json, ["drb", "fio_1"], {"mine": ""}, server_config | ||
) | ||
|
||
@pytest.mark.parametrize( | ||
"login,query,results", | ||
|
@@ -336,7 +367,7 @@ def test_dataset_paged_list(self, query_as, login, query, results, server_config | |
results: A list of the dataset names we expect to be returned | ||
server_config: The PbenchServerConfig object | ||
""" | ||
query.update({"metadata": ["dataset.uploaded"], "limit": 5}) | ||
query.update({"metadata": ["dataset.uploaded"], "limit": "5"}) | ||
result = query_as(query, login, HTTPStatus.OK) | ||
self.compare_results(result.json, results, query, server_config) | ||
|
||
|
@@ -384,6 +415,7 @@ def test_get_key_errors(self, query_as): | |
) | ||
assert response.json == { | ||
"next_url": "", | ||
"parameters": {"metadata": ["global.test.foo"]}, | ||
"results": [ | ||
{ | ||
"metadata": {"global.test.foo": None}, | ||
|
@@ -444,6 +476,12 @@ def test_use_funk_metalog_keys(self, query_as): | |
) | ||
assert response.json == { | ||
"next_url": "", | ||
"parameters": { | ||
"filter": [ | ||
"dataset.metalog.iterations/[email protected]_name:~10" | ||
], | ||
"metadata": ["dataset.metalog.iterations/fooBar=10-what_else@weird"], | ||
}, | ||
"results": [ | ||
{ | ||
"metadata": { | ||
|
@@ -725,7 +763,7 @@ def test_mismatched_json_cast(self, query_as, server_config, query, results): | |
"drb", | ||
HTTPStatus.OK, | ||
) | ||
self.compare_results(response.json, results, {}, server_config) | ||
self.compare_results(response.json, results, {"filter": query}, server_config) | ||
|
||
@pytest.mark.parametrize( | ||
"query,message", | ||
|
@@ -769,7 +807,7 @@ def test_pagination_error(self, caplog, monkeypatch, query_as, exception, error) | |
""" | ||
|
||
def do_error( | ||
self, query: Query, json: JSONOBJECT, url: str | ||
self, query: Query, json: JSONOBJECT, raw_params: ApiParams, url: str | ||
) -> tuple[JSONARRAY, JSONOBJECT]: | ||
raise exception | ||
|
||
|