Skip to content

Commit 0487a9a

Browse files
committed
feat(synonyms): add async support for synonym operations
- add AsyncSynonym class for async individual synonym operations - add AsyncSynonyms class for async synonyms collection operations - add async tests for synonym and synonyms functionality - add async fixtures for testing async synonym operations - remove future annotations imports from test files
1 parent fc0f205 commit 0487a9a

File tree

5 files changed

+459
-2
lines changed

5 files changed

+459
-2
lines changed

src/typesense/async_synonym.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
This module provides async functionality for managing individual synonyms in Typesense.
3+
4+
Classes:
5+
- AsyncSynonym: Handles async operations related to a specific synonym within a collection.
6+
7+
Methods:
8+
- __init__: Initializes the AsyncSynonym object.
9+
- _endpoint_path: Constructs the API endpoint path for this specific synonym.
10+
- retrieve: Retrieves the details of this specific synonym.
11+
- delete: Deletes this specific synonym.
12+
13+
The AsyncSynonym class interacts with the Typesense API to manage operations on a
14+
specific synonym within a collection. It provides methods to retrieve and delete
15+
individual synonyms.
16+
17+
For more information regarding Synonyms, refer to the Synonyms [documentation]
18+
(https://typesense.org/docs/27.0/api/synonyms.html#synonyms).
19+
20+
This module uses type hinting and is compatible with Python 3.11+ as well as earlier
21+
versions through the use of the typing_extensions library.
22+
"""
23+
24+
from typesense.async_api_call import AsyncApiCall
25+
from typesense.logger import warn_deprecation
26+
from typesense.types.synonym import SynonymDeleteSchema, SynonymSchema
27+
28+
29+
class AsyncSynonym:
30+
"""
31+
Class for managing individual synonyms in a Typesense collection (async).
32+
33+
This class provides methods to interact with a specific synonym,
34+
including retrieving and deleting it.
35+
36+
Attributes:
37+
api_call (AsyncApiCall): The API call object for making requests.
38+
collection_name (str): The name of the collection.
39+
synonym_id (str): The ID of the synonym.
40+
"""
41+
42+
def __init__(
43+
self,
44+
api_call: AsyncApiCall,
45+
collection_name: str,
46+
synonym_id: str,
47+
) -> None:
48+
"""
49+
Initialize the AsyncSynonym object.
50+
51+
Args:
52+
api_call (AsyncApiCall): The API call object for making requests.
53+
collection_name (str): The name of the collection.
54+
synonym_id (str): The ID of the synonym.
55+
"""
56+
self.api_call = api_call
57+
self.collection_name = collection_name
58+
self.synonym_id = synonym_id
59+
60+
async def retrieve(self) -> SynonymSchema:
61+
"""
62+
Retrieve this specific synonym.
63+
64+
Returns:
65+
SynonymSchema: The schema containing the synonym details.
66+
"""
67+
return await self.api_call.get(self._endpoint_path, entity_type=SynonymSchema)
68+
69+
async def delete(self) -> SynonymDeleteSchema:
70+
"""
71+
Delete this specific synonym.
72+
73+
Returns:
74+
SynonymDeleteSchema: The schema containing the deletion response.
75+
"""
76+
return await self.api_call.delete(
77+
self._endpoint_path,
78+
entity_type=SynonymDeleteSchema,
79+
)
80+
81+
@property
82+
@warn_deprecation( # type: ignore[untyped-decorator]
83+
"The synonym API (collections/{collection}/synonyms/{synonym_id}) is deprecated is removed on v30+. "
84+
"Use synonym sets (synonym_sets) instead.",
85+
flag_name="synonyms_deprecation",
86+
)
87+
def _endpoint_path(self) -> str:
88+
"""
89+
Construct the API endpoint path for this specific synonym.
90+
91+
Returns:
92+
str: The constructed endpoint path.
93+
"""
94+
from typesense.async_collections import AsyncCollections
95+
from typesense.async_synonyms import AsyncSynonyms
96+
97+
return "/".join(
98+
[
99+
AsyncCollections.resource_path,
100+
self.collection_name,
101+
AsyncSynonyms.resource_path,
102+
self.synonym_id,
103+
],
104+
)

src/typesense/async_synonyms.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""
2+
This module provides async functionality for managing synonyms in Typesense.
3+
4+
Classes:
5+
- AsyncSynonyms: Handles async operations related to synonyms within a collection.
6+
7+
Methods:
8+
- __init__: Initializes the AsyncSynonyms object.
9+
- __getitem__: Retrieves or creates an AsyncSynonym object for a given synonym_id.
10+
- _endpoint_path: Constructs the API endpoint path for synonym operations.
11+
- upsert: Creates or updates a synonym.
12+
- retrieve: Retrieves all synonyms for the collection.
13+
14+
Attributes:
15+
- RESOURCE_PATH: The API resource path for synonyms.
16+
17+
The AsyncSynonyms class interacts with the Typesense API to manage synonym operations
18+
within a specific collection. It provides methods to create, update, and retrieve
19+
synonyms, as well as access individual AsyncSynonym objects.
20+
21+
For more information regarding Synonyms, refer to the Synonyms [documentation]
22+
(https://typesense.org/docs/27.0/api/synonyms.html#synonyms).
23+
24+
This module uses type hinting and is compatible with Python 3.11+ as well as earlier
25+
versions through the use of the typing_extensions library.
26+
"""
27+
28+
import sys
29+
30+
from typing_extensions import deprecated
31+
32+
from typesense.async_api_call import AsyncApiCall
33+
from typesense.async_synonym import AsyncSynonym
34+
from typesense.logger import warn_deprecation
35+
from typesense.types.synonym import (
36+
SynonymCreateSchema,
37+
SynonymSchema,
38+
SynonymsRetrieveSchema,
39+
)
40+
41+
if sys.version_info >= (3, 11):
42+
import typing
43+
else:
44+
import typing_extensions as typing
45+
46+
47+
@deprecated("AsyncSynonyms is deprecated on v30+. Use client.synonym_sets instead.")
48+
class AsyncSynonyms:
49+
"""
50+
Class for managing synonyms in a Typesense collection (async).
51+
52+
This class provides methods to interact with synonyms, including
53+
retrieving, creating, and updating them.
54+
55+
Attributes:
56+
RESOURCE_PATH (str): The API resource path for synonyms.
57+
api_call (AsyncApiCall): The API call object for making requests.
58+
collection_name (str): The name of the collection.
59+
synonyms (Dict[str, AsyncSynonym]): A dictionary of AsyncSynonym objects.
60+
"""
61+
62+
resource_path: typing.Final[str] = "synonyms"
63+
64+
def __init__(self, api_call: AsyncApiCall, collection_name: str) -> None:
65+
"""
66+
Initialize the AsyncSynonyms object.
67+
68+
Args:
69+
api_call (AsyncApiCall): The API call object for making requests.
70+
collection_name (str): The name of the collection.
71+
"""
72+
self.api_call = api_call
73+
self.collection_name = collection_name
74+
self.synonyms: typing.Dict[str, AsyncSynonym] = {}
75+
76+
def __getitem__(self, synonym_id: str) -> AsyncSynonym:
77+
"""
78+
Get or create an AsyncSynonym object for a given synonym_id.
79+
80+
Args:
81+
synonym_id (str): The ID of the synonym.
82+
83+
Returns:
84+
AsyncSynonym: The AsyncSynonym object for the given ID.
85+
"""
86+
if not self.synonyms.get(synonym_id):
87+
self.synonyms[synonym_id] = AsyncSynonym(
88+
self.api_call,
89+
self.collection_name,
90+
synonym_id,
91+
)
92+
return self.synonyms[synonym_id]
93+
94+
async def upsert(
95+
self, synonym_id: str, schema: SynonymCreateSchema
96+
) -> SynonymSchema:
97+
"""
98+
Create or update a synonym.
99+
100+
Args:
101+
id (str): The ID of the synonym.
102+
schema (SynonymCreateSchema): The schema for creating or updating the synonym.
103+
104+
Returns:
105+
SynonymSchema: The created or updated synonym.
106+
"""
107+
response = await self.api_call.put(
108+
self._endpoint_path(synonym_id),
109+
body=schema,
110+
entity_type=SynonymSchema,
111+
)
112+
return response
113+
114+
async def retrieve(self) -> SynonymsRetrieveSchema:
115+
"""
116+
Retrieve all synonyms for the collection.
117+
118+
Returns:
119+
SynonymsRetrieveSchema: The schema containing all synonyms.
120+
"""
121+
response = await self.api_call.get(
122+
self._endpoint_path(),
123+
entity_type=SynonymsRetrieveSchema,
124+
)
125+
return response
126+
127+
@warn_deprecation( # type: ignore[untyped-decorator]
128+
"The synonyms API (collections/{collection}/synonyms) is deprecated is removed on v30+. "
129+
"Use synonym sets (synonym_sets) instead.",
130+
flag_name="synonyms_deprecation",
131+
)
132+
def _endpoint_path(self, synonym_id: typing.Union[str, None] = None) -> str:
133+
"""
134+
Construct the API endpoint path for synonym operations.
135+
136+
Args:
137+
synonym_id (Union[str, None], optional): The ID of the synonym. Defaults to None.
138+
139+
Returns:
140+
str: The constructed endpoint path.
141+
"""
142+
from typesense.async_collections import AsyncCollections
143+
144+
synonym_id = synonym_id or ""
145+
return "/".join(
146+
[
147+
AsyncCollections.resource_path,
148+
self.collection_name,
149+
AsyncSynonyms.resource_path,
150+
synonym_id,
151+
],
152+
)

tests/fixtures/synonym_fixtures.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import requests
55

66
from typesense.api_call import ApiCall
7+
from typesense.async_api_call import AsyncApiCall
8+
from typesense.async_synonym import AsyncSynonym
9+
from typesense.async_synonyms import AsyncSynonyms
710
from typesense.synonym import Synonym
811
from typesense.synonyms import Synonyms
912

@@ -42,3 +45,25 @@ def actual_synonyms_fixture(actual_api_call: ApiCall) -> Synonyms:
4245
def fake_synonym_fixture(fake_api_call: ApiCall) -> Synonym:
4346
"""Return a Synonym object with test values."""
4447
return Synonym(fake_api_call, "companies", "company_synonym")
48+
49+
50+
@pytest.fixture(scope="function", name="actual_async_synonyms")
51+
def actual_async_synonyms_fixture(
52+
actual_async_api_call: AsyncApiCall,
53+
) -> AsyncSynonyms:
54+
"""Return a AsyncSynonyms object using a real API."""
55+
return AsyncSynonyms(actual_async_api_call, "companies")
56+
57+
58+
@pytest.fixture(scope="function", name="fake_async_synonyms")
59+
def fake_async_synonyms_fixture(
60+
fake_async_api_call: AsyncApiCall,
61+
) -> AsyncSynonyms:
62+
"""Return a AsyncSynonyms object with test values."""
63+
return AsyncSynonyms(fake_async_api_call, "companies")
64+
65+
66+
@pytest.fixture(scope="function", name="fake_async_synonym")
67+
def fake_async_synonym_fixture(fake_async_api_call: AsyncApiCall) -> AsyncSynonym:
68+
"""Return a AsyncSynonym object with test values."""
69+
return AsyncSynonym(fake_async_api_call, "companies", "company_synonym")

tests/synonym_test.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Tests for the Synonym class."""
22

3-
from __future__ import annotations
4-
53
import pytest
64

75
from tests.utils.object_assertions import (
@@ -11,8 +9,11 @@
119
)
1210
from tests.utils.version import is_v30_or_above
1311
from typesense.api_call import ApiCall
12+
from typesense.async_api_call import AsyncApiCall
13+
from typesense.async_collections import AsyncCollections
1414
from typesense.collections import Collections
1515
from typesense.client import Client
16+
from typesense.synonym import Synonym
1617

1718

1819
pytestmark = pytest.mark.skipif(
@@ -78,3 +79,63 @@ def test_actual_delete(
7879
response = actual_collections["companies"].synonyms["company_synonym"].delete()
7980

8081
assert response == {"id": "company_synonym"}
82+
83+
84+
def test_init_async(fake_async_api_call: AsyncApiCall) -> None:
85+
"""Test that the AsyncSynonym object is initialized correctly."""
86+
from typesense.async_synonym import AsyncSynonym
87+
88+
synonym = AsyncSynonym(fake_async_api_call, "companies", "company_synonym")
89+
90+
assert synonym.collection_name == "companies"
91+
assert synonym.synonym_id == "company_synonym"
92+
assert_match_object(synonym.api_call, fake_async_api_call)
93+
assert_object_lists_match(
94+
synonym.api_call.node_manager.nodes,
95+
fake_async_api_call.node_manager.nodes,
96+
)
97+
assert_match_object(
98+
synonym.api_call.config.nearest_node,
99+
fake_async_api_call.config.nearest_node,
100+
)
101+
assert (
102+
synonym._endpoint_path() # noqa: WPS437
103+
== "/collections/companies/synonyms/company_synonym"
104+
)
105+
106+
107+
async def test_actual_retrieve_async(
108+
actual_async_collections: AsyncCollections,
109+
delete_all: None,
110+
create_synonym: None,
111+
) -> None:
112+
"""Test that the AsyncSynonym object can retrieve an synonym from Typesense Server."""
113+
response = (
114+
await actual_async_collections["companies"]
115+
.synonyms["company_synonym"]
116+
.retrieve()
117+
)
118+
119+
assert response["id"] == "company_synonym"
120+
121+
assert response["synonyms"] == ["companies", "corporations", "firms"]
122+
assert_to_contain_object(
123+
response,
124+
{
125+
"id": "company_synonym",
126+
"synonyms": ["companies", "corporations", "firms"],
127+
},
128+
)
129+
130+
131+
async def test_actual_delete_async(
132+
actual_async_collections: AsyncCollections,
133+
delete_all: None,
134+
create_synonym: None,
135+
) -> None:
136+
"""Test that the AsyncSynonym object can delete an synonym from Typesense Server."""
137+
response = (
138+
await actual_async_collections["companies"].synonyms["company_synonym"].delete()
139+
)
140+
141+
assert response == {"id": "company_synonym"}

0 commit comments

Comments
 (0)