Skip to content

Commit f09feb7

Browse files
author
Kevin
authored
[DEV-13086] Gallery SDK update (#371)
1 parent 4a44be7 commit f09feb7

5 files changed

Lines changed: 213 additions & 29 deletions

File tree

indico/client/client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from typing_extensions import Self
1919

20-
from indico.client.request import PagedRequest
20+
from indico.client.request import PagedRequest, PagedRequestV2
2121
from indico.typing import Payload
2222

2323
ReturnType = TypeVar("ReturnType")
@@ -115,7 +115,9 @@ def call(
115115
"Invalid request type! Must be one of HTTPRequest or RequestChain."
116116
)
117117

118-
def paginate(self, request: "PagedRequest[ReturnType]") -> "Iterator[ReturnType]":
118+
def paginate(
119+
self, request: "PagedRequest[ReturnType] | PagedRequestV2[ReturnType]"
120+
) -> "Iterator[ReturnType]":
119121
"""
120122
Provides a generator that continues paging through responses
121123
Available with List<> Requests that offer pagination

indico/client/request.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,64 @@ def parse_payload(
128128
return raw_response
129129

130130

131+
class PagedRequestV2(GraphQLRequest[ResponseType]):
132+
"""
133+
A new version of PagedRequest that supports pagination using cursor and total.
134+
The GraphQL query should follow this structure:
135+
query Name(
136+
...
137+
$cursor: String
138+
){
139+
items(
140+
...
141+
cursor: $cursor
142+
){
143+
items {...}
144+
cursor
145+
total
146+
}
147+
}
148+
"""
149+
150+
def __init__(self, query: str, variables: "Optional[AnyDict]" = None):
151+
if variables is None:
152+
variables = {}
153+
154+
variables["cursor"] = None
155+
self.has_next_page = True
156+
self.total = 0
157+
super().__init__(query, variables=variables)
158+
159+
def parse_payload(
160+
self, response: "AnyDict", nested_keys: "Optional[List[str]]" = None
161+
) -> "Any":
162+
raw_response: "AnyDict" = cast("AnyDict", super().parse_payload(response))
163+
164+
if nested_keys:
165+
composite = raw_response
166+
for key in nested_keys:
167+
if key not in composite.keys():
168+
raise IndicoInputError(
169+
f"Nested key not found in response: {key}",
170+
)
171+
composite = composite[key]
172+
173+
pagination_data = composite
174+
else:
175+
pagination_data = next(iter(raw_response.values()))
176+
177+
if "cursor" not in pagination_data or "total" not in pagination_data:
178+
raise ValueError(
179+
"The supplied GraphQL query should respond with 'cursor' and 'total' fields."
180+
)
181+
182+
self.total = pagination_data["total"]
183+
self.has_next_page = pagination_data["cursor"] is not None
184+
cast("AnyDict", self.variables)["cursor"] = pagination_data["cursor"]
185+
186+
return raw_response
187+
188+
131189
class RequestChain(Generic[ResponseType]):
132190
previous: "Any" = None
133191
result: "Optional[ResponseType]" = None

indico/queries/gallery.py

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,36 @@
11
from typing import TYPE_CHECKING
22

3-
from indico.client.request import GraphQLRequest, PagedRequest
3+
from indico.client.request import GraphQLRequest, PagedRequestV2
44
from indico.types.component_blueprint import BlueprintPage, BlueprintTags
55

66
if TYPE_CHECKING: # pragma: no cover
77
from typing import Any, Optional
88

9-
from indico.filters import ComponentBlueprintFilter
109
from indico.typing import Payload
1110

1211

13-
class ListGallery(PagedRequest[BlueprintPage]):
12+
class ListGallery(PagedRequestV2[BlueprintPage]):
1413
"""
1514
List all blueprints available in the gallery.
1615
1716
Args:
18-
filters (ComponentBlueprintFilter): filters to apply to the blueprints
17+
filter (GenericScalar): filters to apply to the blueprints
1918
limit (int): maximum number of blueprints to return
2019
order_by (str): order to sort the blueprints by
2120
desc (bool): whether to sort the blueprints in descending order
21+
cursor (str): cursor to start the pagination from
2222
"""
2323

2424
query = """
25-
query getGalleryBlueprints($desc: Boolean, $orderBy: COMPONENTBLUEPRINT_COLUMN_ENUM, $skip: Int, $limit: Int, $after: Int, $before: Int, $filters: ComponentBlueprintFilter) {
25+
query getGalleryBlueprints($asc: Boolean, $cursor: String, $sortBy: String, $size: Int, $filters: GenericScalar) {
2626
gallery {
2727
component {
2828
blueprintsPage(
29-
skip: $skip
30-
before: $before
31-
after: $after
32-
limit: $limit
33-
desc: $desc
34-
orderBy: $orderBy
35-
filters: $filters
29+
asc: $asc
30+
cursor: $cursor
31+
filter: $filters
32+
size: $size
33+
sortBy: $sortBy
3634
) {
3735
componentBlueprints {
3836
id
@@ -45,12 +43,8 @@ class ListGallery(PagedRequest[BlueprintPage]):
4543
tags
4644
modelOptions
4745
}
48-
pageInfo {
49-
startCursor
50-
endCursor
51-
hasNextPage
52-
aggregateCount
53-
}
46+
cursor
47+
total
5448
}
5549
}
5650
}
@@ -59,19 +53,21 @@ class ListGallery(PagedRequest[BlueprintPage]):
5953

6054
def __init__(
6155
self,
62-
filters: "Optional[ComponentBlueprintFilter]" = None,
56+
filters: "Optional[str]" = None,
6357
limit: int = 100,
64-
order_by: str = "ID",
58+
order_by: str = "name",
6559
desc: bool = False,
60+
cursor: "Optional[str]" = None,
6661
**kwargs: "Any",
6762
):
6863
super().__init__(
6964
self.query,
7065
variables={
7166
"filters": filters,
72-
"limit": limit,
73-
"orderBy": order_by,
74-
"desc": desc,
67+
"size": limit,
68+
"sortBy": order_by,
69+
"asc": not desc,
70+
"cursor": cursor,
7571
**kwargs,
7672
},
7773
)
@@ -96,26 +92,36 @@ class GetGalleryTags(GraphQLRequest[BlueprintTags]):
9692
9793
Args:
9894
component_family (str): the family of components to filter by
95+
tag_categories (str): the category(ies) of tags to filter by
9996
"""
10097

10198
query = """
102-
query getGalleryBlueprints($componentFamily: ComponentFamily) {
99+
query getGalleryBlueprints($componentFamily: ComponentFamily, $tagCategories: [BPTagCategory]) {
103100
gallery {
104101
component {
105-
availableTags(componentFamily: $componentFamily) {
102+
availableTags(componentFamily: $componentFamily, tagCategories: $tagCategories) {
106103
tag
107104
value
105+
tagCategory
108106
}
109107
}
110108
}
111109
}
112110
"""
113111

114-
def __init__(self, component_family: "Optional[str]" = None):
112+
def __init__(
113+
self,
114+
component_family: "Optional[str]" = None,
115+
tag_categories: "Optional[list[str]]" = None,
116+
):
115117
self.component_family = component_family
118+
self.tag_categories = tag_categories
116119
super().__init__(
117120
self.query,
118-
variables={"componentFamily": component_family},
121+
variables={
122+
"componentFamily": component_family,
123+
"tagCategories": tag_categories,
124+
},
119125
)
120126

121127
def process_response(self, response: "Payload") -> "BlueprintTags":

indico/types/component_blueprint.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def get_by_tags(self, tags: List[str]) -> Optional[ComponentBlueprint]:
7474
class Tag(BaseType):
7575
tag: str
7676
value: str
77+
tag_category: str
7778

7879

7980
class BlueprintTags(BaseType):
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import pytest
2+
from indico.client import IndicoClient
3+
from indico.queries.gallery import ListGallery, GetGalleryTags
4+
from indico.types.component_blueprint import BlueprintPage, BlueprintTags
5+
6+
7+
def test_list_gallery(indico):
8+
"""Test listing all blueprints from the gallery."""
9+
client = IndicoClient()
10+
11+
bppage = client.call(ListGallery())
12+
assert isinstance(bppage, BlueprintPage)
13+
assert hasattr(bppage, "blueprints")
14+
15+
if bppage.blueprints:
16+
blueprint = bppage.blueprints[0]
17+
assert hasattr(blueprint, "id")
18+
assert hasattr(blueprint, "name")
19+
assert hasattr(blueprint, "component_type")
20+
assert hasattr(blueprint, "icon")
21+
assert hasattr(blueprint, "description")
22+
assert hasattr(blueprint, "enabled")
23+
assert hasattr(blueprint, "footer")
24+
assert hasattr(blueprint, "tags")
25+
assert hasattr(blueprint, "model_options")
26+
27+
28+
def test_list_gallery_with_args(indico):
29+
"""Test listing gallery blueprints with variables."""
30+
client = IndicoClient()
31+
32+
limit_bppage = client.call(ListGallery(limit=5))
33+
assert isinstance(limit_bppage, BlueprintPage)
34+
assert len(limit_bppage.blueprints) <= 5
35+
36+
desc_bppage = client.call(ListGallery(order_by="name", desc=True))
37+
assert isinstance(desc_bppage, BlueprintPage)
38+
39+
filters = {
40+
"op": "and",
41+
"filters": [
42+
{
43+
"op": "eq",
44+
"field": "component_type",
45+
"value": "model_group",
46+
}
47+
],
48+
}
49+
filtered_bppage = client.call(ListGallery(filters=filters))
50+
assert isinstance(filtered_bppage, BlueprintPage)
51+
filtered_component_bps = filtered_bppage.blueprints
52+
assert filtered_component_bps[0].component_type == "MODEL_GROUP"
53+
assert filtered_component_bps[-1].component_type == "MODEL_GROUP"
54+
55+
56+
def test_list_gallery_pagination(indico):
57+
"""Test gallery pagination functionality using the new cursor-based pagination."""
58+
client = IndicoClient()
59+
blueprints = []
60+
num_pages_to_check = 3 # Check 3 pages of results
61+
62+
# Use paginate to get multiple pages
63+
for i, page in enumerate(client.paginate(ListGallery(limit=2))):
64+
assert isinstance(page, BlueprintPage)
65+
assert len(page.blueprints) <= 2
66+
blueprints.extend(page.blueprints)
67+
68+
if i >= num_pages_to_check - 1:
69+
break
70+
71+
# Verify we got results
72+
assert blueprints is not None
73+
assert len(blueprints) > 0
74+
75+
# Verify we got different results on each page
76+
if len(blueprints) >= 2:
77+
# Check that we have unique blueprints
78+
blueprint_ids = [bp.id for bp in blueprints]
79+
assert len(blueprint_ids) == len(set(blueprint_ids)), (
80+
"Duplicate blueprints found in pagination"
81+
)
82+
83+
84+
def test_get_gallery_tags(indico):
85+
"""Test retrieving gallery tags."""
86+
client = IndicoClient()
87+
88+
# Test getting all tags
89+
tags = client.call(GetGalleryTags())
90+
assert isinstance(tags, BlueprintTags)
91+
assert hasattr(tags, "tags")
92+
93+
# Verify tag structure
94+
if tags.tags:
95+
tag = tags.tags[0]
96+
assert hasattr(tag, "tag")
97+
assert hasattr(tag, "value")
98+
assert hasattr(tag, "tag_category")
99+
100+
101+
def test_get_gallery_tags_with_args(indico):
102+
"""Test retrieving gallery tags with filters."""
103+
client = IndicoClient()
104+
105+
# Test with component family filter
106+
tags = client.call(GetGalleryTags(component_family="MODEL"))
107+
assert isinstance(tags, BlueprintTags)
108+
109+
# Test with tag categories filter
110+
tags = client.call(GetGalleryTags(tag_categories=["PROVIDER"]))
111+
assert isinstance(tags, BlueprintTags)
112+
113+
# Test with both filters
114+
tags = client.call(
115+
GetGalleryTags(component_family="MODEL", tag_categories=["PROVIDER"])
116+
)
117+
assert isinstance(tags, BlueprintTags)

0 commit comments

Comments
 (0)