Skip to content

Commit 26db9a3

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 1db09d5 commit 26db9a3

File tree

5 files changed

+458
-2
lines changed

5 files changed

+458
-2
lines changed

src/typesense/async_synonym.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
@warn_deprecation( # type: ignore[untyped-decorator]
43+
"The synonym API (collections/{collection}/synonyms/{synonym_id}) is deprecated is removed on v30+. "
44+
"Use synonym sets (synonym_sets) instead.",
45+
flag_name="synonyms_deprecation",
46+
)
47+
def __init__(
48+
self,
49+
api_call: AsyncApiCall,
50+
collection_name: str,
51+
synonym_id: str,
52+
) -> None:
53+
"""
54+
Initialize the AsyncSynonym object.
55+
56+
Args:
57+
api_call (AsyncApiCall): The API call object for making requests.
58+
collection_name (str): The name of the collection.
59+
synonym_id (str): The ID of the synonym.
60+
"""
61+
self.api_call = api_call
62+
self.collection_name = collection_name
63+
self.synonym_id = synonym_id
64+
65+
async def retrieve(self) -> SynonymSchema:
66+
"""
67+
Retrieve this specific synonym.
68+
69+
Returns:
70+
SynonymSchema: The schema containing the synonym details.
71+
"""
72+
return await self.api_call.get(self._endpoint_path(), entity_type=SynonymSchema)
73+
74+
async def delete(self) -> SynonymDeleteSchema:
75+
"""
76+
Delete this specific synonym.
77+
78+
Returns:
79+
SynonymDeleteSchema: The schema containing the deletion response.
80+
"""
81+
return await self.api_call.delete(
82+
self._endpoint_path(),
83+
entity_type=SynonymDeleteSchema,
84+
)
85+
86+
def _endpoint_path(self) -> str:
87+
"""
88+
Construct the API endpoint path for this specific synonym.
89+
90+
Returns:
91+
str: The constructed endpoint path.
92+
"""
93+
from typesense.async_collections import AsyncCollections
94+
from typesense.async_synonyms import AsyncSynonyms
95+
96+
return "/".join(
97+
[
98+
AsyncCollections.resource_path,
99+
self.collection_name,
100+
AsyncSynonyms.resource_path,
101+
self.synonym_id,
102+
],
103+
)

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+
@warn_deprecation( # type: ignore[untyped-decorator]
65+
"The synonyms API (collections/{collection}/synonyms) is deprecated is removed on v30+. "
66+
"Use synonym sets (synonym_sets) instead.",
67+
flag_name="synonyms_deprecation",
68+
)
69+
def __init__(self, api_call: AsyncApiCall, collection_name: str) -> None:
70+
"""
71+
Initialize the AsyncSynonyms object.
72+
73+
Args:
74+
api_call (AsyncApiCall): The API call object for making requests.
75+
collection_name (str): The name of the collection.
76+
"""
77+
self.api_call = api_call
78+
self.collection_name = collection_name
79+
self.synonyms: typing.Dict[str, AsyncSynonym] = {}
80+
81+
def __getitem__(self, synonym_id: str) -> AsyncSynonym:
82+
"""
83+
Get or create an AsyncSynonym object for a given synonym_id.
84+
85+
Args:
86+
synonym_id (str): The ID of the synonym.
87+
88+
Returns:
89+
AsyncSynonym: The AsyncSynonym object for the given ID.
90+
"""
91+
if not self.synonyms.get(synonym_id):
92+
self.synonyms[synonym_id] = AsyncSynonym(
93+
self.api_call,
94+
self.collection_name,
95+
synonym_id,
96+
)
97+
return self.synonyms[synonym_id]
98+
99+
async def upsert(
100+
self, synonym_id: str, schema: SynonymCreateSchema
101+
) -> SynonymSchema:
102+
"""
103+
Create or update a synonym.
104+
105+
Args:
106+
id (str): The ID of the synonym.
107+
schema (SynonymCreateSchema): The schema for creating or updating the synonym.
108+
109+
Returns:
110+
SynonymSchema: The created or updated synonym.
111+
"""
112+
response = await self.api_call.put(
113+
self._endpoint_path(synonym_id),
114+
body=schema,
115+
entity_type=SynonymSchema,
116+
)
117+
return response
118+
119+
async def retrieve(self) -> SynonymsRetrieveSchema:
120+
"""
121+
Retrieve all synonyms for the collection.
122+
123+
Returns:
124+
SynonymsRetrieveSchema: The schema containing all synonyms.
125+
"""
126+
response = await self.api_call.get(
127+
self._endpoint_path(),
128+
entity_type=SynonymsRetrieveSchema,
129+
)
130+
return response
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)