Coverage for src/qdrant_loader_mcp_server/search/components/combining/flatten.py: 60%
75 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-08 06:06 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-08 06:06 +0000
1from __future__ import annotations
3from collections.abc import Mapping
4from dataclasses import asdict, is_dataclass
5from typing import Any
8def flatten_metadata_components(metadata_components: dict[str, Any]) -> dict[str, Any]:
9 flattened: dict[str, Any] = {}
10 # Denylist of sensitive key patterns to redact
11 sensitive_key_patterns = {"password", "secret", "token", "api_key", "apikey"}
13 def _should_include(key: Any, value: Any) -> bool:
14 if not isinstance(key, str):
15 return False
16 if key.startswith("_"):
17 return False
18 try:
19 if callable(value):
20 return False
21 except Exception:
22 # Some objects may raise when inspected for callability
23 return False
24 return True
26 def _maybe_redact(key: str, value: Any) -> Any:
27 lower_key = key.lower()
28 for pattern in sensitive_key_patterns:
29 if pattern in lower_key:
30 return "[REDACTED]"
31 return value
33 for _component_name, component in metadata_components.items():
34 if component is None:
35 continue
36 # Dataclasses (supports slots via asdict)
37 if is_dataclass(component):
38 for key, value in asdict(component).items():
39 if _should_include(key, value):
40 flattened[key] = _maybe_redact(key, value)
41 continue
43 # Generic Mapping (handles dict and subclasses like MappingProxyType)
44 try:
45 is_mapping = isinstance(component, Mapping)
46 except Exception:
47 # Some mocked objects with custom __dict__ can raise during isinstance checks
48 is_mapping = False
49 if is_mapping:
50 try:
51 items_iter = component.items() # type: ignore[assignment]
52 except Exception:
53 items_iter = []
54 for key, value in items_iter:
55 if _should_include(key, value):
56 flattened[key] = _maybe_redact(key, value)
57 continue
59 # Fallback: inspect __dict__ but filter out private keys and callables
60 if hasattr(component, "__dict__") and isinstance(component.__dict__, dict):
61 for key, value in component.__dict__.items():
62 if _should_include(key, value):
63 flattened[key] = _maybe_redact(key, value)
65 # Support for objects using __slots__ without a traditional __dict__
66 try:
67 has_slots = hasattr(component, "__slots__")
68 except Exception:
69 has_slots = False
70 if has_slots:
71 try:
72 slots = component.__slots__
73 except Exception:
74 slots = []
75 # __slots__ can be a string or an iterable of strings
76 slot_names: list[str] = []
77 if isinstance(slots, str):
78 slot_names = [slots]
79 else:
80 try:
81 slot_names = [s for s in slots if isinstance(s, str)] # type: ignore[assignment]
82 except Exception:
83 slot_names = []
84 for key in slot_names:
85 if not isinstance(key, str) or key.startswith("_"):
86 continue
87 try:
88 value = getattr(component, key)
89 except Exception:
90 continue
91 if _should_include(key, value):
92 flattened[key] = _maybe_redact(key, value)
93 return flattened