aiagents4pharma 1.45.1__py3-none-any.whl → 1.46.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiagents4pharma/talk2aiagents4pharma/configs/app/__init__.py +0 -0
- aiagents4pharma/talk2aiagents4pharma/configs/app/frontend/__init__.py +0 -0
- aiagents4pharma/talk2aiagents4pharma/configs/app/frontend/default.yaml +102 -0
- aiagents4pharma/talk2aiagents4pharma/configs/config.yaml +1 -0
- aiagents4pharma/talk2aiagents4pharma/tests/test_main_agent.py +144 -54
- aiagents4pharma/talk2biomodels/api/__init__.py +1 -1
- aiagents4pharma/talk2biomodels/configs/app/__init__.py +0 -0
- aiagents4pharma/talk2biomodels/configs/app/frontend/__init__.py +0 -0
- aiagents4pharma/talk2biomodels/configs/app/frontend/default.yaml +72 -0
- aiagents4pharma/talk2biomodels/configs/config.yaml +1 -0
- aiagents4pharma/talk2biomodels/tests/test_api.py +0 -30
- aiagents4pharma/talk2biomodels/tests/test_get_annotation.py +1 -1
- aiagents4pharma/talk2biomodels/tools/get_annotation.py +1 -10
- aiagents4pharma/talk2knowledgegraphs/configs/app/frontend/default.yaml +42 -26
- aiagents4pharma/talk2knowledgegraphs/configs/config.yaml +1 -0
- aiagents4pharma/talk2knowledgegraphs/configs/tools/multimodal_subgraph_extraction/default.yaml +4 -23
- aiagents4pharma/talk2knowledgegraphs/configs/utils/database/milvus/__init__.py +3 -0
- aiagents4pharma/talk2knowledgegraphs/configs/utils/database/milvus/default.yaml +61 -0
- aiagents4pharma/talk2knowledgegraphs/entrypoint.sh +1 -11
- aiagents4pharma/talk2knowledgegraphs/milvus_data_dump.py +11 -10
- aiagents4pharma/talk2knowledgegraphs/tests/test_agents_t2kg_agent.py +193 -73
- aiagents4pharma/talk2knowledgegraphs/tests/test_tools_milvus_multimodal_subgraph_extraction.py +1375 -667
- aiagents4pharma/talk2knowledgegraphs/tests/test_utils_database_milvus_connection_manager.py +812 -0
- aiagents4pharma/talk2knowledgegraphs/tests/test_utils_extractions_milvus_multimodal_pcst.py +723 -539
- aiagents4pharma/talk2knowledgegraphs/tools/milvus_multimodal_subgraph_extraction.py +474 -58
- aiagents4pharma/talk2knowledgegraphs/utils/database/__init__.py +5 -0
- aiagents4pharma/talk2knowledgegraphs/utils/database/milvus_connection_manager.py +586 -0
- aiagents4pharma/talk2knowledgegraphs/utils/extractions/milvus_multimodal_pcst.py +240 -8
- aiagents4pharma/talk2scholars/configs/app/frontend/default.yaml +67 -31
- {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/METADATA +10 -1
- {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/RECORD +33 -23
- aiagents4pharma/talk2biomodels/api/kegg.py +0 -87
- {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/WHEEL +0 -0
- {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,812 @@
|
|
1
|
+
"""Unit tests for MilvusConnectionManager with lightweight fakes.
|
2
|
+
|
3
|
+
Focuses on exercising success and failure branches without real Milvus.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import asyncio as _asyncio
|
7
|
+
import importlib
|
8
|
+
from types import SimpleNamespace
|
9
|
+
|
10
|
+
import pytest
|
11
|
+
from pymilvus.exceptions import MilvusException
|
12
|
+
|
13
|
+
from ..utils.database.milvus_connection_manager import (
|
14
|
+
MilvusConnectionManager,
|
15
|
+
QueryParams,
|
16
|
+
SearchParams,
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
class FakeConnections:
|
21
|
+
"""fake pymilvus.connections module"""
|
22
|
+
|
23
|
+
def __init__(self):
|
24
|
+
self._map = {}
|
25
|
+
self._addr = {}
|
26
|
+
|
27
|
+
def has_connection(self, alias):
|
28
|
+
"""has_connection"""
|
29
|
+
return alias in self._map
|
30
|
+
|
31
|
+
def connect(self, alias, **kwargs):
|
32
|
+
"""Connect using keyword args to avoid signature bloat in tests."""
|
33
|
+
host = kwargs.get("host")
|
34
|
+
port = kwargs.get("port")
|
35
|
+
user = kwargs.get("user")
|
36
|
+
password = kwargs.get("password")
|
37
|
+
self._map[alias] = {
|
38
|
+
"host": host,
|
39
|
+
"port": port,
|
40
|
+
"user": user,
|
41
|
+
"password": password,
|
42
|
+
}
|
43
|
+
self._addr[alias] = (host, port)
|
44
|
+
|
45
|
+
def disconnect(self, alias):
|
46
|
+
"""disconnect"""
|
47
|
+
self._map.pop(alias, None)
|
48
|
+
self._addr.pop(alias, None)
|
49
|
+
|
50
|
+
def get_connection_addr(self, alias):
|
51
|
+
"""connection address"""
|
52
|
+
return self._addr.get(alias, None)
|
53
|
+
|
54
|
+
|
55
|
+
class FakeDB:
|
56
|
+
"""fake pymilvus.db module"""
|
57
|
+
|
58
|
+
def __init__(self):
|
59
|
+
"""init"""
|
60
|
+
self._using = None
|
61
|
+
|
62
|
+
def using_database(self, name):
|
63
|
+
"""Select database by name."""
|
64
|
+
self._using = name
|
65
|
+
|
66
|
+
def current_database(self):
|
67
|
+
"""Return current database selection."""
|
68
|
+
return self._using
|
69
|
+
|
70
|
+
|
71
|
+
class FakeCollection:
|
72
|
+
"""fake pymilvus Collection class"""
|
73
|
+
|
74
|
+
registry = {}
|
75
|
+
|
76
|
+
def __init__(self, name):
|
77
|
+
"""init"""
|
78
|
+
self.name = name
|
79
|
+
# Default num_entities for stats fallback
|
80
|
+
self.num_entities = FakeCollection.registry.get(name, {}).get("num_entities", 7)
|
81
|
+
|
82
|
+
def load(self):
|
83
|
+
"""load"""
|
84
|
+
FakeCollection.registry.setdefault(self.name, {}).update({"loaded": True})
|
85
|
+
|
86
|
+
def query(self, **_kwargs):
|
87
|
+
"""Query stub returning a single row."""
|
88
|
+
return [{"id": 1}]
|
89
|
+
|
90
|
+
def search(self, **kwargs):
|
91
|
+
"""Search stub returning synthetic hits.
|
92
|
+
|
93
|
+
Accepts keyword args similar to Milvus and reads `limit`.
|
94
|
+
"""
|
95
|
+
limit = int(kwargs.get("limit") or 1)
|
96
|
+
|
97
|
+
class Hit:
|
98
|
+
"""hit"""
|
99
|
+
|
100
|
+
def __init__(self, idx, score):
|
101
|
+
"""init"""
|
102
|
+
self.id = idx
|
103
|
+
self.score = score
|
104
|
+
|
105
|
+
def get_id(self):
|
106
|
+
"""Return id to satisfy public-method count."""
|
107
|
+
return self.id
|
108
|
+
|
109
|
+
def to_dict(self):
|
110
|
+
"""Return a dict representation of the hit."""
|
111
|
+
return {"id": self.id, "score": self.score}
|
112
|
+
|
113
|
+
return [[Hit(i, 1.0 - 0.1 * i) for i in range(limit)]]
|
114
|
+
|
115
|
+
|
116
|
+
class FakeSyncClient:
|
117
|
+
"""fake pymilvus MilvusClient class"""
|
118
|
+
|
119
|
+
def __init__(self, uri, token, db_name):
|
120
|
+
"""init"""
|
121
|
+
self.uri = uri
|
122
|
+
self.token = token
|
123
|
+
self.db_name = db_name
|
124
|
+
|
125
|
+
def info(self):
|
126
|
+
"""Return connection info."""
|
127
|
+
return {"uri": self.uri, "db": self.db_name}
|
128
|
+
|
129
|
+
def close(self):
|
130
|
+
"""Close stub for symmetry with async client."""
|
131
|
+
return True
|
132
|
+
|
133
|
+
|
134
|
+
class FakeAsyncClient:
|
135
|
+
"""fake pymilvus AsyncMilvusClient class"""
|
136
|
+
|
137
|
+
def __init__(self, uri, token, db_name):
|
138
|
+
"""init"""
|
139
|
+
self.uri = uri
|
140
|
+
self.token = token
|
141
|
+
self.db_name = db_name
|
142
|
+
self._closed = False
|
143
|
+
|
144
|
+
async def load_collection(self, collection_name):
|
145
|
+
"""load_collection"""
|
146
|
+
# mark loaded in registry
|
147
|
+
FakeCollection.registry.setdefault(collection_name, {}).update({"loaded_async": True})
|
148
|
+
|
149
|
+
async def search(self, **kwargs):
|
150
|
+
"""Async search stub using kwargs; returns synthetic hits."""
|
151
|
+
limit = int(kwargs.get("limit") or 0)
|
152
|
+
return [[{"id": i, "distance": 0.1 * i} for i in range(limit)]]
|
153
|
+
|
154
|
+
async def query(self, **kwargs):
|
155
|
+
"""Async query stub; echoes the provided filter expr."""
|
156
|
+
return [{"ok": True, "filter": kwargs.get("filter")}] # type: ignore[index]
|
157
|
+
|
158
|
+
async def close(self):
|
159
|
+
"""simulate close"""
|
160
|
+
self._closed = True
|
161
|
+
|
162
|
+
|
163
|
+
@pytest.fixture(autouse=True)
|
164
|
+
def patch_pymilvus(monkeypatch):
|
165
|
+
"""
|
166
|
+
Patch the pymilvus symbols inside the module-under-test namespace.
|
167
|
+
"""
|
168
|
+
|
169
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
170
|
+
|
171
|
+
# fresh fakes per test
|
172
|
+
fake_conn = FakeConnections()
|
173
|
+
fake_db = FakeDB()
|
174
|
+
|
175
|
+
monkeypatch.setattr(mod, "connections", fake_conn, raising=True)
|
176
|
+
monkeypatch.setattr(mod, "db", fake_db, raising=True)
|
177
|
+
monkeypatch.setattr(mod, "Collection", FakeCollection, raising=True)
|
178
|
+
monkeypatch.setattr(mod, "MilvusClient", FakeSyncClient, raising=True)
|
179
|
+
monkeypatch.setattr(mod, "AsyncMilvusClient", FakeAsyncClient, raising=True)
|
180
|
+
|
181
|
+
yield
|
182
|
+
# cleanup
|
183
|
+
MilvusConnectionManager.clear_instances()
|
184
|
+
FakeCollection.registry.clear()
|
185
|
+
|
186
|
+
|
187
|
+
@pytest.fixture(name="cfg")
|
188
|
+
def cfg_fixture():
|
189
|
+
"""cfg fixture"""
|
190
|
+
# minimal cfg namespace with milvus_db sub-keys used by the manager
|
191
|
+
return SimpleNamespace(
|
192
|
+
milvus_db=SimpleNamespace(
|
193
|
+
host="127.0.0.1",
|
194
|
+
port=19530,
|
195
|
+
user="u",
|
196
|
+
password="p",
|
197
|
+
database_name="dbX",
|
198
|
+
alias="default",
|
199
|
+
)
|
200
|
+
)
|
201
|
+
|
202
|
+
|
203
|
+
def test_singleton_and_init(cfg):
|
204
|
+
""" "singleton and init"""
|
205
|
+
# Two instances with same config key should be identical
|
206
|
+
a = MilvusConnectionManager(cfg)
|
207
|
+
b = MilvusConnectionManager(cfg)
|
208
|
+
assert a is b
|
209
|
+
# basic attributes initialized once
|
210
|
+
assert a.database_name == "dbX"
|
211
|
+
|
212
|
+
|
213
|
+
def test_ensure_connection_creates_and_reuses(cfg):
|
214
|
+
"""ensure_connection creates and reuses"""
|
215
|
+
mgr = MilvusConnectionManager(cfg)
|
216
|
+
# First call creates connection and sets db
|
217
|
+
assert mgr.ensure_connection() is True
|
218
|
+
# Cover FakeDB.current_database accessor
|
219
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
220
|
+
assert mod.db.current_database() == "dbX"
|
221
|
+
# Second call should reuse
|
222
|
+
assert mgr.ensure_connection() is True
|
223
|
+
|
224
|
+
|
225
|
+
def test_get_connection_info_connected_and_disconnected(cfg):
|
226
|
+
"""connection info connected and disconnected"""
|
227
|
+
mgr = MilvusConnectionManager(cfg)
|
228
|
+
# before ensure, not connected
|
229
|
+
info = mgr.get_connection_info()
|
230
|
+
assert info["connected"] is False
|
231
|
+
# after ensure, connected
|
232
|
+
mgr.ensure_connection()
|
233
|
+
info2 = mgr.get_connection_info()
|
234
|
+
assert info2["connected"] is True
|
235
|
+
assert info2["database"] == "dbX"
|
236
|
+
assert info2["connection_address"] == ("127.0.0.1", 19530)
|
237
|
+
|
238
|
+
|
239
|
+
def test_get_sync_and_async_client(cfg):
|
240
|
+
""" "sync and async client singleton"""
|
241
|
+
mgr = MilvusConnectionManager(cfg)
|
242
|
+
c1 = mgr.get_sync_client()
|
243
|
+
c2 = mgr.get_sync_client()
|
244
|
+
assert c1 is c2
|
245
|
+
# Exercise FakeSyncClient helpers directly (avoid static type lint on MilvusClient)
|
246
|
+
helper_client = FakeSyncClient(uri="uri", token="tk", db_name="dbX")
|
247
|
+
assert helper_client.info()["db"] == "dbX"
|
248
|
+
assert helper_client.close() is True
|
249
|
+
a1 = mgr.get_async_client()
|
250
|
+
a2 = mgr.get_async_client()
|
251
|
+
assert a1 is a2
|
252
|
+
|
253
|
+
|
254
|
+
def test_test_connection_success(cfg):
|
255
|
+
"""connection success"""
|
256
|
+
mgr = MilvusConnectionManager(cfg)
|
257
|
+
assert mgr.test_connection() is True
|
258
|
+
|
259
|
+
|
260
|
+
def test_get_collection_success(cfg):
|
261
|
+
"""collection success"""
|
262
|
+
mgr = MilvusConnectionManager(cfg)
|
263
|
+
coll = mgr.get_collection("dbX_nodes")
|
264
|
+
assert isinstance(coll, FakeCollection)
|
265
|
+
# ensure loaded
|
266
|
+
assert FakeCollection.registry["dbX_nodes"]["loaded"] is True
|
267
|
+
|
268
|
+
|
269
|
+
def test_get_collection_failure_raises(cfg, monkeypatch):
|
270
|
+
"""collection failure raises"""
|
271
|
+
mgr = MilvusConnectionManager(cfg)
|
272
|
+
|
273
|
+
class Boom(FakeCollection):
|
274
|
+
"""collection that fails to load"""
|
275
|
+
|
276
|
+
def load(self):
|
277
|
+
"""load fails"""
|
278
|
+
raise RuntimeError("load failed")
|
279
|
+
|
280
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
281
|
+
monkeypatch.setattr(mod, "Collection", Boom, raising=True)
|
282
|
+
|
283
|
+
with pytest.raises(MilvusException):
|
284
|
+
mgr.get_collection("dbX_nodes")
|
285
|
+
|
286
|
+
|
287
|
+
@pytest.mark.asyncio
|
288
|
+
async def test_async_search_success(cfg):
|
289
|
+
"""async search success"""
|
290
|
+
mgr = MilvusConnectionManager(cfg)
|
291
|
+
res = await mgr.async_search(
|
292
|
+
SearchParams(
|
293
|
+
collection_name="dbX_edges",
|
294
|
+
data=[[0.1, 0.2]],
|
295
|
+
anns_field="feat_emb",
|
296
|
+
search_params={"metric_type": "COSINE"},
|
297
|
+
limit=2,
|
298
|
+
output_fields=["id"],
|
299
|
+
)
|
300
|
+
)
|
301
|
+
assert isinstance(res, list)
|
302
|
+
assert len(res[0]) == 2
|
303
|
+
|
304
|
+
|
305
|
+
@pytest.mark.asyncio
|
306
|
+
async def test_async_search_falls_back_to_sync(cfg, monkeypatch):
|
307
|
+
"""search fallback to sync"""
|
308
|
+
mgr = MilvusConnectionManager(cfg)
|
309
|
+
|
310
|
+
# Make Async client creation fail (get_async_client returns None)
|
311
|
+
def bad_async_client(*_a, **_k):
|
312
|
+
"""aync client fails"""
|
313
|
+
return None
|
314
|
+
|
315
|
+
_mod = importlib.import_module(
|
316
|
+
"..utils.database.milvus_connection_manager", package=__package__
|
317
|
+
)
|
318
|
+
monkeypatch.setattr(mgr, "get_async_client", bad_async_client, raising=True)
|
319
|
+
|
320
|
+
res = await mgr.async_search(
|
321
|
+
SearchParams(
|
322
|
+
collection_name="dbX_edges",
|
323
|
+
data=[[0.1, 0.2]],
|
324
|
+
anns_field="feat_emb",
|
325
|
+
search_params={"metric_type": "COSINE"},
|
326
|
+
limit=3,
|
327
|
+
output_fields=["id"],
|
328
|
+
)
|
329
|
+
)
|
330
|
+
# Sync fallback should produce hits
|
331
|
+
assert len(res[0]) == 3
|
332
|
+
# Exercise Hit helper methods for coverage
|
333
|
+
first = res[0][0]
|
334
|
+
if hasattr(first, "get_id"):
|
335
|
+
assert first.get_id() == 0
|
336
|
+
if hasattr(first, "to_dict"):
|
337
|
+
assert isinstance(first.to_dict(), dict)
|
338
|
+
|
339
|
+
|
340
|
+
def test_sync_search_error_raises(cfg, monkeypatch):
|
341
|
+
"""sync search error raises"""
|
342
|
+
mgr = MilvusConnectionManager(cfg)
|
343
|
+
|
344
|
+
class Boom(FakeCollection):
|
345
|
+
"""version of Collection that fails to search"""
|
346
|
+
|
347
|
+
def load(self):
|
348
|
+
"""load no-op"""
|
349
|
+
return None
|
350
|
+
|
351
|
+
def search(self, *_a, **_k):
|
352
|
+
"""search fails"""
|
353
|
+
raise RuntimeError("sync search fail")
|
354
|
+
|
355
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
356
|
+
monkeypatch.setattr(mod, "Collection", Boom, raising=True)
|
357
|
+
|
358
|
+
with pytest.raises(MilvusException):
|
359
|
+
getattr(mgr, "_" + "sync_search")(
|
360
|
+
SearchParams(
|
361
|
+
collection_name="dbX_edges",
|
362
|
+
data=[[0.1]],
|
363
|
+
anns_field="feat_emb",
|
364
|
+
search_params={"metric_type": "COSINE"},
|
365
|
+
limit=1,
|
366
|
+
output_fields=["id"],
|
367
|
+
)
|
368
|
+
)
|
369
|
+
|
370
|
+
|
371
|
+
@pytest.mark.asyncio
|
372
|
+
async def test_async_query_success(cfg):
|
373
|
+
""" "search success"""
|
374
|
+
mgr = MilvusConnectionManager(cfg)
|
375
|
+
res = await mgr.async_query(
|
376
|
+
QueryParams(
|
377
|
+
collection_name="dbX_nodes",
|
378
|
+
expr="id > 0",
|
379
|
+
output_fields=["id"],
|
380
|
+
limit=1,
|
381
|
+
)
|
382
|
+
)
|
383
|
+
assert isinstance(res, list)
|
384
|
+
assert res[0]["ok"] is True
|
385
|
+
|
386
|
+
|
387
|
+
@pytest.mark.asyncio
|
388
|
+
async def test_async_query_falls_back_to_sync(cfg, monkeypatch):
|
389
|
+
"""search fallback to sync"""
|
390
|
+
mgr = MilvusConnectionManager(cfg)
|
391
|
+
|
392
|
+
def bad_async_client(*_a, **_k):
|
393
|
+
"""simulate async client creation failure"""
|
394
|
+
return None
|
395
|
+
|
396
|
+
monkeypatch.setattr(mgr, "get_async_client", bad_async_client, raising=True)
|
397
|
+
|
398
|
+
res = await mgr.async_query(
|
399
|
+
QueryParams(
|
400
|
+
collection_name="dbX_nodes",
|
401
|
+
expr="id > 0",
|
402
|
+
output_fields=["id"],
|
403
|
+
limit=1,
|
404
|
+
)
|
405
|
+
)
|
406
|
+
assert isinstance(res, list)
|
407
|
+
|
408
|
+
|
409
|
+
def test_sync_query_error_raises(cfg, monkeypatch):
|
410
|
+
"""sync query error raises"""
|
411
|
+
mgr = MilvusConnectionManager(cfg)
|
412
|
+
|
413
|
+
class Boom(FakeCollection):
|
414
|
+
""" "booming collection"""
|
415
|
+
|
416
|
+
def load(self):
|
417
|
+
"""load no-op"""
|
418
|
+
return None
|
419
|
+
|
420
|
+
def query(self, *_a, **_k):
|
421
|
+
"""query fails"""
|
422
|
+
raise RuntimeError("sync query fail")
|
423
|
+
|
424
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
425
|
+
monkeypatch.setattr(mod, "Collection", Boom, raising=True)
|
426
|
+
|
427
|
+
with pytest.raises(MilvusException):
|
428
|
+
getattr(mgr, "_" + "sync_query")(
|
429
|
+
QueryParams(
|
430
|
+
collection_name="dbX_nodes",
|
431
|
+
expr="x > 0",
|
432
|
+
output_fields=["id"],
|
433
|
+
limit=5,
|
434
|
+
)
|
435
|
+
)
|
436
|
+
|
437
|
+
|
438
|
+
@pytest.mark.asyncio
|
439
|
+
async def test_async_load_collection_ok(cfg):
|
440
|
+
"""async load collection ok"""
|
441
|
+
mgr = MilvusConnectionManager(cfg)
|
442
|
+
ok = await mgr.async_load_collection("dbX_nodes")
|
443
|
+
assert ok is True
|
444
|
+
# async loaded mark present
|
445
|
+
assert FakeCollection.registry["dbX_nodes"]["loaded_async"] is True
|
446
|
+
|
447
|
+
|
448
|
+
@pytest.mark.asyncio
|
449
|
+
async def test_async_load_collection_error_raises(cfg, monkeypatch):
|
450
|
+
"""load collection error raises"""
|
451
|
+
mgr = MilvusConnectionManager(cfg)
|
452
|
+
|
453
|
+
class BadAsync(FakeAsyncClient):
|
454
|
+
"""bad async client"""
|
455
|
+
|
456
|
+
async def load_collection(self, *_a, **_k):
|
457
|
+
"""load_collection fails"""
|
458
|
+
raise RuntimeError("boom")
|
459
|
+
|
460
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
461
|
+
monkeypatch.setattr(mod, "AsyncMilvusClient", BadAsync, raising=True)
|
462
|
+
# Force recreation of async client on this mgr
|
463
|
+
setattr(mgr, "_" + "async_client", None)
|
464
|
+
|
465
|
+
with pytest.raises(MilvusException):
|
466
|
+
await mgr.async_load_collection("dbX_nodes")
|
467
|
+
|
468
|
+
|
469
|
+
@pytest.mark.asyncio
|
470
|
+
async def test_async_get_collection_stats_ok(cfg):
|
471
|
+
"""async get collection stats ok"""
|
472
|
+
mgr = MilvusConnectionManager(cfg)
|
473
|
+
FakeCollection.registry["dbX_nodes"] = {"num_entities": 42}
|
474
|
+
stats = await mgr.async_get_collection_stats("dbX_nodes")
|
475
|
+
assert stats == {"num_entities": 42}
|
476
|
+
|
477
|
+
|
478
|
+
@pytest.mark.asyncio
|
479
|
+
async def test_async_get_collection_stats_error(cfg, monkeypatch):
|
480
|
+
""" "async get collection stats error"""
|
481
|
+
mgr = MilvusConnectionManager(cfg)
|
482
|
+
|
483
|
+
class BadCollection(FakeCollection):
|
484
|
+
"""bad collection"""
|
485
|
+
|
486
|
+
def __init__(self, name):
|
487
|
+
"""Init while gracefully handling base assignment to property."""
|
488
|
+
try:
|
489
|
+
# Base __init__ assigns to num_entities; our property has no setter.
|
490
|
+
super().__init__(name)
|
491
|
+
except AttributeError:
|
492
|
+
# Expected due to property; ensure minimal initialization
|
493
|
+
self.name = name
|
494
|
+
|
495
|
+
@property
|
496
|
+
def num_entities(self):
|
497
|
+
"""num_entities fails"""
|
498
|
+
raise RuntimeError("stats fail")
|
499
|
+
|
500
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
501
|
+
monkeypatch.setattr(mod, "Collection", BadCollection, raising=True)
|
502
|
+
|
503
|
+
# Directly trigger the property so the exact line is covered
|
504
|
+
with pytest.raises(RuntimeError):
|
505
|
+
_ = BadCollection("dbX_nodes").num_entities
|
506
|
+
|
507
|
+
# And verify the manager wraps it into MilvusException
|
508
|
+
with pytest.raises(MilvusException):
|
509
|
+
await mgr.async_get_collection_stats("dbX_nodes")
|
510
|
+
|
511
|
+
|
512
|
+
def test_disconnect_closes_both_clients(cfg):
|
513
|
+
""" "disconnect closes both clients"""
|
514
|
+
mgr = MilvusConnectionManager(cfg)
|
515
|
+
# create both clients
|
516
|
+
mgr.get_sync_client()
|
517
|
+
_ac = mgr.get_async_client()
|
518
|
+
mgr.ensure_connection()
|
519
|
+
ok = mgr.disconnect()
|
520
|
+
assert ok is True
|
521
|
+
# references cleared
|
522
|
+
assert getattr(mgr, "_" + "sync_client") is None
|
523
|
+
assert getattr(mgr, "_" + "async_client") is None
|
524
|
+
|
525
|
+
|
526
|
+
def test_from_config_and_get_instance_are_singleton(cfg):
|
527
|
+
""" "config and get_instance singleton"""
|
528
|
+
a = MilvusConnectionManager.from_config(cfg)
|
529
|
+
b = MilvusConnectionManager.get_instance(cfg)
|
530
|
+
assert a is b
|
531
|
+
|
532
|
+
|
533
|
+
def test_from_hydra_config_success(monkeypatch):
|
534
|
+
""" "hydra config success"""
|
535
|
+
|
536
|
+
# Fake hydra returning desired cfg shape
|
537
|
+
class HydraCtx:
|
538
|
+
"""hydra context manager stub."""
|
539
|
+
|
540
|
+
def __enter__(self):
|
541
|
+
"""Enter returns self."""
|
542
|
+
return self
|
543
|
+
|
544
|
+
def __exit__(self, *_a):
|
545
|
+
"""Exit returns False to propagate exceptions."""
|
546
|
+
return False
|
547
|
+
|
548
|
+
def status(self):
|
549
|
+
"""Additional public method."""
|
550
|
+
return "ok"
|
551
|
+
|
552
|
+
def initialize(**_k):
|
553
|
+
"""initialize"""
|
554
|
+
return HydraCtx()
|
555
|
+
|
556
|
+
def compose(*_a, **_k):
|
557
|
+
"""compose"""
|
558
|
+
return SimpleNamespace(
|
559
|
+
utils=SimpleNamespace(
|
560
|
+
database=SimpleNamespace(
|
561
|
+
milvus=SimpleNamespace(
|
562
|
+
milvus_db=SimpleNamespace(
|
563
|
+
host="127.0.0.1",
|
564
|
+
port=19530,
|
565
|
+
user="u",
|
566
|
+
password="p",
|
567
|
+
database_name="dbY",
|
568
|
+
alias="aliasY",
|
569
|
+
)
|
570
|
+
)
|
571
|
+
)
|
572
|
+
)
|
573
|
+
)
|
574
|
+
|
575
|
+
# Touch status() to cover that branch
|
576
|
+
assert HydraCtx().status() == "ok"
|
577
|
+
|
578
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
579
|
+
monkeypatch.setattr(
|
580
|
+
mod,
|
581
|
+
"hydra",
|
582
|
+
SimpleNamespace(initialize=initialize, compose=compose),
|
583
|
+
raising=True,
|
584
|
+
)
|
585
|
+
mgr = MilvusConnectionManager.from_hydra_config(overrides=["utils/database/milvus=default"])
|
586
|
+
assert isinstance(mgr, MilvusConnectionManager)
|
587
|
+
|
588
|
+
|
589
|
+
def test_from_hydra_config_failure_raises(monkeypatch):
|
590
|
+
""" "hydra config failure raises"""
|
591
|
+
|
592
|
+
class HydraCtx:
|
593
|
+
"""hydra context manager stub."""
|
594
|
+
|
595
|
+
def __enter__(self):
|
596
|
+
"""Enter returns self."""
|
597
|
+
return self
|
598
|
+
|
599
|
+
def __exit__(self, *_a):
|
600
|
+
"""Exit returns False to propagate exceptions."""
|
601
|
+
return False
|
602
|
+
|
603
|
+
def status(self):
|
604
|
+
"""Additional public method."""
|
605
|
+
return "ok"
|
606
|
+
|
607
|
+
def initialize(**_k):
|
608
|
+
"""initialize"""
|
609
|
+
return HydraCtx()
|
610
|
+
|
611
|
+
def compose(*_a, **_k):
|
612
|
+
"""compose fails"""
|
613
|
+
raise RuntimeError("compose fail")
|
614
|
+
|
615
|
+
# Touch status() to cover that branch
|
616
|
+
assert HydraCtx().status() == "ok"
|
617
|
+
|
618
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
619
|
+
monkeypatch.setattr(
|
620
|
+
mod,
|
621
|
+
"hydra",
|
622
|
+
SimpleNamespace(initialize=initialize, compose=compose),
|
623
|
+
raising=True,
|
624
|
+
)
|
625
|
+
with pytest.raises(MilvusException):
|
626
|
+
MilvusConnectionManager.from_hydra_config()
|
627
|
+
|
628
|
+
|
629
|
+
def test_get_async_client_init_exception_returns_none(cfg, monkeypatch):
|
630
|
+
""" "async client init exception returns None"""
|
631
|
+
|
632
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
633
|
+
|
634
|
+
class BadAsyncClient:
|
635
|
+
""" "quote-unquote bad async client"""
|
636
|
+
|
637
|
+
def __init__(self, *_a, **_k):
|
638
|
+
"""init fails"""
|
639
|
+
raise RuntimeError("cannot init async client")
|
640
|
+
|
641
|
+
def ping(self):
|
642
|
+
"""Dummy method."""
|
643
|
+
return False
|
644
|
+
|
645
|
+
def name(self):
|
646
|
+
"""Public helper."""
|
647
|
+
return "BadAsyncClient"
|
648
|
+
|
649
|
+
monkeypatch.setattr(mod, "AsyncMilvusClient", BadAsyncClient, raising=True)
|
650
|
+
|
651
|
+
mgr = MilvusConnectionManager(cfg)
|
652
|
+
# Cover class methods without instantiation
|
653
|
+
assert BadAsyncClient.ping(None) is False
|
654
|
+
assert BadAsyncClient.name(None) == "BadAsyncClient"
|
655
|
+
assert mgr.get_async_client() is None # hits the except → log → return None
|
656
|
+
|
657
|
+
|
658
|
+
def test_ensure_connection_milvus_exception_branch(cfg, monkeypatch):
|
659
|
+
"""ensure_connection MilvusException branch"""
|
660
|
+
|
661
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
662
|
+
mgr = MilvusConnectionManager(cfg)
|
663
|
+
|
664
|
+
# has_connection → False so it tries to connect
|
665
|
+
def has_conn(_alias):
|
666
|
+
"""connection exists"""
|
667
|
+
return False
|
668
|
+
|
669
|
+
def connect(*_a, **_k):
|
670
|
+
"""connect fails with MilvusException"""
|
671
|
+
raise MilvusException("boom") # specific MilvusException
|
672
|
+
|
673
|
+
monkeypatch.setattr(mod.connections, "has_connection", has_conn, raising=True)
|
674
|
+
monkeypatch.setattr(mod.connections, "connect", connect, raising=True)
|
675
|
+
|
676
|
+
with pytest.raises(MilvusException):
|
677
|
+
mgr.ensure_connection() # hits 'except MilvusException as e: raise'
|
678
|
+
|
679
|
+
|
680
|
+
def test_ensure_connection_generic_exception_wrapped(cfg, monkeypatch):
|
681
|
+
"""ensure_connection generic exception wrapped"""
|
682
|
+
|
683
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
684
|
+
mgr = MilvusConnectionManager(cfg)
|
685
|
+
|
686
|
+
def has_conn(_alias):
|
687
|
+
"""connection exists"""
|
688
|
+
return False
|
689
|
+
|
690
|
+
def connect(*_a, **_k):
|
691
|
+
""" "connect fails with generic exception"""
|
692
|
+
raise RuntimeError("generic failure") # generic exception
|
693
|
+
|
694
|
+
monkeypatch.setattr(mod.connections, "has_connection", has_conn, raising=True)
|
695
|
+
monkeypatch.setattr(mod.connections, "connect", connect, raising=True)
|
696
|
+
|
697
|
+
with pytest.raises(MilvusException):
|
698
|
+
mgr.ensure_connection() # hits 'except Exception as e: raise MilvusException(...)'
|
699
|
+
|
700
|
+
|
701
|
+
def test_get_connection_info_error_branch(cfg, monkeypatch):
|
702
|
+
""" "get_connection_info error branch"""
|
703
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
704
|
+
mgr = MilvusConnectionManager(cfg)
|
705
|
+
|
706
|
+
# Force an exception when fetching connection info
|
707
|
+
def has_conn(_alias):
|
708
|
+
"""connection exists"""
|
709
|
+
return True
|
710
|
+
|
711
|
+
def get_addr(_alias):
|
712
|
+
"""addr fails"""
|
713
|
+
raise RuntimeError("addr fail")
|
714
|
+
|
715
|
+
monkeypatch.setattr(mod.connections, "has_connection", has_conn, raising=True)
|
716
|
+
monkeypatch.setattr(mod.connections, "get_connection_addr", get_addr, raising=True)
|
717
|
+
|
718
|
+
info = mgr.get_connection_info()
|
719
|
+
assert info["connected"] is False
|
720
|
+
assert "error" in info
|
721
|
+
|
722
|
+
|
723
|
+
def test_test_connection_failure_returns_false(cfg, monkeypatch):
|
724
|
+
"""connection failure returns false"""
|
725
|
+
mgr = MilvusConnectionManager(cfg)
|
726
|
+
# Make ensure_connection blow up so test_connection catches and returns False
|
727
|
+
monkeypatch.setattr(
|
728
|
+
mgr,
|
729
|
+
"ensure_connection",
|
730
|
+
lambda: (_ for _ in ()).throw(RuntimeError("no conn")),
|
731
|
+
raising=True,
|
732
|
+
)
|
733
|
+
assert mgr.test_connection() is False
|
734
|
+
|
735
|
+
|
736
|
+
@pytest.mark.asyncio
|
737
|
+
async def test_disconnect_uses_create_task_when_loop_running(cfg):
|
738
|
+
"""disconnect uses create_task when loop running"""
|
739
|
+
mgr = MilvusConnectionManager(cfg)
|
740
|
+
# create async client so disconnect tries to close it
|
741
|
+
_acli = mgr.get_async_client()
|
742
|
+
# ensure a sync connection exists to also exercise that branch
|
743
|
+
mgr.ensure_connection()
|
744
|
+
|
745
|
+
# We are in an async test → running loop exists → should call loop.create_task(...)
|
746
|
+
ok = mgr.disconnect()
|
747
|
+
assert ok is True
|
748
|
+
assert getattr(mgr, "_" + "async_client") is None
|
749
|
+
assert getattr(mgr, "_" + "sync_client") is None
|
750
|
+
|
751
|
+
|
752
|
+
def test_disconnect_async_close_exception_sets_false(cfg, monkeypatch):
|
753
|
+
"""disconnect async close exception sets false"""
|
754
|
+
mgr = MilvusConnectionManager(cfg)
|
755
|
+
|
756
|
+
class BadAsyncClose:
|
757
|
+
"""bad async close"""
|
758
|
+
|
759
|
+
async def close(self):
|
760
|
+
"""delay and then raise"""
|
761
|
+
raise RuntimeError("close fail")
|
762
|
+
|
763
|
+
def name(self):
|
764
|
+
"""Public helper"""
|
765
|
+
return "BadAsyncClose"
|
766
|
+
|
767
|
+
# Inject a "bad" async client
|
768
|
+
bac = BadAsyncClose()
|
769
|
+
assert bac.name() == "BadAsyncClose" # cover helper
|
770
|
+
setattr(mgr, "_" + "async_client", bac)
|
771
|
+
|
772
|
+
# Force the no-running-loop branch so it uses asyncio.run(...) which will raise from close()
|
773
|
+
|
774
|
+
monkeypatch.setattr(
|
775
|
+
_asyncio,
|
776
|
+
"get_running_loop",
|
777
|
+
lambda: (_ for _ in ()).throw(RuntimeError("no loop")),
|
778
|
+
raising=True,
|
779
|
+
)
|
780
|
+
|
781
|
+
# Stub asyncio.run to directly call the coro and raise
|
782
|
+
def fake_run(coro):
|
783
|
+
"""fake run"""
|
784
|
+
# drive the coroutine to exception
|
785
|
+
loop = _asyncio.new_event_loop()
|
786
|
+
try:
|
787
|
+
return loop.run_until_complete(coro)
|
788
|
+
finally:
|
789
|
+
loop.close()
|
790
|
+
|
791
|
+
monkeypatch.setattr(_asyncio, "run", fake_run, raising=True)
|
792
|
+
|
793
|
+
# Also make sure no sync connection path crashes
|
794
|
+
ok = mgr.disconnect()
|
795
|
+
assert ok is False
|
796
|
+
assert getattr(mgr, "_" + "async_client") is None # cleared even on failure
|
797
|
+
|
798
|
+
|
799
|
+
def test_disconnect_outer_exception_returns_false(cfg, monkeypatch):
|
800
|
+
"""disconnect outer exception returns false"""
|
801
|
+
mgr = MilvusConnectionManager(cfg)
|
802
|
+
# Make connections.has_connection itself raise to jump to outer except
|
803
|
+
|
804
|
+
mod = importlib.import_module("..utils.database.milvus_connection_manager", package=__package__)
|
805
|
+
monkeypatch.setattr(
|
806
|
+
mod.connections,
|
807
|
+
"has_connection",
|
808
|
+
lambda alias: (_ for _ in ()).throw(RuntimeError("outer boom")),
|
809
|
+
raising=True,
|
810
|
+
)
|
811
|
+
|
812
|
+
assert mgr.disconnect() is False
|