Skip to content

Commit f5e27fd

Browse files
committed
feat(api-keys): add async support for key operations
- add AsyncKey class for async individual key operations - add AsyncKeys class for async keys collection operations - add async tests for key and keys functionality - add async fixtures for testing async key operations
1 parent 9a62bc2 commit f5e27fd

5 files changed

Lines changed: 449 additions & 0 deletions

File tree

src/typesense/async_key.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
This module provides async functionality for managing individual API keys in Typesense.
3+
4+
It contains the AsyncKey class, which allows for retrieving and deleting
5+
API keys asynchronously.
6+
7+
Classes:
8+
AsyncKey: Manages async operations on a single API key in the Typesense API.
9+
10+
Dependencies:
11+
- typesense.async_api_call: Provides the AsyncApiCall class for making async API requests.
12+
- typesense.types.key: Provides ApiKeyDeleteSchema and ApiKeySchema types.
13+
14+
Note: This module uses conditional imports to support both Python 3.11+ and earlier versions.
15+
"""
16+
17+
from typesense.async_api_call import AsyncApiCall
18+
from typesense.types.key import ApiKeyDeleteSchema, ApiKeySchema
19+
20+
21+
class AsyncKey:
22+
"""
23+
Manages async operations on a single API key in the Typesense API.
24+
25+
This class provides async methods to retrieve and delete an API key.
26+
27+
Attributes:
28+
key_id (int): The ID of the API key.
29+
api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests.
30+
"""
31+
32+
def __init__(self, api_call: AsyncApiCall, key_id: int) -> None:
33+
"""
34+
Initialize the AsyncKey instance.
35+
36+
Args:
37+
api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests.
38+
key_id (int): The ID of the API key.
39+
"""
40+
self.key_id = key_id
41+
self.api_call = api_call
42+
43+
async def retrieve(self) -> ApiKeySchema:
44+
"""
45+
Retrieve this specific API key.
46+
47+
Returns:
48+
ApiKeySchema: The schema containing the API key details.
49+
"""
50+
response: ApiKeySchema = await self.api_call.get(
51+
self._endpoint_path,
52+
as_json=True,
53+
entity_type=ApiKeySchema,
54+
)
55+
return response
56+
57+
async def delete(self) -> ApiKeyDeleteSchema:
58+
"""
59+
Delete this specific API key.
60+
61+
Returns:
62+
ApiKeyDeleteSchema: The schema containing the deletion response.
63+
"""
64+
response: ApiKeyDeleteSchema = await self.api_call.delete(
65+
self._endpoint_path,
66+
entity_type=ApiKeyDeleteSchema,
67+
)
68+
return response
69+
70+
@property
71+
def _endpoint_path(self) -> str:
72+
"""
73+
Construct the API endpoint path for this specific API key.
74+
75+
Returns:
76+
str: The constructed endpoint path.
77+
"""
78+
from typesense.async_keys import AsyncKeys
79+
80+
return "/".join([AsyncKeys.resource_path, str(self.key_id)])

src/typesense/async_keys.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"""
2+
This module provides async functionality for managing API keys in Typesense.
3+
4+
It contains the AsyncKeys class, which allows for creating, retrieving, and
5+
generating scoped search keys asynchronously.
6+
7+
Classes:
8+
AsyncKeys: Manages API keys in the Typesense API (async).
9+
10+
Dependencies:
11+
- typesense.async_api_call: Provides the AsyncApiCall class for making async API requests.
12+
- typesense.async_key: Provides the AsyncKey class for individual API key operations.
13+
- typesense.types.document: Provides GenerateScopedSearchKeyParams type.
14+
- typesense.types.key: Provides various API key schema types.
15+
16+
Note: This module uses conditional imports to support both Python 3.11+ and earlier versions.
17+
"""
18+
19+
import base64
20+
import hashlib
21+
import hmac
22+
import json
23+
import sys
24+
25+
from typesense.async_api_call import AsyncApiCall
26+
from typesense.async_key import AsyncKey
27+
from typesense.types.document import GenerateScopedSearchKeyParams
28+
from typesense.types.key import (
29+
ApiKeyCreateResponseSchema,
30+
ApiKeyCreateSchema,
31+
ApiKeyRetrieveSchema,
32+
ApiKeySchema,
33+
)
34+
35+
if sys.version_info >= (3, 11):
36+
import typing
37+
else:
38+
import typing_extensions as typing
39+
40+
41+
class AsyncKeys:
42+
"""
43+
Manages API keys in the Typesense API (async).
44+
45+
This class provides async methods to create, retrieve, and generate scoped search keys.
46+
47+
Attributes:
48+
resource_path (str): The API endpoint path for key operations.
49+
api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests.
50+
keys (Dict[int, AsyncKey]): A dictionary of AsyncKey instances, keyed by key ID.
51+
"""
52+
53+
resource_path: typing.Final[str] = "/keys"
54+
55+
def __init__(self, api_call: AsyncApiCall) -> None:
56+
"""
57+
Initialize the AsyncKeys instance.
58+
59+
Args:
60+
api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests.
61+
"""
62+
self.api_call = api_call
63+
self.keys: typing.Dict[int, AsyncKey] = {}
64+
65+
def __getitem__(self, key_id: int) -> AsyncKey:
66+
"""
67+
Get or create an AsyncKey instance for a given key ID.
68+
69+
This method allows accessing API keys using dictionary-like syntax.
70+
If the AsyncKey instance doesn't exist, it creates a new one.
71+
72+
Args:
73+
key_id (int): The ID of the API key.
74+
75+
Returns:
76+
AsyncKey: The AsyncKey instance for the specified key ID.
77+
78+
Example:
79+
>>> keys = AsyncKeys(async_api_call)
80+
>>> key = keys[1]
81+
"""
82+
if not self.keys.get(key_id):
83+
self.keys[key_id] = AsyncKey(self.api_call, key_id)
84+
return self.keys[key_id]
85+
86+
async def create(self, schema: ApiKeyCreateSchema) -> ApiKeyCreateResponseSchema:
87+
"""
88+
Create a new API key.
89+
90+
Args:
91+
schema (ApiKeyCreateSchema): The schema for creating the API key.
92+
93+
Returns:
94+
ApiKeyCreateResponseSchema: The created API key.
95+
96+
Example:
97+
>>> keys = AsyncKeys(async_api_call)
98+
>>> key = await keys.create(
99+
... {
100+
... "actions": ["documents:search"],
101+
... "collections": ["companies"],
102+
... "description": "Search-only key",
103+
... }
104+
... )
105+
"""
106+
response: ApiKeySchema = await self.api_call.post(
107+
AsyncKeys.resource_path,
108+
as_json=True,
109+
body=schema,
110+
entity_type=ApiKeySchema,
111+
)
112+
return response
113+
114+
def generate_scoped_search_key(
115+
self,
116+
search_key: str,
117+
key_parameters: GenerateScopedSearchKeyParams,
118+
) -> bytes:
119+
"""
120+
Generate a scoped search key.
121+
122+
Note: This is a synchronous method as it performs local computation
123+
and does not make any API calls. Only a key generated with the
124+
`documents:search` action will be accepted by the server.
125+
126+
Args:
127+
search_key (str): The search key to use as a base.
128+
key_parameters (GenerateScopedSearchKeyParams): Parameters for the scoped key.
129+
130+
Returns:
131+
bytes: The generated scoped search key.
132+
133+
Example:
134+
>>> keys = AsyncKeys(async_api_call)
135+
>>> scoped_key = keys.generate_scoped_search_key(
136+
... "KmacipDKNqAM3YiigXfw5pZvNOrPQUba",
137+
... {"q": "search query", "collection": "companies"},
138+
... )
139+
"""
140+
params_str = json.dumps(key_parameters)
141+
digest = base64.b64encode(
142+
hmac.new(
143+
search_key.encode("utf-8"),
144+
params_str.encode("utf-8"),
145+
digestmod=hashlib.sha256,
146+
).digest(),
147+
)
148+
key_prefix = search_key[:4]
149+
raw_scoped_key = f"{digest.decode('utf-8')}{key_prefix}{params_str}"
150+
return base64.b64encode(raw_scoped_key.encode("utf-8"))
151+
152+
async def retrieve(self) -> ApiKeyRetrieveSchema:
153+
"""
154+
Retrieve all API keys.
155+
156+
Returns:
157+
ApiKeyRetrieveSchema: The schema containing all API keys.
158+
159+
Example:
160+
>>> keys = AsyncKeys(async_api_call)
161+
>>> all_keys = await keys.retrieve()
162+
>>> for key in all_keys["keys"]:
163+
... print(key["id"])
164+
"""
165+
response: ApiKeyRetrieveSchema = await self.api_call.get(
166+
AsyncKeys.resource_path,
167+
entity_type=ApiKeyRetrieveSchema,
168+
as_json=True,
169+
)
170+
return response

tests/fixtures/key_fixtures.py

Lines changed: 21 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_key import AsyncKey
9+
from typesense.async_keys import AsyncKeys
710
from typesense.key import Key
811
from typesense.keys import Keys
912

@@ -61,3 +64,21 @@ def fake_keys_fixture(fake_api_call: ApiCall) -> Keys:
6164
def fake_key_fixture(fake_api_call: ApiCall) -> Key:
6265
"""Return a Key object with test values."""
6366
return Key(fake_api_call, 1)
67+
68+
69+
@pytest.fixture(scope="function", name="actual_async_keys")
70+
def actual_async_keys_fixture(actual_async_api_call: AsyncApiCall) -> AsyncKeys:
71+
"""Return a AsyncKeys object using a real API."""
72+
return AsyncKeys(actual_async_api_call)
73+
74+
75+
@pytest.fixture(scope="function", name="fake_async_keys")
76+
def fake_async_keys_fixture(fake_async_api_call: AsyncApiCall) -> AsyncKeys:
77+
"""Return a AsyncKeys object with test values."""
78+
return AsyncKeys(fake_async_api_call)
79+
80+
81+
@pytest.fixture(scope="function", name="fake_async_key")
82+
def fake_async_key_fixture(fake_async_api_call: AsyncApiCall) -> AsyncKey:
83+
"""Return a AsyncKey object with test values."""
84+
return AsyncKey(fake_async_api_call, 1)

tests/key_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
assert_to_contain_object,
1010
)
1111
from typesense.api_call import ApiCall
12+
from typesense.async_api_call import AsyncApiCall
13+
from typesense.async_key import AsyncKey
14+
from typesense.async_keys import AsyncKeys
1215
from typesense.key import Key
1316
from typesense.keys import Keys
1417

@@ -30,6 +33,23 @@ def test_init(fake_api_call: ApiCall) -> None:
3033
assert key._endpoint_path == "/keys/3" # noqa: WPS437
3134

3235

36+
def test_init_async(fake_async_api_call: AsyncApiCall) -> None:
37+
"""Test that the AsyncKey object is initialized correctly."""
38+
key = AsyncKey(fake_async_api_call, 3)
39+
40+
assert key.key_id == 3
41+
assert_match_object(key.api_call, fake_async_api_call)
42+
assert_object_lists_match(
43+
key.api_call.node_manager.nodes,
44+
fake_async_api_call.node_manager.nodes,
45+
)
46+
assert_match_object(
47+
key.api_call.config.nearest_node,
48+
fake_async_api_call.config.nearest_node,
49+
)
50+
assert key._endpoint_path == "/keys/3" # noqa: WPS437
51+
52+
3353
def test_actual_retrieve(
3454
actual_keys: Keys,
3555
delete_all_keys: None,
@@ -60,3 +80,35 @@ def test_actual_delete(
6080
response = actual_keys[create_key_id].delete()
6181

6282
assert response == {"id": create_key_id}
83+
84+
85+
async def test_actual_retrieve_async(
86+
actual_async_keys: AsyncKeys,
87+
delete_all_keys: None,
88+
delete_all: None,
89+
create_key_id: int,
90+
) -> None:
91+
"""Test that the AsyncKey object can retrieve an key from Typesense Server."""
92+
response = await actual_async_keys[create_key_id].retrieve()
93+
94+
assert_to_contain_object(
95+
response,
96+
{
97+
"actions": ["documents:search"],
98+
"collections": ["companies"],
99+
"description": "Search-only key",
100+
"id": create_key_id,
101+
},
102+
)
103+
104+
105+
async def test_actual_delete_async(
106+
actual_async_keys: AsyncKeys,
107+
delete_all_keys: None,
108+
delete_all: None,
109+
create_key_id: int,
110+
) -> None:
111+
"""Test that the AsyncKey object can delete an key from Typesense Server."""
112+
response = await actual_async_keys[create_key_id].delete()
113+
114+
assert response == {"id": create_key_id}

0 commit comments

Comments
 (0)