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.
Files changed (34) hide show
  1. aiagents4pharma/talk2aiagents4pharma/configs/app/__init__.py +0 -0
  2. aiagents4pharma/talk2aiagents4pharma/configs/app/frontend/__init__.py +0 -0
  3. aiagents4pharma/talk2aiagents4pharma/configs/app/frontend/default.yaml +102 -0
  4. aiagents4pharma/talk2aiagents4pharma/configs/config.yaml +1 -0
  5. aiagents4pharma/talk2aiagents4pharma/tests/test_main_agent.py +144 -54
  6. aiagents4pharma/talk2biomodels/api/__init__.py +1 -1
  7. aiagents4pharma/talk2biomodels/configs/app/__init__.py +0 -0
  8. aiagents4pharma/talk2biomodels/configs/app/frontend/__init__.py +0 -0
  9. aiagents4pharma/talk2biomodels/configs/app/frontend/default.yaml +72 -0
  10. aiagents4pharma/talk2biomodels/configs/config.yaml +1 -0
  11. aiagents4pharma/talk2biomodels/tests/test_api.py +0 -30
  12. aiagents4pharma/talk2biomodels/tests/test_get_annotation.py +1 -1
  13. aiagents4pharma/talk2biomodels/tools/get_annotation.py +1 -10
  14. aiagents4pharma/talk2knowledgegraphs/configs/app/frontend/default.yaml +42 -26
  15. aiagents4pharma/talk2knowledgegraphs/configs/config.yaml +1 -0
  16. aiagents4pharma/talk2knowledgegraphs/configs/tools/multimodal_subgraph_extraction/default.yaml +4 -23
  17. aiagents4pharma/talk2knowledgegraphs/configs/utils/database/milvus/__init__.py +3 -0
  18. aiagents4pharma/talk2knowledgegraphs/configs/utils/database/milvus/default.yaml +61 -0
  19. aiagents4pharma/talk2knowledgegraphs/entrypoint.sh +1 -11
  20. aiagents4pharma/talk2knowledgegraphs/milvus_data_dump.py +11 -10
  21. aiagents4pharma/talk2knowledgegraphs/tests/test_agents_t2kg_agent.py +193 -73
  22. aiagents4pharma/talk2knowledgegraphs/tests/test_tools_milvus_multimodal_subgraph_extraction.py +1375 -667
  23. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_database_milvus_connection_manager.py +812 -0
  24. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_extractions_milvus_multimodal_pcst.py +723 -539
  25. aiagents4pharma/talk2knowledgegraphs/tools/milvus_multimodal_subgraph_extraction.py +474 -58
  26. aiagents4pharma/talk2knowledgegraphs/utils/database/__init__.py +5 -0
  27. aiagents4pharma/talk2knowledgegraphs/utils/database/milvus_connection_manager.py +586 -0
  28. aiagents4pharma/talk2knowledgegraphs/utils/extractions/milvus_multimodal_pcst.py +240 -8
  29. aiagents4pharma/talk2scholars/configs/app/frontend/default.yaml +67 -31
  30. {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/METADATA +10 -1
  31. {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/RECORD +33 -23
  32. aiagents4pharma/talk2biomodels/api/kegg.py +0 -87
  33. {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/WHEEL +0 -0
  34. {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