Skip to content

Commit 15c67dc

Browse files
committed
updates for opensearch tests
1 parent 22bbfa2 commit 15c67dc

File tree

4 files changed

+181
-83
lines changed

4 files changed

+181
-83
lines changed

key-value/key-value-aio/src/key_value/aio/stores/opensearch/store.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@
6868
},
6969
"value": {
7070
"properties": {
71-
"flattened": {
72-
"type": "flattened",
71+
"flat": {
72+
"type": "flat_object",
7373
},
7474
},
7575
},
@@ -101,14 +101,14 @@ def prepare_dump(self, data: dict[str, Any]) -> dict[str, Any]:
101101
value = data.pop("value")
102102

103103
data["value"] = {
104-
"flattened": value,
104+
"flat": value,
105105
}
106106

107107
return data
108108

109109
@override
110110
def prepare_load(self, data: dict[str, Any]) -> dict[str, Any]:
111-
data["value"] = data.pop("value").get("flattened")
111+
data["value"] = data.pop("value").get("flat")
112112

113113
return data
114114

@@ -224,7 +224,6 @@ def __init__(
224224
if opensearch_client:
225225
self._client = opensearch_client
226226
elif url:
227-
# Build kwargs for AsyncOpenSearch
228227
client_kwargs: dict[str, Any] = {
229228
"hosts": [url],
230229
"http_compress": True,

key-value/key-value-aio/tests/stores/opensearch/test_opensearch.py

Lines changed: 89 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import contextlib
22
from collections.abc import AsyncGenerator
33
from datetime import datetime, timedelta, timezone
4+
from typing import Any
45

56
import pytest
6-
from dirty_equals import IsFloat
7+
from dirty_equals import IsFloat, IsStr
8+
from elasticsearch import AsyncElasticsearch
79
from inline_snapshot import snapshot
810
from key_value.shared.stores.wait import async_wait_for_true
911
from key_value.shared.utils.managed_entry import ManagedEntry
1012
from opensearchpy import AsyncOpenSearch
1113
from typing_extensions import override
1214

15+
from key_value.aio.protocols.key_value import AsyncKeyValueProtocol
1316
from key_value.aio.stores.base import BaseStore
1417
from key_value.aio.stores.opensearch import OpenSearchStore
1518
from key_value.aio.stores.opensearch.store import (
@@ -21,10 +24,13 @@
2124
from tests.stores.base import BaseStoreTests, ContextManagerStoreTestMixin
2225

2326
TEST_SIZE_LIMIT = 1 * 1024 * 1024 # 1MB
24-
OS_HOST = "localhost"
25-
OS_PORT = 9200
26-
OS_URL = f"http://{OS_HOST}:{OS_PORT}"
27-
OS_CONTAINER_PORT = 9200
27+
LOCALHOST = "localhost"
28+
29+
CONTAINER_PORT = 9200
30+
HOST_PORT = 19200
31+
32+
OPENSEARCH_URL = f"http://{LOCALHOST}:{HOST_PORT}"
33+
2834

2935
WAIT_FOR_OPENSEARCH_TIMEOUT = 30
3036

@@ -35,15 +41,15 @@
3541

3642

3743
def get_opensearch_client() -> AsyncOpenSearch:
38-
return AsyncOpenSearch(hosts=[OS_URL], use_ssl=False, verify_certs=False)
44+
return AsyncOpenSearch(hosts=[OPENSEARCH_URL], use_ssl=False, verify_certs=False)
3945

4046

4147
async def ping_opensearch() -> bool:
42-
os_client: AsyncOpenSearch = get_opensearch_client()
48+
opensearch_client: AsyncOpenSearch = get_opensearch_client()
4349

44-
async with os_client:
50+
async with opensearch_client:
4551
try:
46-
return await os_client.ping()
52+
return await opensearch_client.ping()
4753
except Exception:
4854
return False
4955

@@ -69,7 +75,7 @@ def test_managed_entry_document_conversion():
6975

7076
assert document == snapshot(
7177
{
72-
"value": {"flattened": {"test": "test"}},
78+
"value": {"f": {"test": "test"}},
7379
"created_at": "2025-01-01T00:00:00+00:00",
7480
"expires_at": "2025-01-01T00:00:10+00:00",
7581
}
@@ -94,7 +100,7 @@ async def setup_opensearch(self, request: pytest.FixtureRequest) -> AsyncGenerat
94100
with docker_container(
95101
f"opensearch-test-{version}",
96102
os_image,
97-
{str(OS_CONTAINER_PORT): OS_PORT},
103+
{str(CONTAINER_PORT): HOST_PORT},
98104
{
99105
"discovery.type": "single-node",
100106
"DISABLE_SECURITY_PLUGIN": "true",
@@ -109,16 +115,16 @@ async def setup_opensearch(self, request: pytest.FixtureRequest) -> AsyncGenerat
109115

110116
@pytest.fixture
111117
async def opensearch_client(self, setup_opensearch: None) -> AsyncGenerator[AsyncOpenSearch, None]:
112-
os_client = get_opensearch_client()
118+
opensearch_client = get_opensearch_client()
113119

114-
async with os_client:
115-
await cleanup_opensearch_indices(opensearch_client=os_client)
120+
async with opensearch_client:
121+
await cleanup_opensearch_indices(opensearch_client=opensearch_client)
116122

117-
yield os_client
123+
yield opensearch_client
118124

119125
@override
120126
@pytest.fixture
121-
async def default_store(self, opensearch_client: AsyncOpenSearch) -> AsyncGenerator[BaseStore, None]:
127+
async def store(self, opensearch_client: AsyncOpenSearch) -> AsyncGenerator[BaseStore, None]:
122128
store = OpenSearchStore(
123129
opensearch_client=opensearch_client,
124130
index_prefix="opensearch-kv-store-e2e-test",
@@ -130,40 +136,86 @@ async def default_store(self, opensearch_client: AsyncOpenSearch) -> AsyncGenera
130136

131137
@override
132138
@pytest.fixture
133-
async def collection_sanitized_store(self, opensearch_client: AsyncOpenSearch) -> AsyncGenerator[BaseStore, None]:
139+
async def sanitizing_store(self, opensearch_client: AsyncOpenSearch) -> AsyncGenerator[BaseStore, None]:
134140
store = OpenSearchStore(
135141
opensearch_client=opensearch_client,
136142
index_prefix="opensearch-kv-store-e2e-test",
137143
default_collection="test-collection",
144+
key_sanitization_strategy=OpenSearchV1KeySanitizationStrategy(),
138145
collection_sanitization_strategy=OpenSearchV1CollectionSanitizationStrategy(),
139146
)
140147

141148
async with store:
142149
yield store
143150

144151
@override
145-
@pytest.fixture
146-
async def key_sanitized_store(self, opensearch_client: AsyncOpenSearch) -> AsyncGenerator[BaseStore, None]:
147-
store = OpenSearchStore(
148-
opensearch_client=opensearch_client,
149-
index_prefix="opensearch-kv-store-e2e-test",
150-
default_collection="test-collection",
151-
key_sanitization_strategy=OpenSearchV1KeySanitizationStrategy(),
152-
)
152+
@pytest.mark.timeout(120)
153+
async def test_store(self, store: BaseStore):
154+
"""Tests that the store is a valid AsyncKeyValueProtocol."""
155+
assert isinstance(store, AsyncKeyValueProtocol) is True
153156

154-
async with store:
155-
yield store
157+
@pytest.mark.skip(reason="Distributed Caches are unbounded")
158+
@override
159+
async def test_not_unbounded(self, store: BaseStore): ...
156160

161+
@pytest.mark.skip(reason="Skip concurrent tests on distributed caches")
157162
@override
158-
@pytest.fixture
159-
async def fully_sanitized_store(self, opensearch_client: AsyncOpenSearch) -> AsyncGenerator[BaseStore, None]:
160-
store = OpenSearchStore(
161-
opensearch_client=opensearch_client,
162-
index_prefix="opensearch-kv-store-e2e-test",
163-
default_collection="test-collection",
164-
key_sanitization_strategy=OpenSearchV1KeySanitizationStrategy(),
165-
collection_sanitization_strategy=OpenSearchV1CollectionSanitizationStrategy(),
163+
async def test_concurrent_operations(self, store: BaseStore): ...
164+
165+
@override
166+
async def test_long_collection_name(self, store: OpenSearchStore, sanitizing_store: OpenSearchStore): # pyright: ignore[reportIncompatibleMethodOverride]
167+
with pytest.raises(Exception): # noqa: B017, PT011
168+
await store.put(collection="test_collection" * 100, key="test_key", value={"test": "test"})
169+
170+
await sanitizing_store.put(collection="test_collection" * 100, key="test_key", value={"test": "test"})
171+
assert await sanitizing_store.get(collection="test_collection" * 100, key="test_key") == {"test": "test"}
172+
173+
@override
174+
async def test_long_key_name(self, store: OpenSearchStore, sanitizing_store: OpenSearchStore): # pyright: ignore[reportIncompatibleMethodOverride]
175+
"""Tests that a long key name will not raise an error."""
176+
with pytest.raises(Exception): # noqa: B017, PT011
177+
await store.put(collection="test_collection", key="test_key" * 100, value={"test": "test"})
178+
179+
await sanitizing_store.put(collection="test_collection", key="test_key" * 100, value={"test": "test"})
180+
assert await sanitizing_store.get(collection="test_collection", key="test_key" * 100) == {"test": "test"}
181+
182+
async def test_put_put_two_indices(self, store: OpenSearchStore, opensearch_client: AsyncOpenSearch):
183+
await store.put(collection="test_collection", key="test_key", value={"test": "test"})
184+
await store.put(collection="test_collection_2", key="test_key", value={"test": "test"})
185+
assert await store.get(collection="test_collection", key="test_key") == {"test": "test"}
186+
assert await store.get(collection="test_collection_2", key="test_key") == {"test": "test"}
187+
188+
indices: dict[str, Any] = await opensearch_client.indices.get(index="opensearch-kv-store-e2e-test-*")
189+
index_names: list[str] = list(indices.keys())
190+
assert index_names == snapshot(["opensearch-kv-store-e2e-test-test_collection", "opensearch-kv-store-e2e-test-test_collection_2"])
191+
192+
async def test_value_stored_as_f_object(self, store: OpenSearchStore, opensearch_client: AsyncElasticsearch):
193+
"""Verify values are stored as f objects, not JSON strings"""
194+
await store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30})
195+
196+
index_name = store._get_index_name(collection="test") # pyright: ignore[reportPrivateUsage]
197+
doc_id = store._get_document_id(key="test_key") # pyright: ignore[reportPrivateUsage]
198+
199+
response = await opensearch_client.get(index=index_name, id=doc_id)
200+
assert response.body["_source"] == snapshot(
201+
{
202+
"value": {"f": {"name": "Alice", "age": 30}},
203+
"created_at": IsStr(min_length=20, max_length=40),
204+
}
166205
)
167206

168-
async with store:
169-
yield store
207+
# Test with TTL
208+
await store.put(collection="test", key="test_key", value={"name": "Bob", "age": 25}, ttl=10)
209+
response = await opensearch_client.get(index=index_name, id=doc_id)
210+
assert response.body["_source"] == snapshot(
211+
{
212+
"value": {"f": {"name": "Bob", "age": 25}},
213+
"created_at": IsStr(min_length=20, max_length=40),
214+
"expires_at": IsStr(min_length=20, max_length=40),
215+
}
216+
)
217+
218+
@override
219+
async def test_special_characters_in_collection_name(self, store: OpenSearchStore, sanitizing_store: OpenSearchStore): # pyright: ignore[reportIncompatibleMethodOverride]
220+
"""Tests that a special characters in the collection name will not raise an error."""
221+
await super().test_special_characters_in_collection_name(store=sanitizing_store)

key-value/key-value-sync/src/key_value/sync/code_gen/stores/opensearch/store.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"expires_at": {"type": "date"},
5151
"collection": {"type": "keyword"},
5252
"key": {"type": "keyword"},
53-
"value": {"properties": {"flattened": {"type": "flattened"}}},
53+
"value": {"properties": {"flat": {"type": "flat_object"}}},
5454
}
5555
}
5656

@@ -78,13 +78,13 @@ def __init__(self) -> None:
7878
def prepare_dump(self, data: dict[str, Any]) -> dict[str, Any]:
7979
value = data.pop("value")
8080

81-
data["value"] = {"flattened": value}
81+
data["value"] = {"flat": value}
8282

8383
return data
8484

8585
@override
8686
def prepare_load(self, data: dict[str, Any]) -> dict[str, Any]:
87-
data["value"] = data.pop("value").get("flattened")
87+
data["value"] = data.pop("value").get("flat")
8888

8989
return data
9090

@@ -198,7 +198,6 @@ def __init__(
198198
if opensearch_client:
199199
self._client = opensearch_client
200200
elif url:
201-
# Build kwargs for AsyncOpenSearch
202201
client_kwargs: dict[str, Any] = {"hosts": [url], "http_compress": True, "timeout": 10, "max_retries": 3}
203202
if api_key:
204203
client_kwargs["api_key"] = api_key

0 commit comments

Comments
 (0)