Skip to content

Commit afd5d92

Browse files
committed
add: curation_sets
1 parent 47b4c42 commit afd5d92

17 files changed

Lines changed: 768 additions & 23 deletions

src/typesense/curation_set.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Client for single Curation Set operations, including items APIs."""
2+
3+
import sys
4+
5+
if sys.version_info >= (3, 11):
6+
import typing
7+
else:
8+
import typing_extensions as typing
9+
10+
from typesense.api_call import ApiCall
11+
from typesense.types.curation_set import (
12+
CurationSetSchema,
13+
CurationSetDeleteSchema,
14+
CurationSetUpsertSchema,
15+
CurationSetListItemResponseSchema,
16+
CurationItemSchema,
17+
CurationItemDeleteSchema,
18+
)
19+
20+
21+
class CurationSet:
22+
def __init__(self, api_call: ApiCall, name: str) -> None:
23+
self.api_call = api_call
24+
self.name = name
25+
26+
@property
27+
def _endpoint_path(self) -> str:
28+
from typesense.curation_sets import CurationSets
29+
30+
return "/".join([CurationSets.resource_path, self.name])
31+
32+
def retrieve(self) -> CurationSetSchema:
33+
response: CurationSetSchema = self.api_call.get(
34+
self._endpoint_path,
35+
as_json=True,
36+
entity_type=CurationSetSchema,
37+
)
38+
return response
39+
40+
def delete(self) -> CurationSetDeleteSchema:
41+
response: CurationSetDeleteSchema = self.api_call.delete(
42+
self._endpoint_path,
43+
entity_type=CurationSetDeleteSchema,
44+
)
45+
return response
46+
47+
# Items sub-resource
48+
@property
49+
def _items_path(self) -> str:
50+
return "/".join([self._endpoint_path, "items"]) # /curation_sets/{name}/items
51+
52+
def list_items(
53+
self,
54+
*,
55+
limit: typing.Union[int, None] = None,
56+
offset: typing.Union[int, None] = None,
57+
) -> CurationSetListItemResponseSchema:
58+
params: typing.Dict[str, typing.Union[int, None]] = {
59+
"limit": limit,
60+
"offset": offset,
61+
}
62+
# Filter out None values to avoid sending them
63+
clean_params: typing.Dict[str, int] = {
64+
k: v for k, v in params.items() if v is not None # type: ignore[dict-item]
65+
}
66+
response: CurationSetListItemResponseSchema = self.api_call.get(
67+
self._items_path,
68+
as_json=True,
69+
entity_type=CurationSetListItemResponseSchema,
70+
params=clean_params or None,
71+
)
72+
return response
73+
74+
def get_item(self, item_id: str) -> CurationItemSchema:
75+
response: CurationItemSchema = self.api_call.get(
76+
"/".join([self._items_path, item_id]),
77+
as_json=True,
78+
entity_type=CurationItemSchema,
79+
)
80+
return response
81+
82+
def upsert_item(self, item_id: str, item: CurationItemSchema) -> CurationItemSchema:
83+
response: CurationItemSchema = self.api_call.put(
84+
"/".join([self._items_path, item_id]),
85+
body=item,
86+
entity_type=CurationItemSchema,
87+
)
88+
return response
89+
90+
def delete_item(self, item_id: str) -> CurationItemDeleteSchema:
91+
response: CurationItemDeleteSchema = self.api_call.delete(
92+
"/".join([self._items_path, item_id]),
93+
entity_type=CurationItemDeleteSchema,
94+
)
95+
return response
96+
97+

src/typesense/curation_sets.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Client for Curation Sets collection operations."""
2+
3+
import sys
4+
5+
if sys.version_info >= (3, 11):
6+
import typing
7+
else:
8+
import typing_extensions as typing
9+
10+
from typesense.api_call import ApiCall
11+
from typesense.types.curation_set import (
12+
CurationSetSchema,
13+
CurationSetUpsertSchema,
14+
CurationSetsListResponseSchema,
15+
CurationSetListItemResponseSchema,
16+
CurationItemDeleteSchema,
17+
CurationSetDeleteSchema,
18+
CurationItemSchema,
19+
)
20+
21+
22+
class CurationSets:
23+
resource_path: typing.Final[str] = "/curation_sets"
24+
25+
def __init__(self, api_call: ApiCall) -> None:
26+
self.api_call = api_call
27+
28+
def retrieve(self) -> CurationSetsListResponseSchema:
29+
response: CurationSetsListResponseSchema = self.api_call.get(
30+
CurationSets.resource_path,
31+
as_json=True,
32+
entity_type=CurationSetsListResponseSchema,
33+
)
34+
return response
35+
36+
def __getitem__(self, curation_set_name: str) -> "CurationSet":
37+
from typesense.curation_set import CurationSet as PerSet
38+
39+
return PerSet(self.api_call, curation_set_name)
40+
41+
def upsert(
42+
self,
43+
curation_set_name: str,
44+
payload: CurationSetUpsertSchema,
45+
) -> CurationSetSchema:
46+
response: CurationSetSchema = self.api_call.put(
47+
"/".join([CurationSets.resource_path, curation_set_name]),
48+
body=payload,
49+
entity_type=CurationSetSchema,
50+
)
51+
return response
52+
53+

src/typesense/synonym_set.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from typesense.types.synonym_set import (
1212
SynonymSetDeleteSchema,
1313
SynonymSetRetrieveSchema,
14+
SynonymItemSchema,
15+
SynonymItemDeleteSchema,
1416
)
1517

1618

@@ -39,5 +41,54 @@ def delete(self) -> SynonymSetDeleteSchema:
3941
entity_type=SynonymSetDeleteSchema,
4042
)
4143
return response
44+
45+
@property
46+
def _items_path(self) -> str:
47+
return "/".join([self._endpoint_path, "items"]) # /synonym_sets/{name}/items
48+
49+
def list_items(
50+
self,
51+
*,
52+
limit: typing.Union[int, None] = None,
53+
offset: typing.Union[int, None] = None,
54+
) -> typing.List[SynonymItemSchema]:
55+
params: typing.Dict[str, typing.Union[int, None]] = {
56+
"limit": limit,
57+
"offset": offset,
58+
}
59+
clean_params: typing.Dict[str, int] = {
60+
k: v for k, v in params.items() if v is not None # type: ignore[dict-item]
61+
}
62+
response: typing.List[SynonymItemSchema] = self.api_call.get(
63+
self._items_path,
64+
as_json=True,
65+
entity_type=typing.List[SynonymItemSchema],
66+
params=clean_params or None,
67+
)
68+
return response
69+
70+
def get_item(self, item_id: str) -> SynonymItemSchema:
71+
response: SynonymItemSchema = self.api_call.get(
72+
"/".join([self._items_path, item_id]),
73+
as_json=True,
74+
entity_type=SynonymItemSchema,
75+
)
76+
return response
77+
78+
def upsert_item(self, item_id: str, item: SynonymItemSchema) -> SynonymItemSchema:
79+
response: SynonymItemSchema = self.api_call.put(
80+
"/".join([self._items_path, item_id]),
81+
body=item,
82+
entity_type=SynonymItemSchema,
83+
)
84+
return response
85+
86+
def delete_item(self, item_id: str) -> typing.Dict[str, str]:
87+
# API returns {"id": "..."} for delete; openapi defines SynonymItemDeleteResponse with name but for items it's id
88+
response: SynonymItemDeleteSchema = self.api_call.delete(
89+
"/".join([self._items_path, item_id]),
90+
entity_type=typing.Dict[str, str],
91+
)
92+
return response
4293

4394

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Curation Set types for Typesense Python Client."""
2+
3+
import sys
4+
5+
if sys.version_info >= (3, 11):
6+
import typing
7+
else:
8+
import typing_extensions as typing
9+
10+
11+
class CurationIncludeSchema(typing.TypedDict):
12+
"""
13+
Schema representing an included document for a curation rule.
14+
"""
15+
16+
id: str
17+
position: int
18+
19+
20+
class CurationExcludeSchema(typing.TypedDict):
21+
"""
22+
Schema representing an excluded document for a curation rule.
23+
"""
24+
25+
id: str
26+
27+
28+
class CurationRuleSchema(typing.TypedDict, total=False):
29+
"""
30+
Schema representing rule conditions for a curation item.
31+
"""
32+
33+
query: str
34+
match: typing.Literal["exact", "contains"]
35+
filter_by: str
36+
tags: typing.List[str]
37+
38+
39+
class CurationItemSchema(typing.TypedDict, total=False):
40+
"""
41+
Schema for a single curation item (aka CurationObject in the API).
42+
"""
43+
44+
id: str
45+
rule: CurationRuleSchema
46+
includes: typing.List[CurationIncludeSchema]
47+
excludes: typing.List[CurationExcludeSchema]
48+
filter_by: str
49+
sort_by: str
50+
replace_query: str
51+
remove_matched_tokens: bool
52+
filter_curated_hits: bool
53+
stop_processing: bool
54+
metadata: typing.Dict[str, typing.Any]
55+
56+
57+
class CurationSetUpsertSchema(typing.TypedDict):
58+
"""
59+
Payload schema to create or replace a curation set.
60+
"""
61+
62+
items: typing.List[CurationItemSchema]
63+
64+
65+
class CurationSetSchema(CurationSetUpsertSchema):
66+
"""
67+
Response schema for a curation set.
68+
"""
69+
70+
name: str
71+
72+
73+
class CurationSetsListEntrySchema(typing.TypedDict):
74+
"""A single entry in the curation sets list response."""
75+
76+
name: str
77+
items: typing.List[CurationItemSchema]
78+
79+
80+
class CurationSetsListResponseSchema(typing.List[CurationSetsListEntrySchema]):
81+
"""List response for all curation sets."""
82+
83+
84+
class CurationSetListItemResponseSchema(typing.List[CurationItemSchema]):
85+
"""List response for items under a specific curation set."""
86+
87+
88+
class CurationItemDeleteSchema(typing.TypedDict):
89+
"""Response schema for deleting a curation item."""
90+
91+
id: str
92+
93+
94+
class CurationSetDeleteSchema(typing.TypedDict):
95+
"""Response schema for deleting a curation set."""
96+
97+
name: str
98+
99+

src/typesense/types/synonym_set.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ class SynonymItemSchema(typing.TypedDict):
2929
locale: typing.NotRequired[Locales]
3030
symbols_to_index: typing.NotRequired[typing.List[str]]
3131

32+
class SynonymItemDeleteSchema(typing.TypedDict):
33+
"""
34+
Schema for deleting a synonym item.
35+
"""
36+
37+
id: str
3238

3339
class SynonymSetCreateSchema(typing.TypedDict):
3440
"""
@@ -67,6 +73,4 @@ class SynonymSetDeleteSchema(typing.TypedDict):
6773
name (str): Name of the deleted synonym set.
6874
"""
6975

70-
name: str
71-
72-
76+
name: str

tests/analytics_rule_v1_test.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,16 @@
1212
from typesense.api_call import ApiCall
1313
from typesense.types.analytics_rule_v1 import RuleDeleteSchema, RuleSchemaForQueries
1414

15+
pytestmark = pytest.mark.skipif(
16+
is_v30_or_above(
17+
Client({
18+
"api_key": "xyz",
19+
"nodes": [{"host": "localhost", "port": 8108, "protocol": "http"}],
20+
})
21+
),
22+
reason="Skip AnalyticsV1 tests on v30+"
23+
)
1524

16-
@pytest.mark.skipif(is_v30_or_above(Client({"api_key": "xyz", "nodes": [{"host": "localhost", "port": 8108, "protocol": "http"}]})), reason="Skip AnalyticsV1 tests on v30+")
1725
def test_init(fake_api_call: ApiCall) -> None:
1826
"""Test that the AnalyticsRuleV1 object is initialized correctly."""
1927
analytics_rule = AnalyticsRuleV1(fake_api_call, "company_analytics_rule")
@@ -34,7 +42,7 @@ def test_init(fake_api_call: ApiCall) -> None:
3442
)
3543

3644

37-
@pytest.mark.skipif(is_v30_or_above(Client({"api_key": "xyz", "nodes": [{"host": "localhost", "port": 8108, "protocol": "http"}]})), reason="Skip AnalyticsV1 tests on v30+")
45+
3846
def test_retrieve(fake_analytics_rule: AnalyticsRuleV1) -> None:
3947
"""Test that the AnalyticsRuleV1 object can retrieve an analytics_rule."""
4048
json_response: RuleSchemaForQueries = {
@@ -65,7 +73,7 @@ def test_retrieve(fake_analytics_rule: AnalyticsRuleV1) -> None:
6573
assert response == json_response
6674

6775

68-
@pytest.mark.skipif(is_v30_or_above(Client({"api_key": "xyz", "nodes": [{"host": "localhost", "port": 8108, "protocol": "http"}]})), reason="Skip AnalyticsV1 tests on v30+")
76+
6977
def test_delete(fake_analytics_rule: AnalyticsRuleV1) -> None:
7078
"""Test that the AnalyticsRuleV1 object can delete an analytics_rule."""
7179
json_response: RuleDeleteSchema = {
@@ -88,7 +96,7 @@ def test_delete(fake_analytics_rule: AnalyticsRuleV1) -> None:
8896
assert response == json_response
8997

9098

91-
@pytest.mark.skipif(is_v30_or_above(Client({"api_key": "xyz", "nodes": [{"host": "localhost", "port": 8108, "protocol": "http"}]})), reason="Skip AnalyticsV1 tests on v30+")
99+
92100
def test_actual_retrieve(
93101
actual_analytics_rules: AnalyticsRulesV1,
94102
delete_all: None,
@@ -111,7 +119,7 @@ def test_actual_retrieve(
111119
assert response == expected
112120

113121

114-
@pytest.mark.skipif(is_v30_or_above(Client({"api_key": "xyz", "nodes": [{"host": "localhost", "port": 8108, "protocol": "http"}]})), reason="Skip AnalyticsV1 tests on v30+")
122+
115123
def test_actual_delete(
116124
actual_analytics_rules: AnalyticsRulesV1,
117125
delete_all: None,

0 commit comments

Comments
 (0)