Skip to content

Commit 70fcf75

Browse files
committed
perf(ctl): request larger page size when paginating marketplace listings
Bulk list/search fetches inherited the marketplace's UI-tuned default page size, causing many round-trips over a large catalog. Request 100 items per page while paginating, kept separate from the user-facing --limit.
1 parent 195c382 commit 70fcf75

2 files changed

Lines changed: 23 additions & 18 deletions

File tree

infrahub_sdk/ctl/marketplace.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424

2525
MarketplaceItemType = Literal["schema", "collection"]
2626

27+
# Per-request page size used when paginating through a full listing (no user
28+
# --limit). Set explicitly so bulk fetches don't inherit the marketplace's
29+
# UI-tuned default page size, which would mean many more round-trips.
30+
_PAGE_SIZE = 100
31+
2732

2833
class _ErrorClass(Enum):
2934
INVALID_INPUT = "invalid-input"
@@ -90,15 +95,15 @@ async def _fetch_listing(
9095
"""Fetch marketplace listing items, following cursor pagination.
9196
9297
When ``limit`` is given, a single page of that size is requested (no cursor
93-
loop). Otherwise every page is fetched until ``has_next_page`` is false.
94-
Returns the accumulated items and the reported ``total_count``.
98+
loop). Otherwise every page is fetched — ``_PAGE_SIZE`` items at a time — until
99+
``has_next_page`` is false. Returns the accumulated items and the reported
100+
``total_count``.
95101
"""
96102
url = _list_url(base_url, item_type)
97-
params: dict[str, Any] = {}
103+
single_page = limit is not None
104+
params: dict[str, Any] = {"limit": limit if single_page else _PAGE_SIZE}
98105
if search:
99106
params["search"] = search
100-
if limit is not None:
101-
params["limit"] = limit
102107

103108
items: list[dict[str, Any]] = []
104109
total_count = 0
@@ -115,7 +120,7 @@ async def _fetch_listing(
115120
# Stop when a single page was requested, when the server reports no more
116121
# pages, or when it claims a next page but gives no cursor to follow
117122
# (guards against an infinite loop re-requesting the same page).
118-
if limit is not None or not page_info.get("has_next_page") or not cursor:
123+
if single_page or not page_info.get("has_next_page") or not cursor:
119124
break
120125
params = {**params, "cursor": cursor}
121126
return items, total_count

tests/unit/ctl/test_marketplace_app.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ def _schema_item(namespace: str, name: str, *, display: str, semver: str, downlo
594594
def test_list_schemas(httpx_mock: HTTPXMock) -> None:
595595
httpx_mock.add_response(
596596
method="GET",
597-
url="https://marketplace.infrahub.app/api/v1/schemas",
597+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100",
598598
json=_listing_json(
599599
"schemas",
600600
[_schema_item("infrahub", "dcim", display="DCIM", semver="1.2.0", downloads=42, tags=["core"])],
@@ -624,7 +624,7 @@ def _collection_item(namespace: str, name: str, *, display: str, schema_count: i
624624
def test_list_collections(httpx_mock: HTTPXMock) -> None:
625625
httpx_mock.add_response(
626626
method="GET",
627-
url="https://marketplace.infrahub.app/api/v1/collections",
627+
url="https://marketplace.infrahub.app/api/v1/collections?limit=100",
628628
json=_listing_json(
629629
"collections",
630630
[_collection_item("infrahub", "security-mgmt", display="Security", schema_count=5, downloads=7)],
@@ -643,7 +643,7 @@ def test_list_collections(httpx_mock: HTTPXMock) -> None:
643643
def test_list_follows_cursor_pagination(httpx_mock: HTTPXMock) -> None:
644644
httpx_mock.add_response(
645645
method="GET",
646-
url="https://marketplace.infrahub.app/api/v1/schemas",
646+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100",
647647
json=_listing_json(
648648
"schemas",
649649
[_schema_item("infrahub", "a", display="A", semver="1.0.0", downloads=1, tags=[])],
@@ -653,7 +653,7 @@ def test_list_follows_cursor_pagination(httpx_mock: HTTPXMock) -> None:
653653
)
654654
httpx_mock.add_response(
655655
method="GET",
656-
url="https://marketplace.infrahub.app/api/v1/schemas?cursor=CURSOR1",
656+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100&cursor=CURSOR1",
657657
json=_listing_json(
658658
"schemas",
659659
[_schema_item("infrahub", "b", display="B", semver="1.0.0", downloads=1, tags=[])],
@@ -690,7 +690,7 @@ def test_list_limit_requests_single_page_and_shows_footer(httpx_mock: HTTPXMock)
690690
def test_list_network_error_exits_2(httpx_mock: HTTPXMock) -> None:
691691
httpx_mock.add_response(
692692
method="GET",
693-
url="https://marketplace.infrahub.app/api/v1/schemas",
693+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100",
694694
status_code=503,
695695
json={"detail": "unavailable"},
696696
)
@@ -703,7 +703,7 @@ def test_list_network_error_exits_2(httpx_mock: HTTPXMock) -> None:
703703
def test_search_passes_term_and_renders(httpx_mock: HTTPXMock) -> None:
704704
httpx_mock.add_response(
705705
method="GET",
706-
url="https://marketplace.infrahub.app/api/v1/schemas?search=vlan",
706+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100&search=vlan",
707707
json=_listing_json(
708708
"schemas",
709709
[_schema_item("infrahub", "vlan", display="VLAN", semver="1.0.0", downloads=3, tags=[])],
@@ -719,7 +719,7 @@ def test_search_passes_term_and_renders(httpx_mock: HTTPXMock) -> None:
719719
def test_search_empty_results(httpx_mock: HTTPXMock) -> None:
720720
httpx_mock.add_response(
721721
method="GET",
722-
url="https://marketplace.infrahub.app/api/v1/schemas?search=nomatch",
722+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100&search=nomatch",
723723
json=_listing_json("schemas", [], total=0),
724724
)
725725
result = runner.invoke(app, ["search", "nomatch"])
@@ -732,7 +732,7 @@ def test_search_empty_results(httpx_mock: HTTPXMock) -> None:
732732
def test_list_json_output_is_parseable(httpx_mock: HTTPXMock) -> None:
733733
httpx_mock.add_response(
734734
method="GET",
735-
url="https://marketplace.infrahub.app/api/v1/schemas",
735+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100",
736736
json=_listing_json(
737737
"schemas",
738738
[_schema_item("infrahub", "dcim", display="DCIM", semver="1.2.0", downloads=42, tags=["core"])],
@@ -932,7 +932,7 @@ def test_list_4xx_error_exits_1(httpx_mock: HTTPXMock) -> None:
932932
"""A 4xx response on the listing endpoint must exit 1, not 2."""
933933
httpx_mock.add_response(
934934
method="GET",
935-
url="https://marketplace.infrahub.app/api/v1/schemas",
935+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100",
936936
status_code=400,
937937
json={"detail": "Bad Request"},
938938
)
@@ -1028,7 +1028,7 @@ def test_list_stops_when_next_page_has_null_cursor(httpx_mock: HTTPXMock) -> Non
10281028
"""
10291029
httpx_mock.add_response(
10301030
method="GET",
1031-
url="https://marketplace.infrahub.app/api/v1/schemas",
1031+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100",
10321032
json={
10331033
"items": [_schema_item("infrahub", "a", display="A", semver="1.0.0", downloads=1, tags=[])],
10341034
"page_info": {"has_next_page": True, "end_cursor": None},
@@ -1045,7 +1045,7 @@ def test_list_invalid_json_body_is_network_error(httpx_mock: HTTPXMock) -> None:
10451045
"""A 200 response with a non-JSON body is reported as a clean network error (exit 2)."""
10461046
httpx_mock.add_response(
10471047
method="GET",
1048-
url="https://marketplace.infrahub.app/api/v1/schemas",
1048+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100",
10491049
text="<html>not json</html>",
10501050
)
10511051
result = runner.invoke(app, ["list"])
@@ -1058,7 +1058,7 @@ def test_list_non_dict_payload_is_network_error(httpx_mock: HTTPXMock) -> None:
10581058
"""A 200 response whose body is valid JSON but not an object exits 2 cleanly, not a traceback."""
10591059
httpx_mock.add_response(
10601060
method="GET",
1061-
url="https://marketplace.infrahub.app/api/v1/schemas",
1061+
url="https://marketplace.infrahub.app/api/v1/schemas?limit=100",
10621062
json=[{"namespace": "infrahub", "name": "dcim"}],
10631063
)
10641064
result = runner.invoke(app, ["list"])

0 commit comments

Comments
 (0)