From 84eb1f72896a498f92e17baf1881c140c6b34620 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 9 Nov 2026 18:22:33 +0200 Subject: [PATCH 1/2] feat(synonym-sets): add an items accessor to synonym sets --- src/typesense/async_/synonym_set.py | 46 ++--- src/typesense/async_/synonym_set_item.py | 53 ++++++ src/typesense/async_/synonym_set_items.py | 55 ++++++ src/typesense/sync/synonym_set.py | 46 ++--- src/typesense/sync/synonym_set_item.py | 53 ++++++ src/typesense/sync/synonym_set_items.py | 55 ++++++ src/typesense/types/synonym_set.py | 17 +- tests/synonym_set_items_test.py | 194 ++++++++++++++++++++++ 8 files changed, 443 insertions(+), 76 deletions(-) create mode 100644 src/typesense/async_/synonym_set_item.py create mode 100644 src/typesense/async_/synonym_set_items.py create mode 100644 src/typesense/sync/synonym_set_item.py create mode 100644 src/typesense/sync/synonym_set_items.py diff --git a/src/typesense/async_/synonym_set.py b/src/typesense/async_/synonym_set.py index 43dde26..03c85c7 100644 --- a/src/typesense/async_/synonym_set.py +++ b/src/typesense/async_/synonym_set.py @@ -8,9 +8,11 @@ import typing_extensions as typing from .api_call import AsyncApiCall +from .synonym_set_items import AsyncSynonymSetItems from typesense.types.synonym_set import ( SynonymItemDeleteSchema, SynonymItemSchema, + SynonymItemUpsertSchema, SynonymSetCreateSchema, SynonymSetDeleteSchema, SynonymSetRetrieveSchema, @@ -21,6 +23,7 @@ class AsyncSynonymSet: def __init__(self, api_call: AsyncApiCall, name: str) -> None: self.api_call = api_call self.name = name + self.items = AsyncSynonymSetItems(api_call, name) @property def _endpoint_path(self) -> str: @@ -51,52 +54,23 @@ async def delete(self) -> SynonymSetDeleteSchema: ) return response - @property - def _items_path(self) -> str: - return "/".join([self._endpoint_path, "items"]) # /synonym_sets/{name}/items - async def list_items( self, *, limit: typing.Union[int, None] = None, offset: typing.Union[int, None] = None, ) -> typing.List[SynonymItemSchema]: - params: typing.Dict[str, typing.Union[int, None]] = { - "limit": limit, - "offset": offset, - } - clean_params: typing.Dict[str, int] = { - k: v for k, v in params.items() if v is not None - } - response: typing.List[SynonymItemSchema] = await self.api_call.get( - self._items_path, - as_json=True, - entity_type=typing.List[SynonymItemSchema], - params=clean_params or None, - ) - return response + return await self.items.retrieve(limit=limit, offset=offset) async def get_item(self, item_id: str) -> SynonymItemSchema: - response: SynonymItemSchema = await self.api_call.get( - "/".join([self._items_path, item_id]), - as_json=True, - entity_type=SynonymItemSchema, - ) - return response + return await self.items[item_id].retrieve() async def upsert_item( - self, item_id: str, item: SynonymItemSchema + self, + item_id: str, + item: typing.Union[SynonymItemSchema, SynonymItemUpsertSchema], ) -> SynonymItemSchema: - response: SynonymItemSchema = await self.api_call.put( - "/".join([self._items_path, item_id]), - body=item, - entity_type=SynonymItemSchema, - ) - return response + return await self.items[item_id].upsert(item) async def delete_item(self, item_id: str) -> SynonymItemDeleteSchema: - # API returns {"id": "..."} for delete; openapi defines SynonymItemDeleteResponse with name but for items it's id - response: SynonymItemDeleteSchema = await self.api_call.delete( - "/".join([self._items_path, item_id]), entity_type=SynonymItemDeleteSchema - ) - return response + return await self.items[item_id].delete() diff --git a/src/typesense/async_/synonym_set_item.py b/src/typesense/async_/synonym_set_item.py new file mode 100644 index 0000000..4d41b0c --- /dev/null +++ b/src/typesense/async_/synonym_set_item.py @@ -0,0 +1,53 @@ +"""Client for single Synonym Set item operations (async).""" + +from .api_call import AsyncApiCall +from typesense.types.synonym_set import ( + SynonymItemDeleteSchema, + SynonymItemSchema, + SynonymItemUpsertSchema, +) + + +class AsyncSynonymSetItem: + def __init__( + self, api_call: AsyncApiCall, synonym_set_name: str, item_id: str + ) -> None: + self.api_call = api_call + self.synonym_set_name = synonym_set_name + self.item_id = item_id + + @property + def _endpoint_path(self) -> str: + from .synonym_sets import AsyncSynonymSets + + return "/".join( + [ + AsyncSynonymSets.resource_path, + self.synonym_set_name, + "items", + self.item_id, + ] + ) + + async def retrieve(self) -> SynonymItemSchema: + response: SynonymItemSchema = await self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=SynonymItemSchema, + ) + return response + + async def upsert(self, item: SynonymItemUpsertSchema) -> SynonymItemSchema: + response: SynonymItemSchema = await self.api_call.put( + self._endpoint_path, + body=item, + entity_type=SynonymItemSchema, + ) + return response + + async def delete(self) -> SynonymItemDeleteSchema: + response: SynonymItemDeleteSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=SynonymItemDeleteSchema, + ) + return response diff --git a/src/typesense/async_/synonym_set_items.py b/src/typesense/async_/synonym_set_items.py new file mode 100644 index 0000000..4902a43 --- /dev/null +++ b/src/typesense/async_/synonym_set_items.py @@ -0,0 +1,55 @@ +"""Client for Synonym Set items collection operations (async).""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from .api_call import AsyncApiCall +from .synonym_set_item import AsyncSynonymSetItem +from typesense.types.synonym_set import SynonymItemSchema, SynonymItemUpsertSchema + + +class AsyncSynonymSetItems: + def __init__(self, api_call: AsyncApiCall, synonym_set_name: str) -> None: + self.api_call = api_call + self.synonym_set_name = synonym_set_name + + def __getitem__(self, item_id: str) -> AsyncSynonymSetItem: + return AsyncSynonymSetItem(self.api_call, self.synonym_set_name, item_id) + + @property + def _endpoint_path(self) -> str: + from .synonym_sets import AsyncSynonymSets + + return "/".join( + [AsyncSynonymSets.resource_path, self.synonym_set_name, "items"] + ) + + async def retrieve( + self, + *, + limit: typing.Union[int, None] = None, + offset: typing.Union[int, None] = None, + ) -> typing.List[SynonymItemSchema]: + params: typing.Dict[str, typing.Union[int, None]] = { + "limit": limit, + "offset": offset, + } + clean_params: typing.Dict[str, int] = { + k: v for k, v in params.items() if v is not None + } + response: typing.List[SynonymItemSchema] = await self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=typing.List[SynonymItemSchema], + params=clean_params or None, + ) + return response + + async def upsert( + self, item_id: str, item: SynonymItemUpsertSchema + ) -> SynonymItemSchema: + return await self[item_id].upsert(item) diff --git a/src/typesense/sync/synonym_set.py b/src/typesense/sync/synonym_set.py index dc11d0b..3b8ba0b 100644 --- a/src/typesense/sync/synonym_set.py +++ b/src/typesense/sync/synonym_set.py @@ -8,9 +8,11 @@ import typing_extensions as typing from .api_call import ApiCall +from .synonym_set_items import SynonymSetItems from typesense.types.synonym_set import ( SynonymItemDeleteSchema, SynonymItemSchema, + SynonymItemUpsertSchema, SynonymSetCreateSchema, SynonymSetDeleteSchema, SynonymSetRetrieveSchema, @@ -21,6 +23,7 @@ class SynonymSet: def __init__(self, api_call: ApiCall, name: str) -> None: self.api_call = api_call self.name = name + self.items = SynonymSetItems(api_call, name) @property def _endpoint_path(self) -> str: @@ -51,52 +54,23 @@ def delete(self) -> SynonymSetDeleteSchema: ) return response - @property - def _items_path(self) -> str: - return "/".join([self._endpoint_path, "items"]) # /synonym_sets/{name}/items - def list_items( self, *, limit: typing.Union[int, None] = None, offset: typing.Union[int, None] = None, ) -> typing.List[SynonymItemSchema]: - params: typing.Dict[str, typing.Union[int, None]] = { - "limit": limit, - "offset": offset, - } - clean_params: typing.Dict[str, int] = { - k: v for k, v in params.items() if v is not None - } - response: typing.List[SynonymItemSchema] = self.api_call.get( - self._items_path, - as_json=True, - entity_type=typing.List[SynonymItemSchema], - params=clean_params or None, - ) - return response + return self.items.retrieve(limit=limit, offset=offset) def get_item(self, item_id: str) -> SynonymItemSchema: - response: SynonymItemSchema = self.api_call.get( - "/".join([self._items_path, item_id]), - as_json=True, - entity_type=SynonymItemSchema, - ) - return response + return self.items[item_id].retrieve() def upsert_item( - self, item_id: str, item: SynonymItemSchema + self, + item_id: str, + item: typing.Union[SynonymItemSchema, SynonymItemUpsertSchema], ) -> SynonymItemSchema: - response: SynonymItemSchema = self.api_call.put( - "/".join([self._items_path, item_id]), - body=item, - entity_type=SynonymItemSchema, - ) - return response + return self.items[item_id].upsert(item) def delete_item(self, item_id: str) -> SynonymItemDeleteSchema: - # API returns {"id": "..."} for delete; openapi defines SynonymItemDeleteResponse with name but for items it's id - response: SynonymItemDeleteSchema = self.api_call.delete( - "/".join([self._items_path, item_id]), entity_type=SynonymItemDeleteSchema - ) - return response + return self.items[item_id].delete() diff --git a/src/typesense/sync/synonym_set_item.py b/src/typesense/sync/synonym_set_item.py new file mode 100644 index 0000000..d3e9124 --- /dev/null +++ b/src/typesense/sync/synonym_set_item.py @@ -0,0 +1,53 @@ +"""Client for single Synonym Set item operations (async).""" + +from .api_call import ApiCall +from typesense.types.synonym_set import ( + SynonymItemDeleteSchema, + SynonymItemSchema, + SynonymItemUpsertSchema, +) + + +class SynonymSetItem: + def __init__( + self, api_call: ApiCall, synonym_set_name: str, item_id: str + ) -> None: + self.api_call = api_call + self.synonym_set_name = synonym_set_name + self.item_id = item_id + + @property + def _endpoint_path(self) -> str: + from .synonym_sets import SynonymSets + + return "/".join( + [ + SynonymSets.resource_path, + self.synonym_set_name, + "items", + self.item_id, + ] + ) + + def retrieve(self) -> SynonymItemSchema: + response: SynonymItemSchema = self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=SynonymItemSchema, + ) + return response + + def upsert(self, item: SynonymItemUpsertSchema) -> SynonymItemSchema: + response: SynonymItemSchema = self.api_call.put( + self._endpoint_path, + body=item, + entity_type=SynonymItemSchema, + ) + return response + + def delete(self) -> SynonymItemDeleteSchema: + response: SynonymItemDeleteSchema = self.api_call.delete( + self._endpoint_path, + entity_type=SynonymItemDeleteSchema, + ) + return response diff --git a/src/typesense/sync/synonym_set_items.py b/src/typesense/sync/synonym_set_items.py new file mode 100644 index 0000000..ee01bab --- /dev/null +++ b/src/typesense/sync/synonym_set_items.py @@ -0,0 +1,55 @@ +"""Client for Synonym Set items collection operations (async).""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from .api_call import ApiCall +from .synonym_set_item import SynonymSetItem +from typesense.types.synonym_set import SynonymItemSchema, SynonymItemUpsertSchema + + +class SynonymSetItems: + def __init__(self, api_call: ApiCall, synonym_set_name: str) -> None: + self.api_call = api_call + self.synonym_set_name = synonym_set_name + + def __getitem__(self, item_id: str) -> SynonymSetItem: + return SynonymSetItem(self.api_call, self.synonym_set_name, item_id) + + @property + def _endpoint_path(self) -> str: + from .synonym_sets import SynonymSets + + return "/".join( + [SynonymSets.resource_path, self.synonym_set_name, "items"] + ) + + def retrieve( + self, + *, + limit: typing.Union[int, None] = None, + offset: typing.Union[int, None] = None, + ) -> typing.List[SynonymItemSchema]: + params: typing.Dict[str, typing.Union[int, None]] = { + "limit": limit, + "offset": offset, + } + clean_params: typing.Dict[str, int] = { + k: v for k, v in params.items() if v is not None + } + response: typing.List[SynonymItemSchema] = self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=typing.List[SynonymItemSchema], + params=clean_params or None, + ) + return response + + def upsert( + self, item_id: str, item: SynonymItemUpsertSchema + ) -> SynonymItemSchema: + return self[item_id].upsert(item) diff --git a/src/typesense/types/synonym_set.py b/src/typesense/types/synonym_set.py index d036411..faa831d 100644 --- a/src/typesense/types/synonym_set.py +++ b/src/typesense/types/synonym_set.py @@ -10,12 +10,11 @@ import typing_extensions as typing -class SynonymItemSchema(typing.TypedDict): +class SynonymItemUpsertSchema(typing.TypedDict): """ - Schema representing an individual synonym item inside a synonym set. + Schema for creating or updating a synonym item through its own endpoint. Attributes: - id (str): Unique identifier for the synonym item. synonyms (list[str]): The synonyms array. root (str, optional): For 1-way synonyms, indicates the root word that words in the synonyms parameter map to. @@ -23,13 +22,23 @@ class SynonymItemSchema(typing.TypedDict): symbols_to_index (list[str], optional): Symbols to index as-is in synonyms. """ - id: str synonyms: typing.List[str] root: typing.NotRequired[str] locale: typing.NotRequired[Locales] symbols_to_index: typing.NotRequired[typing.List[str]] +class SynonymItemSchema(SynonymItemUpsertSchema): + """ + Schema representing an individual synonym item inside a synonym set. + + Attributes: + id (str): Unique identifier for the synonym item. + """ + + id: str + + class SynonymItemDeleteSchema(typing.TypedDict): """ Schema for deleting a synonym item. diff --git a/tests/synonym_set_items_test.py b/tests/synonym_set_items_test.py index f871719..f3614f1 100644 --- a/tests/synonym_set_items_test.py +++ b/tests/synonym_set_items_test.py @@ -4,11 +4,14 @@ import pytest from tests.utils.version import is_v30_or_above +from typesense.async_.synonym_set import AsyncSynonymSet from typesense.async_.synonym_sets import AsyncSynonymSets from typesense.sync.client import Client +from typesense.sync.synonym_set import SynonymSet from typesense.sync.synonym_sets import SynonymSets from typesense.types.synonym_set import ( SynonymItemSchema, + SynonymItemUpsertSchema, ) pytestmark = pytest.mark.skipif( @@ -24,6 +27,24 @@ ) +def test_items_paths(fake_synonym_set: SynonymSet) -> None: + items = fake_synonym_set.items + assert items._endpoint_path == "/synonym_sets/test-set/items" # noqa: WPS437 + assert ( + items["company_synonym"]._endpoint_path # noqa: WPS437 + == "/synonym_sets/test-set/items/company_synonym" + ) + + +def test_items_paths_async(fake_async_synonym_set: AsyncSynonymSet) -> None: + items = fake_async_synonym_set.items + assert items._endpoint_path == "/synonym_sets/test-set/items" # noqa: WPS437 + assert ( + items["company_synonym"]._endpoint_path # noqa: WPS437 + == "/synonym_sets/test-set/items/company_synonym" + ) + + def test_actual_list_items( actual_synonym_sets: SynonymSets, delete_all_synonym_sets: None, @@ -148,3 +169,176 @@ async def test_actual_delete_item_async( ) assert response == {"id": "company_synonym"} + + +def test_actual_items_retrieve( + actual_synonym_sets: SynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the SynonymSetItems object can list items from Typesense Server.""" + response = actual_synonym_sets["test-set"].items.retrieve() + + assert response == [ + { + "id": "company_synonym", + "root": "", + "synonyms": ["companies", "corporations", "firms"], + }, + ] + + +def test_actual_items_retrieve_paginated( + actual_synonym_sets: SynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the SynonymSetItems object can paginate items from Typesense Server.""" + items = actual_synonym_sets["test-set"].items + items.upsert("brand_synonym", {"synonyms": ["brand", "brands", "label"]}) + response = items.retrieve(limit=1, offset=1) + + assert response == [ + { + "id": "brand_synonym", + "root": "", + "synonyms": ["brand", "brands", "label"], + }, + ] + + +def test_actual_items_upsert( + actual_synonym_sets: SynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the SynonymSetItems object can upsert an item in Typesense Server.""" + payload: SynonymItemUpsertSchema = {"synonyms": ["brand", "brands", "label"]} + response = actual_synonym_sets["test-set"].items.upsert("brand_synonym", payload) + + assert response == { + "id": "brand_synonym", + "synonyms": ["brand", "brands", "label"], + } + + +def test_actual_item_retrieve( + actual_synonym_sets: SynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the SynonymSetItem object can retrieve an item from Typesense Server.""" + response = actual_synonym_sets["test-set"].items["company_synonym"].retrieve() + + assert response == { + "id": "company_synonym", + "root": "", + "synonyms": ["companies", "corporations", "firms"], + } + + +def test_actual_item_upsert( + actual_synonym_sets: SynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the SynonymSetItem object can upsert an item in Typesense Server.""" + payload: SynonymItemUpsertSchema = {"synonyms": ["brand", "brands", "label"]} + response = actual_synonym_sets["test-set"].items["brand_synonym"].upsert(payload) + + assert response == { + "id": "brand_synonym", + "synonyms": ["brand", "brands", "label"], + } + + +def test_actual_item_delete( + actual_synonym_sets: SynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the SynonymSetItem object can delete an item from Typesense Server.""" + response = actual_synonym_sets["test-set"].items["company_synonym"].delete() + + assert response == {"id": "company_synonym"} + + +async def test_actual_items_retrieve_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSetItems object can list items from Typesense Server.""" + response = await actual_async_synonym_sets["test-set"].items.retrieve() + + assert response == [ + { + "id": "company_synonym", + "root": "", + "synonyms": ["companies", "corporations", "firms"], + }, + ] + + +async def test_actual_items_upsert_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSetItems object can upsert an item in Typesense Server.""" + payload: SynonymItemUpsertSchema = {"synonyms": ["brand", "brands", "label"]} + response = await actual_async_synonym_sets["test-set"].items.upsert( + "brand_synonym", payload + ) + + assert response == { + "id": "brand_synonym", + "synonyms": ["brand", "brands", "label"], + } + + +async def test_actual_item_retrieve_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSetItem object can retrieve an item from Typesense Server.""" + response = ( + await actual_async_synonym_sets["test-set"].items["company_synonym"].retrieve() + ) + + assert response == { + "id": "company_synonym", + "root": "", + "synonyms": ["companies", "corporations", "firms"], + } + + +async def test_actual_item_upsert_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSetItem object can upsert an item in Typesense Server.""" + payload: SynonymItemUpsertSchema = {"synonyms": ["brand", "brands", "label"]} + response = ( + await actual_async_synonym_sets["test-set"].items["brand_synonym"].upsert(payload) + ) + + assert response == { + "id": "brand_synonym", + "synonyms": ["brand", "brands", "label"], + } + + +async def test_actual_item_delete_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSetItem object can delete an item from Typesense Server.""" + response = ( + await actual_async_synonym_sets["test-set"].items["company_synonym"].delete() + ) + + assert response == {"id": "company_synonym"} From a0f2d456b800074368e3ef688dca386abf72d6ff Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 9 Nov 2026 18:23:52 +0200 Subject: [PATCH 2/2] feat(curation-sets): add an items accessor to curation sets --- src/typesense/async_/curation_set.py | 57 +--- src/typesense/async_/curation_set_item.py | 88 ++++++ src/typesense/async_/curation_set_items.py | 98 +++++++ src/typesense/sync/curation_set.py | 57 +--- src/typesense/sync/curation_set_item.py | 88 ++++++ src/typesense/sync/curation_set_items.py | 98 +++++++ src/typesense/types/curation_set.py | 13 +- tests/curation_set_test.py | 321 ++++++++++++++++++++- 8 files changed, 724 insertions(+), 96 deletions(-) create mode 100644 src/typesense/async_/curation_set_item.py create mode 100644 src/typesense/async_/curation_set_items.py create mode 100644 src/typesense/sync/curation_set_item.py create mode 100644 src/typesense/sync/curation_set_items.py diff --git a/src/typesense/async_/curation_set.py b/src/typesense/async_/curation_set.py index b2ce069..08cc583 100644 --- a/src/typesense/async_/curation_set.py +++ b/src/typesense/async_/curation_set.py @@ -22,7 +22,9 @@ import typing_extensions as typing from .api_call import AsyncApiCall +from .curation_set_items import AsyncCurationSetItems from typesense.types.curation_set import ( + CurationItemCreateSchema, CurationItemDeleteSchema, CurationItemSchema, CurationSetDeleteSchema, @@ -42,6 +44,7 @@ class AsyncCurationSet: Attributes: api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. name (str): The name of the curation set. + items (AsyncCurationSetItems): The items of the curation set. """ def __init__(self, api_call: AsyncApiCall, name: str) -> None: @@ -54,6 +57,7 @@ def __init__(self, api_call: AsyncApiCall, name: str) -> None: """ self.api_call = api_call self.name = name + self.items = AsyncCurationSetItems(api_call, name) @property def _endpoint_path(self) -> str: @@ -114,17 +118,6 @@ async def upsert( ) return response - # Items sub-resource - @property - def _items_path(self) -> str: - """ - Get the API endpoint path for items in this curation set. - - Returns: - str: The full endpoint path for items (e.g., /curation_sets/{name}/items). - """ - return "/".join([self._endpoint_path, "items"]) - async def list_items( self, *, @@ -141,21 +134,7 @@ async def list_items( Returns: CurationSetListItemResponseSchema: The list of items in the curation set. """ - params: typing.Dict[str, typing.Union[int, None]] = { - "limit": limit, - "offset": offset, - } - # Filter out None values to avoid sending them - clean_params: typing.Dict[str, int] = { - k: v for k, v in params.items() if v is not None - } - response: CurationSetListItemResponseSchema = await self.api_call.get( - self._items_path, - as_json=True, - entity_type=CurationSetListItemResponseSchema, - params=clean_params or None, - ) - return response + return await self.items.retrieve(limit=limit, offset=offset) async def get_item(self, item_id: str) -> CurationItemSchema: """ @@ -167,32 +146,24 @@ async def get_item(self, item_id: str) -> CurationItemSchema: Returns: CurationItemSchema: The item schema. """ - response: CurationItemSchema = await self.api_call.get( - "/".join([self._items_path, item_id]), - as_json=True, - entity_type=CurationItemSchema, - ) - return response + return await self.items[item_id].retrieve() async def upsert_item( - self, item_id: str, item: CurationItemSchema + self, + item_id: str, + item: typing.Union[CurationItemSchema, CurationItemCreateSchema], ) -> CurationItemSchema: """ Create or update an item in this curation set. Args: item_id (str): The ID of the item. - item (CurationItemSchema): The item schema. + item (Union[CurationItemSchema, CurationItemCreateSchema]): The item schema. Returns: CurationItemSchema: The created or updated item. """ - response: CurationItemSchema = await self.api_call.put( - "/".join([self._items_path, item_id]), - body=item, - entity_type=CurationItemSchema, - ) - return response + return await self.items[item_id].upsert(item) async def delete_item(self, item_id: str) -> CurationItemDeleteSchema: """ @@ -204,8 +175,4 @@ async def delete_item(self, item_id: str) -> CurationItemDeleteSchema: Returns: CurationItemDeleteSchema: The deletion response. """ - response: CurationItemDeleteSchema = await self.api_call.delete( - "/".join([self._items_path, item_id]), - entity_type=CurationItemDeleteSchema, - ) - return response + return await self.items[item_id].delete() diff --git a/src/typesense/async_/curation_set_item.py b/src/typesense/async_/curation_set_item.py new file mode 100644 index 0000000..a1a218b --- /dev/null +++ b/src/typesense/async_/curation_set_item.py @@ -0,0 +1,88 @@ +""" +This module provides async functionality for managing a single curation set item in Typesense. + +Classes: + AsyncCurationSetItem: Retrieves, upserts and deletes one item of a curation set (async). +""" + +from .api_call import AsyncApiCall +from typesense.types.curation_set import ( + CurationItemCreateSchema, + CurationItemDeleteSchema, + CurationItemSchema, +) + + +class AsyncCurationSetItem: + """ + Manages a single item of a curation set in the Typesense API (async). + + Attributes: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + curation_set_name (str): The name of the curation set. + item_id (str): The ID of the item. + """ + + def __init__( + self, api_call: AsyncApiCall, curation_set_name: str, item_id: str + ) -> None: + self.api_call = api_call + self.curation_set_name = curation_set_name + self.item_id = item_id + + @property + def _endpoint_path(self) -> str: + from .curation_sets import AsyncCurationSets + + return "/".join( + [ + AsyncCurationSets.resource_path, + self.curation_set_name, + "items", + self.item_id, + ] + ) + + async def retrieve(self) -> CurationItemSchema: + """ + Retrieve this item. + + Returns: + CurationItemSchema: The item schema. + """ + response: CurationItemSchema = await self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=CurationItemSchema, + ) + return response + + async def upsert(self, item: CurationItemCreateSchema) -> CurationItemSchema: + """ + Create or update this item. + + Args: + item (CurationItemCreateSchema): The item schema. + + Returns: + CurationItemSchema: The created or updated item. + """ + response: CurationItemSchema = await self.api_call.put( + self._endpoint_path, + body=item, + entity_type=CurationItemSchema, + ) + return response + + async def delete(self) -> CurationItemDeleteSchema: + """ + Delete this item. + + Returns: + CurationItemDeleteSchema: The deletion response. + """ + response: CurationItemDeleteSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=CurationItemDeleteSchema, + ) + return response diff --git a/src/typesense/async_/curation_set_items.py b/src/typesense/async_/curation_set_items.py new file mode 100644 index 0000000..1e23114 --- /dev/null +++ b/src/typesense/async_/curation_set_items.py @@ -0,0 +1,98 @@ +""" +This module provides async functionality for managing the items of a curation set in Typesense. + +Classes: + AsyncCurationSetItems: Lists, upserts and accesses items of a single curation set (async). +""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from .api_call import AsyncApiCall +from .curation_set_item import AsyncCurationSetItem +from typesense.types.curation_set import ( + CurationItemCreateSchema, + CurationItemSchema, + CurationSetListItemResponseSchema, +) + + +class AsyncCurationSetItems: + """ + Manages the items of a single curation set in the Typesense API (async). + + Attributes: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + curation_set_name (str): The name of the curation set. + """ + + def __init__(self, api_call: AsyncApiCall, curation_set_name: str) -> None: + self.api_call = api_call + self.curation_set_name = curation_set_name + + def __getitem__(self, item_id: str) -> AsyncCurationSetItem: + """ + Get an AsyncCurationSetItem instance for a given item ID. + + Example: + >>> item = curation_sets["products"].items["rule-1"] + """ + return AsyncCurationSetItem(self.api_call, self.curation_set_name, item_id) + + @property + def _endpoint_path(self) -> str: + from .curation_sets import AsyncCurationSets + + return "/".join( + [AsyncCurationSets.resource_path, self.curation_set_name, "items"] + ) + + async def retrieve( + self, + *, + limit: typing.Union[int, None] = None, + offset: typing.Union[int, None] = None, + ) -> CurationSetListItemResponseSchema: + """ + List items in this curation set. + + Args: + limit (Union[int, None], optional): Maximum number of items to return. Defaults to None. + offset (Union[int, None], optional): Number of items to skip. Defaults to None. + + Returns: + CurationSetListItemResponseSchema: The list of items in the curation set. + """ + params: typing.Dict[str, typing.Union[int, None]] = { + "limit": limit, + "offset": offset, + } + clean_params: typing.Dict[str, int] = { + k: v for k, v in params.items() if v is not None + } + response: CurationSetListItemResponseSchema = await self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=CurationSetListItemResponseSchema, + params=clean_params or None, + ) + return response + + async def upsert( + self, item_id: str, item: CurationItemCreateSchema + ) -> CurationItemSchema: + """ + Create or update an item in this curation set. + + Args: + item_id (str): The ID of the item. + item (CurationItemCreateSchema): The item schema. + + Returns: + CurationItemSchema: The created or updated item. + """ + return await self[item_id].upsert(item) diff --git a/src/typesense/sync/curation_set.py b/src/typesense/sync/curation_set.py index 04f3646..459b236 100644 --- a/src/typesense/sync/curation_set.py +++ b/src/typesense/sync/curation_set.py @@ -22,7 +22,9 @@ import typing_extensions as typing from .api_call import ApiCall +from .curation_set_items import CurationSetItems from typesense.types.curation_set import ( + CurationItemCreateSchema, CurationItemDeleteSchema, CurationItemSchema, CurationSetDeleteSchema, @@ -42,6 +44,7 @@ class CurationSet: Attributes: api_call (ApiCall): The ApiCall instance for making async API requests. name (str): The name of the curation set. + items (CurationSetItems): The items of the curation set. """ def __init__(self, api_call: ApiCall, name: str) -> None: @@ -54,6 +57,7 @@ def __init__(self, api_call: ApiCall, name: str) -> None: """ self.api_call = api_call self.name = name + self.items = CurationSetItems(api_call, name) @property def _endpoint_path(self) -> str: @@ -114,17 +118,6 @@ def upsert( ) return response - # Items sub-resource - @property - def _items_path(self) -> str: - """ - Get the API endpoint path for items in this curation set. - - Returns: - str: The full endpoint path for items (e.g., /curation_sets/{name}/items). - """ - return "/".join([self._endpoint_path, "items"]) - def list_items( self, *, @@ -141,21 +134,7 @@ def list_items( Returns: CurationSetListItemResponseSchema: The list of items in the curation set. """ - params: typing.Dict[str, typing.Union[int, None]] = { - "limit": limit, - "offset": offset, - } - # Filter out None values to avoid sending them - clean_params: typing.Dict[str, int] = { - k: v for k, v in params.items() if v is not None - } - response: CurationSetListItemResponseSchema = self.api_call.get( - self._items_path, - as_json=True, - entity_type=CurationSetListItemResponseSchema, - params=clean_params or None, - ) - return response + return self.items.retrieve(limit=limit, offset=offset) def get_item(self, item_id: str) -> CurationItemSchema: """ @@ -167,32 +146,24 @@ def get_item(self, item_id: str) -> CurationItemSchema: Returns: CurationItemSchema: The item schema. """ - response: CurationItemSchema = self.api_call.get( - "/".join([self._items_path, item_id]), - as_json=True, - entity_type=CurationItemSchema, - ) - return response + return self.items[item_id].retrieve() def upsert_item( - self, item_id: str, item: CurationItemSchema + self, + item_id: str, + item: typing.Union[CurationItemSchema, CurationItemCreateSchema], ) -> CurationItemSchema: """ Create or update an item in this curation set. Args: item_id (str): The ID of the item. - item (CurationItemSchema): The item schema. + item (Union[CurationItemSchema, CurationItemCreateSchema]): The item schema. Returns: CurationItemSchema: The created or updated item. """ - response: CurationItemSchema = self.api_call.put( - "/".join([self._items_path, item_id]), - body=item, - entity_type=CurationItemSchema, - ) - return response + return self.items[item_id].upsert(item) def delete_item(self, item_id: str) -> CurationItemDeleteSchema: """ @@ -204,8 +175,4 @@ def delete_item(self, item_id: str) -> CurationItemDeleteSchema: Returns: CurationItemDeleteSchema: The deletion response. """ - response: CurationItemDeleteSchema = self.api_call.delete( - "/".join([self._items_path, item_id]), - entity_type=CurationItemDeleteSchema, - ) - return response + return self.items[item_id].delete() diff --git a/src/typesense/sync/curation_set_item.py b/src/typesense/sync/curation_set_item.py new file mode 100644 index 0000000..da75620 --- /dev/null +++ b/src/typesense/sync/curation_set_item.py @@ -0,0 +1,88 @@ +""" +This module provides async functionality for managing a single curation set item in Typesense. + +Classes: + CurationSetItem: Retrieves, upserts and deletes one item of a curation set (async). +""" + +from .api_call import ApiCall +from typesense.types.curation_set import ( + CurationItemCreateSchema, + CurationItemDeleteSchema, + CurationItemSchema, +) + + +class CurationSetItem: + """ + Manages a single item of a curation set in the Typesense API (async). + + Attributes: + api_call (ApiCall): The ApiCall instance for making async API requests. + curation_set_name (str): The name of the curation set. + item_id (str): The ID of the item. + """ + + def __init__( + self, api_call: ApiCall, curation_set_name: str, item_id: str + ) -> None: + self.api_call = api_call + self.curation_set_name = curation_set_name + self.item_id = item_id + + @property + def _endpoint_path(self) -> str: + from .curation_sets import CurationSets + + return "/".join( + [ + CurationSets.resource_path, + self.curation_set_name, + "items", + self.item_id, + ] + ) + + def retrieve(self) -> CurationItemSchema: + """ + Retrieve this item. + + Returns: + CurationItemSchema: The item schema. + """ + response: CurationItemSchema = self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=CurationItemSchema, + ) + return response + + def upsert(self, item: CurationItemCreateSchema) -> CurationItemSchema: + """ + Create or update this item. + + Args: + item (CurationItemCreateSchema): The item schema. + + Returns: + CurationItemSchema: The created or updated item. + """ + response: CurationItemSchema = self.api_call.put( + self._endpoint_path, + body=item, + entity_type=CurationItemSchema, + ) + return response + + def delete(self) -> CurationItemDeleteSchema: + """ + Delete this item. + + Returns: + CurationItemDeleteSchema: The deletion response. + """ + response: CurationItemDeleteSchema = self.api_call.delete( + self._endpoint_path, + entity_type=CurationItemDeleteSchema, + ) + return response diff --git a/src/typesense/sync/curation_set_items.py b/src/typesense/sync/curation_set_items.py new file mode 100644 index 0000000..e4a0168 --- /dev/null +++ b/src/typesense/sync/curation_set_items.py @@ -0,0 +1,98 @@ +""" +This module provides async functionality for managing the items of a curation set in Typesense. + +Classes: + CurationSetItems: Lists, upserts and accesses items of a single curation set (async). +""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from .api_call import ApiCall +from .curation_set_item import CurationSetItem +from typesense.types.curation_set import ( + CurationItemCreateSchema, + CurationItemSchema, + CurationSetListItemResponseSchema, +) + + +class CurationSetItems: + """ + Manages the items of a single curation set in the Typesense API (async). + + Attributes: + api_call (ApiCall): The ApiCall instance for making async API requests. + curation_set_name (str): The name of the curation set. + """ + + def __init__(self, api_call: ApiCall, curation_set_name: str) -> None: + self.api_call = api_call + self.curation_set_name = curation_set_name + + def __getitem__(self, item_id: str) -> CurationSetItem: + """ + Get an CurationSetItem instance for a given item ID. + + Example: + >>> item = curation_sets["products"].items["rule-1"] + """ + return CurationSetItem(self.api_call, self.curation_set_name, item_id) + + @property + def _endpoint_path(self) -> str: + from .curation_sets import CurationSets + + return "/".join( + [CurationSets.resource_path, self.curation_set_name, "items"] + ) + + def retrieve( + self, + *, + limit: typing.Union[int, None] = None, + offset: typing.Union[int, None] = None, + ) -> CurationSetListItemResponseSchema: + """ + List items in this curation set. + + Args: + limit (Union[int, None], optional): Maximum number of items to return. Defaults to None. + offset (Union[int, None], optional): Number of items to skip. Defaults to None. + + Returns: + CurationSetListItemResponseSchema: The list of items in the curation set. + """ + params: typing.Dict[str, typing.Union[int, None]] = { + "limit": limit, + "offset": offset, + } + clean_params: typing.Dict[str, int] = { + k: v for k, v in params.items() if v is not None + } + response: CurationSetListItemResponseSchema = self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=CurationSetListItemResponseSchema, + params=clean_params or None, + ) + return response + + def upsert( + self, item_id: str, item: CurationItemCreateSchema + ) -> CurationItemSchema: + """ + Create or update an item in this curation set. + + Args: + item_id (str): The ID of the item. + item (CurationItemCreateSchema): The item schema. + + Returns: + CurationItemSchema: The created or updated item. + """ + return self[item_id].upsert(item) diff --git a/src/typesense/types/curation_set.py b/src/typesense/types/curation_set.py index f99a020..46b40a9 100644 --- a/src/typesense/types/curation_set.py +++ b/src/typesense/types/curation_set.py @@ -106,12 +106,11 @@ class CurationItemSchema(typing.TypedDict): metadata: typing.NotRequired[typing.Dict[str, typing.Any]] -class CurationItemUpsertSchema(typing.TypedDict): +class CurationItemCreateSchema(typing.TypedDict): """ - Schema for creating or updating a single curation item. + Schema for creating or updating a single curation item through its own endpoint. """ - id: str rule: typing.Union[ CurationRuleTagsSchema, CurationRuleQueryCreateSchema, @@ -130,6 +129,14 @@ class CurationItemUpsertSchema(typing.TypedDict): metadata: typing.NotRequired[typing.Dict[str, typing.Any]] +class CurationItemUpsertSchema(CurationItemCreateSchema): + """ + Schema for a curation item inside a curation set upsert payload. + """ + + id: str + + class CurationSetUpsertSchema(typing.TypedDict): """ Payload schema to create or replace a curation set. diff --git a/tests/curation_set_test.py b/tests/curation_set_test.py index fe07278..43aa3c4 100644 --- a/tests/curation_set_test.py +++ b/tests/curation_set_test.py @@ -8,7 +8,7 @@ from typesense.sync.client import Client from typesense.sync.curation_set import CurationSet from typesense.sync.curation_sets import CurationSets -from typesense.types.curation_set import CurationItemSchema +from typesense.types.curation_set import CurationItemCreateSchema, CurationItemSchema pytestmark = pytest.mark.skipif( not is_v30_or_above( @@ -25,12 +25,20 @@ def test_paths(fake_curation_set: CurationSet) -> None: assert fake_curation_set._endpoint_path == "/curation_sets/products" # noqa: WPS437 - assert fake_curation_set._items_path == "/curation_sets/products/items" # noqa: WPS437 + assert fake_curation_set.items._endpoint_path == "/curation_sets/products/items" # noqa: WPS437 + assert ( + fake_curation_set.items["rule-1"]._endpoint_path # noqa: WPS437 + == "/curation_sets/products/items/rule-1" + ) def test_paths_async(fake_async_curation_set: AsyncCurationSet) -> None: assert fake_async_curation_set._endpoint_path == "/curation_sets/products" # noqa: WPS437 - assert fake_async_curation_set._items_path == "/curation_sets/products/items" # noqa: WPS437 + assert fake_async_curation_set.items._endpoint_path == "/curation_sets/products/items" # noqa: WPS437 + assert ( + fake_async_curation_set.items["rule-1"]._endpoint_path # noqa: WPS437 + == "/curation_sets/products/items/rule-1" + ) def test_actual_retrieve( @@ -356,3 +364,310 @@ async def test_actual_delete_item_async( response = await actual_async_curation_sets["products"].delete_item("rule-1") assert response == {"id": "rule-1"} + + +def test_actual_items_retrieve( + actual_curation_sets: CurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the CurationSetItems object can list items from Typesense Server.""" + response = actual_curation_sets["products"].items.retrieve() + + assert response == [ + { + "excludes": [ + { + "id": "999", + }, + ], + "filter_curated_hits": False, + "id": "rule-1", + "includes": [ + { + "id": "123", + "position": 1, + }, + ], + "remove_matched_tokens": False, + "rule": { + "match": "contains", + "query": "shoe", + "stem": False, + "synonyms": False, + }, + "stop_processing": True, + }, + ] + + +def test_actual_items_retrieve_paginated( + actual_curation_sets: CurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the CurationSetItems object can paginate items from Typesense Server.""" + items = actual_curation_sets["products"].items + items.upsert("rule-2", {"rule": {"tags": ["boot"]}, "excludes": [{"id": "888"}]}) + response = items.retrieve(limit=1, offset=1) + + assert [item["id"] for item in response] == ["rule-2"] + + +def test_actual_items_upsert( + actual_curation_sets: CurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the CurationSetItems object can upsert an item in Typesense Server.""" + payload: CurationItemCreateSchema = { + "rule": {"query": "boot", "match": "exact"}, + "includes": [{"id": "456", "position": 2}], + "excludes": [{"id": "888"}], + } + response = actual_curation_sets["products"].items.upsert("rule-2", payload) + + assert response == { + "excludes": [ + { + "id": "888", + }, + ], + "id": "rule-2", + "includes": [ + { + "id": "456", + "position": 2, + }, + ], + "rule": { + "match": "exact", + "query": "boot", + }, + } + + +def test_actual_item_retrieve( + actual_curation_sets: CurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the CurationSetItem object can retrieve an item from Typesense Server.""" + response = actual_curation_sets["products"].items["rule-1"].retrieve() + + assert response == { + "excludes": [ + { + "id": "999", + }, + ], + "filter_curated_hits": False, + "id": "rule-1", + "includes": [ + { + "id": "123", + "position": 1, + }, + ], + "remove_matched_tokens": False, + "rule": { + "match": "contains", + "query": "shoe", + "stem": False, + "synonyms": False, + }, + "stop_processing": True, + } + + +def test_actual_item_upsert( + actual_curation_sets: CurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the CurationSetItem object can upsert an item in Typesense Server.""" + payload: CurationItemCreateSchema = { + "rule": {"query": "boot", "match": "exact"}, + "includes": [{"id": "456", "position": 2}], + "excludes": [{"id": "888"}], + } + response = actual_curation_sets["products"].items["rule-2"].upsert(payload) + + assert response == { + "excludes": [ + { + "id": "888", + }, + ], + "id": "rule-2", + "includes": [ + { + "id": "456", + "position": 2, + }, + ], + "rule": { + "match": "exact", + "query": "boot", + }, + } + + +def test_actual_item_delete( + actual_curation_sets: CurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the CurationSetItem object can delete an item from Typesense Server.""" + response = actual_curation_sets["products"].items["rule-1"].delete() + + assert response == {"id": "rule-1"} + + +async def test_actual_items_retrieve_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSetItems object can list items from Typesense Server.""" + response = await actual_async_curation_sets["products"].items.retrieve() + + assert response == [ + { + "excludes": [ + { + "id": "999", + }, + ], + "filter_curated_hits": False, + "id": "rule-1", + "includes": [ + { + "id": "123", + "position": 1, + }, + ], + "remove_matched_tokens": False, + "rule": { + "match": "contains", + "query": "shoe", + "stem": False, + "synonyms": False, + }, + "stop_processing": True, + }, + ] + + +async def test_actual_items_upsert_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSetItems object can upsert an item in Typesense Server.""" + payload: CurationItemCreateSchema = { + "rule": {"query": "boot", "match": "exact"}, + "includes": [{"id": "456", "position": 2}], + "excludes": [{"id": "888"}], + } + response = ( + await actual_async_curation_sets["products"].items.upsert("rule-2", payload) + ) + + assert response == { + "excludes": [ + { + "id": "888", + }, + ], + "id": "rule-2", + "includes": [ + { + "id": "456", + "position": 2, + }, + ], + "rule": { + "match": "exact", + "query": "boot", + }, + } + + +async def test_actual_item_retrieve_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSetItem object can retrieve an item from Typesense Server.""" + response = await actual_async_curation_sets["products"].items["rule-1"].retrieve() + + assert response == { + "excludes": [ + { + "id": "999", + }, + ], + "filter_curated_hits": False, + "id": "rule-1", + "includes": [ + { + "id": "123", + "position": 1, + }, + ], + "remove_matched_tokens": False, + "rule": { + "match": "contains", + "query": "shoe", + "stem": False, + "synonyms": False, + }, + "stop_processing": True, + } + + +async def test_actual_item_upsert_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSetItem object can upsert an item in Typesense Server.""" + payload: CurationItemCreateSchema = { + "rule": {"query": "boot", "match": "exact"}, + "includes": [{"id": "456", "position": 2}], + "excludes": [{"id": "888"}], + } + response = ( + await actual_async_curation_sets["products"].items["rule-2"].upsert(payload) + ) + + assert response == { + "excludes": [ + { + "id": "888", + }, + ], + "id": "rule-2", + "includes": [ + { + "id": "456", + "position": 2, + }, + ], + "rule": { + "match": "exact", + "query": "boot", + }, + } + + +async def test_actual_item_delete_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSetItem object can delete an item from Typesense Server.""" + response = await actual_async_curation_sets["products"].items["rule-1"].delete() + + assert response == {"id": "rule-1"}