Skip to content

Commit 9dc6e78

Browse files
authored
Merge pull request #91 from NorthShoreAutomation/feature/update-search-endpoint-to-better-match-api
feat: add search query params and tests
2 parents bb4884b + 911dd49 commit 9dc6e78

2 files changed

Lines changed: 154 additions & 36 deletions

File tree

pythonik/specs/search.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Union, Dict, Any
1+
from typing import Union, Dict, Any, Optional
22

33
from pythonik.models.base import Response
44
from pythonik.models.search.search_body import SearchBody
@@ -15,24 +15,63 @@ class SearchSpec(Spec):
1515
def search(
1616
self,
1717
search_body: Union[SearchBody, Dict[str, Any]],
18+
per_page: Optional[int] = None,
19+
page: Optional[int] = None,
20+
scroll: Optional[bool] = None, # Deprecated
21+
scroll_id: Optional[str] = None, # Deprecated
22+
generate_signed_url: Optional[bool] = None,
23+
generate_signed_download_url: Optional[bool] = None,
24+
generate_signed_proxy_url: Optional[bool] = None,
25+
save_search_history: Optional[bool] = None,
1826
exclude_defaults: bool = True,
19-
**kwargs
27+
**kwargs,
2028
) -> Response: # Response.data will be SearchResponse
2129
"""
22-
Search iconik
30+
Search iconik.
31+
Corresponds to POST /v1/search/
2332
2433
Args:
25-
search_body: Search parameters, either as SearchBody model or dict
26-
exclude_defaults: Whether to exclude default values when dumping Pydantic models
27-
**kwargs: Additional kwargs to pass to the request
34+
search_body: Search parameters, either as SearchBody model or dict.
35+
per_page: The number of documents for each page.
36+
page: Which page number to fetch.
37+
scroll: If true, uses scroll pagination. (Deprecated, use search_after in body).
38+
scroll_id: Scroll ID for scroll pagination. (Deprecated).
39+
generate_signed_url: Set to false if you don't need a URL, will speed things up.
40+
generate_signed_download_url: Set to true if you also want the file download URLs generated.
41+
generate_signed_proxy_url: Set to true if you want to generate signed download urls for proxies.
42+
save_search_history: Set to false if you don't want to save the search to the history.
43+
exclude_defaults: Whether to exclude default values when dumping Pydantic models for the request body.
44+
**kwargs: Additional kwargs to pass to the request (e.g., headers).
2845
2946
Returns:
30-
Response with SearchResponse data model
47+
Response with SearchResponse data model.
3148
"""
32-
json_data = self._prepare_model_data(search_body, exclude_defaults=exclude_defaults)
49+
json_data = self._prepare_model_data(
50+
search_body, exclude_defaults=exclude_defaults
51+
)
52+
53+
params = {}
54+
if per_page is not None:
55+
params["per_page"] = per_page
56+
if page is not None:
57+
params["page"] = page
58+
if scroll is not None:
59+
params["scroll"] = scroll
60+
if scroll_id is not None:
61+
params["scroll_id"] = scroll_id
62+
if generate_signed_url is not None:
63+
params["generate_signed_url"] = generate_signed_url
64+
if generate_signed_download_url is not None:
65+
params["generate_signed_download_url"] = generate_signed_download_url
66+
if generate_signed_proxy_url is not None:
67+
params["generate_signed_proxy_url"] = generate_signed_proxy_url
68+
if save_search_history is not None:
69+
params["save_search_history"] = save_search_history
70+
3371
resp = self._post(
34-
SEARCH_PATH,
72+
SEARCH_PATH, # Use the new path constant, which is ""
3573
json=json_data,
36-
**kwargs
74+
params=params if params else None,
75+
**kwargs,
3776
)
3877
return self.parse_response(resp, SearchResponse)

pythonik/tests/test_search.py

Lines changed: 105 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,123 @@
11
import uuid
2+
import pytest
23
import requests_mock
3-
from pythonik.client import PythonikClient
4+
# from urllib.parse import parse_qs # Unused import removed
45

5-
from pythonik.models.metadata.views import ViewMetadata
6-
from pythonik.models.mutation.metadata.mutate import (
7-
UpdateMetadata,
8-
UpdateMetadataResponse,
9-
)
6+
from pythonik.client import PythonikClient
107
from pythonik.models.search.search_body import Filter, SearchBody, SortItem, Term
11-
from pythonik.specs.metadata import (
12-
ASSET_METADATA_FROM_VIEW_PATH,
13-
UPDATE_ASSET_METADATA,
14-
MetadataSpec,
15-
)
168
from pythonik.specs.search import SEARCH_PATH, SearchSpec
179

10+
# Unused imports removed by Cascade:
11+
# from pythonik.models.metadata.views import ViewMetadata
12+
# from pythonik.models.mutation.metadata.mutate import (
13+
# UpdateMetadata,
14+
# UpdateMetadataResponse,
15+
# )
16+
# from pythonik.specs.metadata import (
17+
# ASSET_METADATA_FROM_VIEW_PATH,
18+
# UPDATE_ASSET_METADATA,
19+
# MetadataSpec,
20+
# )
1821

19-
def test_search_assets():
22+
def test_search_assets_basic():
23+
"""Test basic search functionality, similar to original test but using named params."""
2024
with requests_mock.Mocker() as m:
2125
app_id = str(uuid.uuid4())
2226
auth_token = str(uuid.uuid4())
2327
asset_id = str(uuid.uuid4())
24-
view_id = str(uuid.uuid4())
28+
# view_id = str(uuid.uuid4()) # Unused variable removed
2529

26-
# needs model
27-
params = {"generate_signed_url": "true", "generate_signed_download_url": "true"}
30+
search_criteria = SearchBody(
31+
doc_types=["assets"],
32+
query=f"id:{asset_id}",
33+
filter=Filter(operator="AND", terms=[Term(name="status", value="active")]),
34+
sort=[SortItem(name="date_created", order="desc")]
35+
)
2836

29-
# search criteria
30-
search_chriteria = SearchBody()
31-
search_chriteria.doc_types = ["assets"]
32-
search_chriteria.query = f"id:{asset_id}"
37+
mock_address = SearchSpec.gen_url(SEARCH_PATH)
38+
# Mock will match the base address; query params will be checked on m.last_request.qs
39+
matcher = m.post(mock_address, json=search_criteria.model_dump())
3340

34-
search_chriteria.filter = Filter(
35-
operator="AND", terms=[Term(name="status", value="active")]
41+
client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3)
42+
client.search().search(
43+
search_body=search_criteria,
44+
generate_signed_url=True,
45+
generate_signed_download_url=True
3646
)
37-
# get only active assets
47+
assert matcher.called_once
48+
expected_qs = {
49+
'generate_signed_url': ['true'],
50+
'generate_signed_download_url': ['true']
51+
}
52+
assert m.last_request.qs == expected_qs
3853

39-
search_chriteria.sort = [SortItem(name="date_created", order="desc")]
4054

41-
mock_address = SearchSpec.gen_url(SEARCH_PATH)
42-
m.post(mock_address, json=search_chriteria.model_dump())
55+
# Test cases for various query parameter combinations
56+
# Each tuple: (test_id, query_params_for_search_method, expected_query_string_dict)
57+
search_param_test_cases = [
58+
(
59+
"pagination",
60+
{"per_page": 20, "page": 3},
61+
{"per_page": ["20"], "page": ["3"]}
62+
),
63+
(
64+
"signed_urls_off_and_proxy_on",
65+
{"generate_signed_url": False, "generate_signed_download_url": False, "generate_signed_proxy_url": True},
66+
{"generate_signed_url": ["false"], "generate_signed_download_url": ["false"], "generate_signed_proxy_url": ["true"]}
67+
),
68+
(
69+
"save_history_off",
70+
{"save_search_history": False},
71+
{"save_search_history": ["false"]}
72+
),
73+
(
74+
"scroll_params_active",
75+
{"scroll": True, "scroll_id": "test_scroll_123"},
76+
{"scroll": ["true"], "scroll_id": ["test_scroll_123"]}
77+
),
78+
(
79+
"all_bools_mixed_values",
80+
{
81+
"generate_signed_url": True,
82+
"generate_signed_download_url": False,
83+
"generate_signed_proxy_url": True,
84+
"save_search_history": False,
85+
},
86+
{
87+
"generate_signed_url": ["true"],
88+
"generate_signed_download_url": ["false"],
89+
"generate_signed_proxy_url": ["true"],
90+
"save_search_history": ["false"],
91+
}
92+
),
93+
(
94+
"no_extra_query_params",
95+
{},
96+
{}
97+
),
98+
]
99+
100+
@pytest.mark.parametrize("test_id, query_params, expected_qs_dict", search_param_test_cases)
101+
def test_search_with_various_query_params(test_id, query_params, expected_qs_dict):
102+
with requests_mock.Mocker() as m:
103+
app_id = str(uuid.uuid4())
104+
auth_token = str(uuid.uuid4())
105+
asset_id = str(uuid.uuid4())
106+
107+
search_body_data = SearchBody(
108+
doc_types=["collections"],
109+
query=f"title:Test Collection AND id:{asset_id}"
110+
)
111+
112+
base_mock_address = SearchSpec.gen_url(SEARCH_PATH)
113+
# Mock the base address, query parameters will be checked via m.last_request.qs
114+
matcher = m.post(base_mock_address, json=search_body_data.model_dump())
115+
43116
client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3)
44-
client.search().search(search_chriteria, params=params)
117+
client.search().search(
118+
search_body=search_body_data,
119+
**query_params # Pass the dictionary of query params as keyword arguments
120+
)
121+
122+
assert matcher.called_once
123+
assert m.last_request.qs == expected_qs_dict

0 commit comments

Comments
 (0)