iris-vector-rag 0.2.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.
- adapters/rag_templates_bridge.py +578 -0
- common/__init__.py +1 -0
- common/batch_utils.py +294 -0
- common/config.py +58 -0
- common/connection_manager.py +242 -0
- common/connection_pool.py +445 -0
- common/database.py +141 -0
- common/database_schema_manager.py +289 -0
- common/db_init_with_indexes.py +181 -0
- common/db_vector_utils.py +181 -0
- common/environment_manager.py +220 -0
- common/huggingface_utils.py +206 -0
- common/iris_client.py +92 -0
- common/iris_connection_manager.py +411 -0
- common/iris_connection_pool.py +360 -0
- common/iris_connector.py +71 -0
- common/iris_dbapi_connector.py +323 -0
- common/iris_index_utils.py +195 -0
- common/iris_sql_utils.py +704 -0
- common/iris_stream_reader.py +166 -0
- common/llm_cache_config.py +308 -0
- common/llm_cache_iris.py +671 -0
- common/llm_cache_manager.py +911 -0
- common/utils.py +559 -0
- common/vector_sql_utils.py +584 -0
- evaluation_framework/__init__.py +66 -0
- evaluation_framework/biomedical_question_generator.py +826 -0
- evaluation_framework/comparative_analysis_system.py +1173 -0
- evaluation_framework/empirical_reporting.py +1298 -0
- evaluation_framework/evaluation_orchestrator.py +1123 -0
- evaluation_framework/execute_production_evaluation.py +382 -0
- evaluation_framework/pmc_data_pipeline.py +1332 -0
- evaluation_framework/production_evaluation_config.py +353 -0
- evaluation_framework/ragas_metrics_framework.py +1013 -0
- evaluation_framework/real_production_evaluation.py +544 -0
- evaluation_framework/run_10k_evaluation.py +175 -0
- evaluation_framework/simple_test.py +364 -0
- evaluation_framework/statistical_evaluation_methodology.py +1016 -0
- evaluation_framework/test_framework.py +281 -0
- evaluation_framework/test_iris_connectivity.py +244 -0
- evaluation_framework/test_small_scale_evaluation.py +432 -0
- evaluation_framework/true_e2e_evaluation.py +639 -0
- evaluation_framework/visualization_engine.py +585 -0
- iris_rag/__init__.py +284 -0
- iris_rag/config/default_config.yaml +295 -0
- iris_vector_rag-0.2.1.dist-info/METADATA +499 -0
- iris_vector_rag-0.2.1.dist-info/RECORD +50 -0
- iris_vector_rag-0.2.1.dist-info/WHEEL +5 -0
- iris_vector_rag-0.2.1.dist-info/entry_points.txt +2 -0
- iris_vector_rag-0.2.1.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Environment Manager - Smart Environment Detection and Activation
|
|
4
|
+
|
|
5
|
+
This module automatically detects and uses the best available Python environment:
|
|
6
|
+
1. UV environment (.venv) if available and working
|
|
7
|
+
2. Current active environment if it has required packages
|
|
8
|
+
3. System Python as fallback
|
|
9
|
+
|
|
10
|
+
Developers can override by setting FORCE_ENV environment variable or by
|
|
11
|
+
activating their preferred environment before running scripts.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EnvironmentManager:
|
|
25
|
+
"""Smart environment detection and management."""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.project_root = Path(__file__).parent.parent
|
|
29
|
+
self.uv_env_path = self.project_root / ".venv"
|
|
30
|
+
self.current_env = os.environ.get("VIRTUAL_ENV")
|
|
31
|
+
self.force_env = os.environ.get("FORCE_ENV") # Allow developer override
|
|
32
|
+
|
|
33
|
+
def get_best_python_executable(self) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Get the best Python executable to use.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Path to Python executable
|
|
39
|
+
"""
|
|
40
|
+
# Developer override
|
|
41
|
+
if self.force_env:
|
|
42
|
+
if self.force_env == "system":
|
|
43
|
+
return sys.executable
|
|
44
|
+
elif self.force_env == "uv" and self.uv_env_path.exists():
|
|
45
|
+
return str(self.uv_env_path / "bin" / "python")
|
|
46
|
+
else:
|
|
47
|
+
logger.warning(
|
|
48
|
+
f"FORCE_ENV={self.force_env} not valid, using auto-detection"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# If already in a virtual environment and it has required packages, use it
|
|
52
|
+
if self.current_env and self._check_environment_has_iris():
|
|
53
|
+
logger.debug(f"Using current virtual environment: {self.current_env}")
|
|
54
|
+
return sys.executable
|
|
55
|
+
|
|
56
|
+
# Check UV environment if available
|
|
57
|
+
if self.uv_env_path.exists():
|
|
58
|
+
uv_python = self.uv_env_path / "bin" / "python"
|
|
59
|
+
if uv_python.exists() and self._check_environment_has_iris(str(uv_python)):
|
|
60
|
+
logger.debug(f"Using UV environment: {self.uv_env_path}")
|
|
61
|
+
return str(uv_python)
|
|
62
|
+
|
|
63
|
+
# Fallback to current Python
|
|
64
|
+
logger.debug("Using current Python executable")
|
|
65
|
+
return sys.executable
|
|
66
|
+
|
|
67
|
+
def _check_environment_has_iris(self, python_exe: Optional[str] = None) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Check if an environment has the required IRIS packages.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
python_exe: Python executable to check (None for current)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if environment has required packages
|
|
76
|
+
"""
|
|
77
|
+
if python_exe is None:
|
|
78
|
+
python_exe = sys.executable
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
# Quick check for intersystems_irispython package
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
[
|
|
84
|
+
python_exe,
|
|
85
|
+
"-c",
|
|
86
|
+
"try: import iris; print(hasattr(iris, 'connect')); except ImportError: import iris; print(hasattr(iris, 'connect'))",
|
|
87
|
+
],
|
|
88
|
+
capture_output=True,
|
|
89
|
+
text=True,
|
|
90
|
+
timeout=5,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return result.returncode == 0 and "True" in result.stdout
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.debug(f"Environment check failed for {python_exe}: {e}")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def get_environment_info(self) -> Dict[str, Any]:
|
|
100
|
+
"""
|
|
101
|
+
Get information about the current environment setup.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dictionary with environment information
|
|
105
|
+
"""
|
|
106
|
+
best_python = self.get_best_python_executable()
|
|
107
|
+
|
|
108
|
+
info = {
|
|
109
|
+
"best_python": best_python,
|
|
110
|
+
"current_python": sys.executable,
|
|
111
|
+
"current_venv": self.current_env,
|
|
112
|
+
"uv_env_exists": self.uv_env_path.exists(),
|
|
113
|
+
"force_env": self.force_env,
|
|
114
|
+
"iris_available": self._check_environment_has_iris(),
|
|
115
|
+
"best_iris_available": self._check_environment_has_iris(best_python),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return info
|
|
119
|
+
|
|
120
|
+
def ensure_iris_available(self) -> bool:
|
|
121
|
+
"""
|
|
122
|
+
Ensure IRIS packages are available in the best environment.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
True if IRIS is available, False otherwise
|
|
126
|
+
"""
|
|
127
|
+
best_python = self.get_best_python_executable()
|
|
128
|
+
return self._check_environment_has_iris(best_python)
|
|
129
|
+
|
|
130
|
+
def run_in_best_environment(
|
|
131
|
+
self, script_path: str, args: list = None
|
|
132
|
+
) -> subprocess.CompletedProcess:
|
|
133
|
+
"""
|
|
134
|
+
Run a script in the best available environment.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
script_path: Path to Python script
|
|
138
|
+
args: Additional arguments
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
CompletedProcess result
|
|
142
|
+
"""
|
|
143
|
+
best_python = self.get_best_python_executable()
|
|
144
|
+
cmd = [best_python, script_path]
|
|
145
|
+
if args:
|
|
146
|
+
cmd.extend(args)
|
|
147
|
+
|
|
148
|
+
logger.debug(f"Running: {' '.join(cmd)}")
|
|
149
|
+
return subprocess.run(cmd, cwd=self.project_root)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_iris_connection_in_best_env():
|
|
153
|
+
"""
|
|
154
|
+
Get an IRIS connection using the best available environment.
|
|
155
|
+
This function automatically handles environment switching if needed.
|
|
156
|
+
"""
|
|
157
|
+
env_manager = EnvironmentManager()
|
|
158
|
+
|
|
159
|
+
# If we're already in the best environment, use direct import
|
|
160
|
+
if env_manager.get_best_python_executable() == sys.executable:
|
|
161
|
+
try:
|
|
162
|
+
import iris
|
|
163
|
+
|
|
164
|
+
if hasattr(iris, "connect"):
|
|
165
|
+
return iris.connect(
|
|
166
|
+
hostname=os.environ.get("IRIS_HOST", "localhost"),
|
|
167
|
+
port=int(os.environ.get("IRIS_PORT", "1974")),
|
|
168
|
+
namespace=os.environ.get("IRIS_NAMESPACE", "USER"),
|
|
169
|
+
username=os.environ.get("IRIS_USERNAME", "SuperUser"),
|
|
170
|
+
password=os.environ.get("IRIS_PASSWORD", "SYS"),
|
|
171
|
+
)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.warning(f"Direct IRIS import failed: {e}")
|
|
174
|
+
|
|
175
|
+
# Otherwise, we need to spawn a subprocess in the correct environment
|
|
176
|
+
logger.info("Switching to optimal environment for IRIS connection")
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
"IRIS connection requires environment switch. "
|
|
179
|
+
"Please use the connection manager or run in the correct environment."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def check_environment_status():
|
|
184
|
+
"""Check and report environment status."""
|
|
185
|
+
env_manager = EnvironmentManager()
|
|
186
|
+
info = env_manager.get_environment_info()
|
|
187
|
+
|
|
188
|
+
print("=== Environment Status ===")
|
|
189
|
+
print(f"Current Python: {info['current_python']}")
|
|
190
|
+
print(f"Best Python: {info['best_python']}")
|
|
191
|
+
print(f"Current VENV: {info['current_venv'] or 'None'}")
|
|
192
|
+
print(f"UV Environment: {'Available' if info['uv_env_exists'] else 'Not found'}")
|
|
193
|
+
print(f"Force Environment: {info['force_env'] or 'Auto-detect'}")
|
|
194
|
+
print(f"IRIS in Current: {'✅' if info['iris_available'] else '❌'}")
|
|
195
|
+
print(f"IRIS in Best: {'✅' if info['best_iris_available'] else '❌'}")
|
|
196
|
+
|
|
197
|
+
if not info["best_iris_available"]:
|
|
198
|
+
print("\n⚠️ Warning: IRIS packages not available in any environment")
|
|
199
|
+
print(" Consider running: uv sync # if using UV")
|
|
200
|
+
print(" Or: pip install intersystems-irispython")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
import argparse
|
|
205
|
+
|
|
206
|
+
parser = argparse.ArgumentParser(description="Environment Manager")
|
|
207
|
+
parser.add_argument("--check", action="store_true", help="Check environment status")
|
|
208
|
+
parser.add_argument("--run", help="Run script in best environment")
|
|
209
|
+
parser.add_argument("args", nargs="*", help="Arguments for script")
|
|
210
|
+
|
|
211
|
+
args = parser.parse_args()
|
|
212
|
+
|
|
213
|
+
if args.check:
|
|
214
|
+
check_environment_status()
|
|
215
|
+
elif args.run:
|
|
216
|
+
env_manager = EnvironmentManager()
|
|
217
|
+
result = env_manager.run_in_best_environment(args.run, args.args)
|
|
218
|
+
sys.exit(result.returncode)
|
|
219
|
+
else:
|
|
220
|
+
parser.print_help()
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HuggingFace model download utilities with rate limiting and retry logic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import random
|
|
7
|
+
import time
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from typing import Any, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def retry_with_exponential_backoff(
|
|
15
|
+
max_retries: int = 5,
|
|
16
|
+
base_delay: float = 1.0,
|
|
17
|
+
max_delay: float = 60.0,
|
|
18
|
+
exponential_base: float = 2.0,
|
|
19
|
+
jitter: bool = True,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Decorator for retrying functions with exponential backoff.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
max_retries: Maximum number of retry attempts
|
|
26
|
+
base_delay: Initial delay in seconds
|
|
27
|
+
max_delay: Maximum delay in seconds
|
|
28
|
+
exponential_base: Base for exponential backoff
|
|
29
|
+
jitter: Whether to add random jitter to delays
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def decorator(func):
|
|
33
|
+
@wraps(func)
|
|
34
|
+
def wrapper(*args, **kwargs):
|
|
35
|
+
last_exception = None
|
|
36
|
+
|
|
37
|
+
for attempt in range(max_retries + 1):
|
|
38
|
+
try:
|
|
39
|
+
return func(*args, **kwargs)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
last_exception = e
|
|
42
|
+
|
|
43
|
+
# Check if this is a rate limiting error
|
|
44
|
+
error_str = str(e).lower()
|
|
45
|
+
is_rate_limit = any(
|
|
46
|
+
indicator in error_str
|
|
47
|
+
for indicator in [
|
|
48
|
+
"rate limit",
|
|
49
|
+
"too many requests",
|
|
50
|
+
"429",
|
|
51
|
+
"quota exceeded",
|
|
52
|
+
"service unavailable",
|
|
53
|
+
"503",
|
|
54
|
+
"timeout",
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if attempt == max_retries:
|
|
59
|
+
logger.error(f"Failed after {max_retries} retries: {e}")
|
|
60
|
+
raise last_exception
|
|
61
|
+
|
|
62
|
+
if is_rate_limit:
|
|
63
|
+
# Calculate delay with exponential backoff
|
|
64
|
+
delay = min(base_delay * (exponential_base**attempt), max_delay)
|
|
65
|
+
|
|
66
|
+
# Add jitter to prevent thundering herd
|
|
67
|
+
if jitter:
|
|
68
|
+
delay *= 0.5 + random.random() * 0.5
|
|
69
|
+
|
|
70
|
+
logger.warning(
|
|
71
|
+
f"Rate limit detected (attempt {attempt + 1}/{max_retries + 1}). "
|
|
72
|
+
f"Retrying in {delay:.2f} seconds: {e}"
|
|
73
|
+
)
|
|
74
|
+
time.sleep(delay)
|
|
75
|
+
else:
|
|
76
|
+
# For non-rate-limit errors, fail immediately
|
|
77
|
+
logger.error(f"Non-rate-limit error encountered: {e}")
|
|
78
|
+
raise e
|
|
79
|
+
|
|
80
|
+
raise last_exception
|
|
81
|
+
|
|
82
|
+
return wrapper
|
|
83
|
+
|
|
84
|
+
return decorator
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@retry_with_exponential_backoff(max_retries=5, base_delay=2.0, max_delay=120.0)
|
|
88
|
+
def download_huggingface_model(
|
|
89
|
+
model_name: str, trust_remote_code: bool = False, **kwargs
|
|
90
|
+
) -> Tuple[Any, Any]:
|
|
91
|
+
"""
|
|
92
|
+
Download HuggingFace model and tokenizer with retry logic.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
model_name: Name of the model to download
|
|
96
|
+
trust_remote_code: Whether to trust remote code
|
|
97
|
+
**kwargs: Additional arguments for model loading
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Tuple of (tokenizer, model)
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
from transformers import AutoModel, AutoTokenizer
|
|
104
|
+
|
|
105
|
+
logger.info(f"Downloading HuggingFace model: {model_name}")
|
|
106
|
+
|
|
107
|
+
# Download tokenizer first
|
|
108
|
+
logger.debug(f"Loading tokenizer for {model_name}")
|
|
109
|
+
tokenizer = AutoTokenizer.from_pretrained(
|
|
110
|
+
model_name, trust_remote_code=trust_remote_code, **kwargs
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Download model
|
|
114
|
+
logger.debug(f"Loading model for {model_name}")
|
|
115
|
+
model = AutoModel.from_pretrained(
|
|
116
|
+
model_name, trust_remote_code=trust_remote_code, **kwargs
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
logger.info(f"Successfully downloaded HuggingFace model: {model_name}")
|
|
120
|
+
return tokenizer, model
|
|
121
|
+
|
|
122
|
+
except ImportError as e:
|
|
123
|
+
logger.error(f"transformers library not available: {e}")
|
|
124
|
+
raise
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error(f"Failed to download model {model_name}: {e}")
|
|
127
|
+
raise
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_cached_model(
|
|
131
|
+
model_name: str, cache_dict: dict, trust_remote_code: bool = False, **kwargs
|
|
132
|
+
) -> Tuple[Any, Any]:
|
|
133
|
+
"""
|
|
134
|
+
Get model from cache or download if not cached.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
model_name: Name of the model
|
|
138
|
+
cache_dict: Dictionary to use for caching
|
|
139
|
+
trust_remote_code: Whether to trust remote code
|
|
140
|
+
**kwargs: Additional arguments for model loading
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Tuple of (tokenizer, model)
|
|
144
|
+
"""
|
|
145
|
+
if model_name not in cache_dict:
|
|
146
|
+
logger.info(f"Model {model_name} not in cache, downloading...")
|
|
147
|
+
tokenizer, model = download_huggingface_model(
|
|
148
|
+
model_name, trust_remote_code=trust_remote_code, **kwargs
|
|
149
|
+
)
|
|
150
|
+
cache_dict[model_name] = (tokenizer, model)
|
|
151
|
+
logger.info(f"Cached model {model_name}")
|
|
152
|
+
else:
|
|
153
|
+
logger.info(f"Using cached model {model_name}")
|
|
154
|
+
tokenizer, model = cache_dict[model_name]
|
|
155
|
+
|
|
156
|
+
return tokenizer, model
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def clear_model_cache(cache_dict: dict, model_name: Optional[str] = None):
|
|
160
|
+
"""
|
|
161
|
+
Clear model cache.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
cache_dict: Dictionary containing cached models
|
|
165
|
+
model_name: Specific model to clear, or None to clear all
|
|
166
|
+
"""
|
|
167
|
+
if model_name:
|
|
168
|
+
if model_name in cache_dict:
|
|
169
|
+
del cache_dict[model_name]
|
|
170
|
+
logger.info(f"Cleared cache for model {model_name}")
|
|
171
|
+
else:
|
|
172
|
+
cache_dict.clear()
|
|
173
|
+
logger.info("Cleared all model cache")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Global cache for models
|
|
177
|
+
_global_model_cache = {}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_global_cached_model(
|
|
181
|
+
model_name: str, trust_remote_code: bool = False, **kwargs
|
|
182
|
+
) -> Tuple[Any, Any]:
|
|
183
|
+
"""
|
|
184
|
+
Get model from global cache or download if not cached.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
model_name: Name of the model
|
|
188
|
+
trust_remote_code: Whether to trust remote code
|
|
189
|
+
**kwargs: Additional arguments for model loading
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Tuple of (tokenizer, model)
|
|
193
|
+
"""
|
|
194
|
+
return get_cached_model(
|
|
195
|
+
model_name, _global_model_cache, trust_remote_code, **kwargs
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def clear_global_cache(model_name: Optional[str] = None):
|
|
200
|
+
"""
|
|
201
|
+
Clear global model cache.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
model_name: Specific model to clear, or None to clear all
|
|
205
|
+
"""
|
|
206
|
+
clear_model_cache(_global_model_cache, model_name)
|
common/iris_client.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple IRIS client for database operations
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from .iris_connection_manager import IRISConnectionManager
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IRISClient:
|
|
14
|
+
"""Simple IRIS database client"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
17
|
+
"""Initialize IRIS client with configuration"""
|
|
18
|
+
self.config = config or {}
|
|
19
|
+
self.connection_manager = IRISConnectionManager()
|
|
20
|
+
self._connection = None
|
|
21
|
+
|
|
22
|
+
def connect(self) -> bool:
|
|
23
|
+
"""Establish database connection"""
|
|
24
|
+
try:
|
|
25
|
+
self._connection = self.connection_manager.get_connection(self.config)
|
|
26
|
+
return True
|
|
27
|
+
except Exception as e:
|
|
28
|
+
logger.error(f"Failed to connect to IRIS: {e}")
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def query(self, sql: str, params: Optional[tuple] = None) -> List[Dict[str, Any]]:
|
|
32
|
+
"""Execute query and return results"""
|
|
33
|
+
if not self._connection:
|
|
34
|
+
if not self.connect():
|
|
35
|
+
raise ConnectionError("No database connection available")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
cursor = self._connection.cursor()
|
|
39
|
+
cursor.execute(sql, params or ())
|
|
40
|
+
|
|
41
|
+
# Get column names
|
|
42
|
+
columns = (
|
|
43
|
+
[desc[0] for desc in cursor.description] if cursor.description else []
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Fetch results
|
|
47
|
+
rows = cursor.fetchall()
|
|
48
|
+
|
|
49
|
+
# Convert to list of dictionaries
|
|
50
|
+
results = []
|
|
51
|
+
for row in rows:
|
|
52
|
+
results.append(dict(zip(columns, row)))
|
|
53
|
+
|
|
54
|
+
return results
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"Query failed: {e}")
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
def execute(self, sql: str, params: Optional[tuple] = None) -> int:
|
|
61
|
+
"""Execute non-query SQL statement"""
|
|
62
|
+
if not self._connection:
|
|
63
|
+
if not self.connect():
|
|
64
|
+
raise ConnectionError("No database connection available")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
cursor = self._connection.cursor()
|
|
68
|
+
cursor.execute(sql, params or ())
|
|
69
|
+
self._connection.commit()
|
|
70
|
+
return cursor.rowcount
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Execute failed: {e}")
|
|
73
|
+
self._connection.rollback()
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
def close(self):
|
|
77
|
+
"""Close database connection"""
|
|
78
|
+
if self._connection:
|
|
79
|
+
try:
|
|
80
|
+
self._connection.close()
|
|
81
|
+
except:
|
|
82
|
+
pass
|
|
83
|
+
self._connection = None
|
|
84
|
+
|
|
85
|
+
def __enter__(self):
|
|
86
|
+
"""Context manager entry"""
|
|
87
|
+
self.connect()
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
91
|
+
"""Context manager exit"""
|
|
92
|
+
self.close()
|