pyplaykit 1.7.0__tar.gz → 1.8.0__tar.gz
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.
- {pyplaykit-1.7.0/pyplaykit.egg-info → pyplaykit-1.8.0}/PKG-INFO +1 -1
- {pyplaykit-1.7.0 → pyplaykit-1.8.0/pyplaykit.egg-info}/PKG-INFO +1 -1
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit.egg-info/SOURCES.txt +2 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyproject.toml +1 -1
- pyplaykit-1.8.0/utils/database_clients.py +424 -0
- pyplaykit-1.8.0/utils/db_drivers/informix.py +185 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_factory.py +15 -1
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/MANIFEST.in +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/README.md +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/components/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/components/base_component.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/components/data_table.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/components/form_field.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/components/modal.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/config/config.yaml +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/config/environments.yaml +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/config/projects/customs_modernization.yaml +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/config/projects/dispatcho.yaml +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/config/projects/timelyquote.yaml +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/core/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/core/base_page.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/core/base_test.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/core/browser_factory.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/core/playwright_manager.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/dist_pkg/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/dist_pkg/distribution.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/integrations/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/integrations/adapters.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/orchestration/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/orchestration/enforcer.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/orchestration/planner.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/dashboard_page.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/login_page.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/orangehrm_login_page.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/saucedemo_inventory_page.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/saucedemo_login_page.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/api_plugin.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/base.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/data_plugin.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/registry.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/security_plugin.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit/cli.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit/plugin.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit.egg-info/dependency_links.txt +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit.egg-info/entry_points.txt +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit.egg-info/requires.txt +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit.egg-info/top_level.txt +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/resilience/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/resilience/locator_recovery.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/setup.cfg +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/api/ecom/auth_payloads.json +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/api/ecom/product_payloads.json +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/api_test_data.json +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/data_validation_test_data.json +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/login_test_data.json +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/orangehrm_login_data.json +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/ui_test_data.json +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/api_client.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/api_test_helper.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/assertion_helper.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/auth_manager.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/data_comparison_report.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/data_comparison_utils.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/data_loader.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/data_source.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/data_validator.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_client.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/__init__.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/db2.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/mysql.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/oracle.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/snowflake.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/sqlite.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/sqlserver.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_fixtures.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_pool.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_validation_helper.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/environment_validator.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/file_reader.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/graphql_client.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/logger.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/observability.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/response_validator.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/screenshot_helper.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/soap_client.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/tdm.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/transformation_validator.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/ui_db_validator.py +0 -0
- {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/yaml_config_reader.py +0 -0
|
@@ -63,6 +63,7 @@ utils/data_comparison_utils.py
|
|
|
63
63
|
utils/data_loader.py
|
|
64
64
|
utils/data_source.py
|
|
65
65
|
utils/data_validator.py
|
|
66
|
+
utils/database_clients.py
|
|
66
67
|
utils/db_client.py
|
|
67
68
|
utils/db_factory.py
|
|
68
69
|
utils/db_fixtures.py
|
|
@@ -82,6 +83,7 @@ utils/ui_db_validator.py
|
|
|
82
83
|
utils/yaml_config_reader.py
|
|
83
84
|
utils/db_drivers/__init__.py
|
|
84
85
|
utils/db_drivers/db2.py
|
|
86
|
+
utils/db_drivers/informix.py
|
|
85
87
|
utils/db_drivers/mysql.py
|
|
86
88
|
utils/db_drivers/oracle.py
|
|
87
89
|
utils/db_drivers/snowflake.py
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom database client wrappers for cross-database comparisons.
|
|
3
|
+
|
|
4
|
+
PyPlayKit's built-in db_client fixture supports PostgreSQL by default.
|
|
5
|
+
Use these wrappers to compare other enterprise database types:
|
|
6
|
+
- SQL Server
|
|
7
|
+
- Oracle
|
|
8
|
+
- DB2
|
|
9
|
+
- Informix
|
|
10
|
+
|
|
11
|
+
DEPRECATED: This module is superseded by the unified driver architecture in
|
|
12
|
+
utils/db_drivers/ and utils/db_factory.py. Use DbClientFactory.create() instead.
|
|
13
|
+
|
|
14
|
+
For new code, prefer:
|
|
15
|
+
from utils.db_factory import DbClientFactory
|
|
16
|
+
client = DbClientFactory.create("sqlserver", host="...", ...)
|
|
17
|
+
|
|
18
|
+
Legacy Installation:
|
|
19
|
+
pip install cx_Oracle # Oracle
|
|
20
|
+
pip install pyodbc # SQL Server
|
|
21
|
+
pip install ibm_db # DB2
|
|
22
|
+
pip install IfxPy # Informix
|
|
23
|
+
|
|
24
|
+
Legacy Usage:
|
|
25
|
+
from utils.database_clients import OracleClient, SQLServerClient, DB2Client, InformixClient
|
|
26
|
+
|
|
27
|
+
oracle = OracleClient(user="...", password="...", dsn="...")
|
|
28
|
+
results = oracle.fetch_all("SELECT * FROM customers")
|
|
29
|
+
oracle.close()
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DatabaseClient:
|
|
36
|
+
"""Base class for all database clients."""
|
|
37
|
+
|
|
38
|
+
def fetch_all(self, query: str, params: tuple = ()) -> list[dict[str, Any]]:
|
|
39
|
+
"""
|
|
40
|
+
Execute query and return results as list of dictionaries.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
query: SQL query to execute
|
|
44
|
+
params: Query parameters (default: ())
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of dictionaries where each dict represents one row
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
results = client.fetch_all("SELECT id, name FROM users WHERE id = %s", (123,))
|
|
51
|
+
# Returns: [{"id": 123, "name": "Alice"}]
|
|
52
|
+
"""
|
|
53
|
+
raise NotImplementedError("Subclasses must implement fetch_all()")
|
|
54
|
+
|
|
55
|
+
def close(self):
|
|
56
|
+
"""Close database connection."""
|
|
57
|
+
raise NotImplementedError("Subclasses must implement close()")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class OracleClient(DatabaseClient):
|
|
61
|
+
"""
|
|
62
|
+
Oracle database client wrapper.
|
|
63
|
+
|
|
64
|
+
Installation:
|
|
65
|
+
pip install cx_Oracle
|
|
66
|
+
# Also install Oracle Instant Client
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
oracle = OracleClient(
|
|
70
|
+
user="oracle_user",
|
|
71
|
+
password="oracle_password",
|
|
72
|
+
dsn="hostname:1521/service_name"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
results = oracle.fetch_all("SELECT * FROM CUSTOMERS ORDER BY ID")
|
|
76
|
+
oracle.close()
|
|
77
|
+
|
|
78
|
+
Column Names:
|
|
79
|
+
Automatically converts Oracle's uppercase column names to lowercase
|
|
80
|
+
for consistency with PostgreSQL/MySQL.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, user: str, password: str, dsn: str):
|
|
84
|
+
"""
|
|
85
|
+
Initialize Oracle connection.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
user: Oracle username
|
|
89
|
+
password: Oracle password
|
|
90
|
+
dsn: Data Source Name in format "host:port/service_name"
|
|
91
|
+
Example: "oracle.example.com:1521/PROD"
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
import cx_Oracle
|
|
95
|
+
except ImportError:
|
|
96
|
+
raise ImportError(
|
|
97
|
+
"cx_Oracle is required for Oracle connections. "
|
|
98
|
+
"Install with: pip install cx_Oracle"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self.connection = cx_Oracle.connect(user=user, password=password, dsn=dsn)
|
|
102
|
+
|
|
103
|
+
def fetch_all(self, query: str, params: tuple = ()) -> list[dict[str, Any]]:
|
|
104
|
+
"""Execute query and return list of dictionaries."""
|
|
105
|
+
cursor = self.connection.cursor()
|
|
106
|
+
cursor.execute(query, params)
|
|
107
|
+
|
|
108
|
+
# Get column names (convert to lowercase for consistency)
|
|
109
|
+
columns = [desc[0].lower() for desc in cursor.description]
|
|
110
|
+
|
|
111
|
+
# Fetch all rows
|
|
112
|
+
rows = cursor.fetchall()
|
|
113
|
+
|
|
114
|
+
# Convert to list of dictionaries
|
|
115
|
+
result = [dict(zip(columns, row)) for row in rows]
|
|
116
|
+
|
|
117
|
+
cursor.close()
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
def close(self):
|
|
121
|
+
"""Close Oracle connection."""
|
|
122
|
+
if self.connection:
|
|
123
|
+
self.connection.close()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SQLServerClient(DatabaseClient):
|
|
127
|
+
"""
|
|
128
|
+
SQL Server database client wrapper.
|
|
129
|
+
|
|
130
|
+
Installation:
|
|
131
|
+
pip install pyodbc
|
|
132
|
+
# Also install: ODBC Driver 17 for SQL Server
|
|
133
|
+
|
|
134
|
+
Windows:
|
|
135
|
+
Download ODBC Driver from Microsoft
|
|
136
|
+
|
|
137
|
+
Linux:
|
|
138
|
+
sudo apt-get install msodbcsql17
|
|
139
|
+
|
|
140
|
+
macOS:
|
|
141
|
+
brew tap microsoft/mssql-release
|
|
142
|
+
brew install msodbcsql17
|
|
143
|
+
|
|
144
|
+
Usage:
|
|
145
|
+
sqlserver = SQLServerClient(
|
|
146
|
+
server="sqlserver.example.com",
|
|
147
|
+
database="production",
|
|
148
|
+
username="sa",
|
|
149
|
+
password="password"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
results = sqlserver.fetch_all("SELECT * FROM dbo.Customers")
|
|
153
|
+
sqlserver.close()
|
|
154
|
+
|
|
155
|
+
Column Names:
|
|
156
|
+
Automatically converts column names to lowercase for consistency.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
server: str,
|
|
162
|
+
database: str,
|
|
163
|
+
username: str,
|
|
164
|
+
password: str,
|
|
165
|
+
driver: str = "ODBC Driver 17 for SQL Server",
|
|
166
|
+
port: int = 1433
|
|
167
|
+
):
|
|
168
|
+
"""
|
|
169
|
+
Initialize SQL Server connection.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
server: SQL Server hostname
|
|
173
|
+
database: Database name
|
|
174
|
+
username: SQL Server username
|
|
175
|
+
password: SQL Server password
|
|
176
|
+
driver: ODBC driver name (default: ODBC Driver 17 for SQL Server)
|
|
177
|
+
port: SQL Server port (default: 1433)
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
import pyodbc
|
|
181
|
+
except ImportError:
|
|
182
|
+
raise ImportError(
|
|
183
|
+
"pyodbc is required for SQL Server connections. "
|
|
184
|
+
"Install with: pip install pyodbc"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
conn_string = (
|
|
188
|
+
f"DRIVER={{{driver}}};"
|
|
189
|
+
f"SERVER={server},{port};"
|
|
190
|
+
f"DATABASE={database};"
|
|
191
|
+
f"UID={username};"
|
|
192
|
+
f"PWD={password}"
|
|
193
|
+
)
|
|
194
|
+
self.connection = pyodbc.connect(conn_string)
|
|
195
|
+
|
|
196
|
+
def fetch_all(self, query: str, params: tuple = ()) -> list[dict[str, Any]]:
|
|
197
|
+
"""Execute query and return list of dictionaries."""
|
|
198
|
+
cursor = self.connection.cursor()
|
|
199
|
+
cursor.execute(query, params)
|
|
200
|
+
|
|
201
|
+
# Get column names (convert to lowercase)
|
|
202
|
+
columns = [column[0].lower() for column in cursor.description]
|
|
203
|
+
|
|
204
|
+
# Fetch all rows
|
|
205
|
+
rows = cursor.fetchall()
|
|
206
|
+
|
|
207
|
+
# Convert to list of dictionaries
|
|
208
|
+
result = [dict(zip(columns, row)) for row in rows]
|
|
209
|
+
|
|
210
|
+
cursor.close()
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
def close(self):
|
|
214
|
+
"""Close SQL Server connection."""
|
|
215
|
+
if self.connection:
|
|
216
|
+
self.connection.close()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class DB2Client(DatabaseClient):
|
|
220
|
+
"""
|
|
221
|
+
IBM DB2 database client wrapper.
|
|
222
|
+
|
|
223
|
+
Installation:
|
|
224
|
+
pip install ibm_db ibm_db_dbi
|
|
225
|
+
|
|
226
|
+
Usage:
|
|
227
|
+
db2 = DB2Client(
|
|
228
|
+
host="db2.example.com",
|
|
229
|
+
port=50000,
|
|
230
|
+
database="PRODDB",
|
|
231
|
+
user="db2user",
|
|
232
|
+
password="password"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
results = db2.fetch_all("SELECT * FROM CUSTOMERS ORDER BY ID")
|
|
236
|
+
db2.close()
|
|
237
|
+
|
|
238
|
+
Column Names:
|
|
239
|
+
Automatically converts DB2's uppercase column names to lowercase
|
|
240
|
+
for consistency with PostgreSQL.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def __init__(
|
|
244
|
+
self,
|
|
245
|
+
host: str,
|
|
246
|
+
port: int,
|
|
247
|
+
database: str,
|
|
248
|
+
user: str,
|
|
249
|
+
password: str,
|
|
250
|
+
connect_timeout: int = 10
|
|
251
|
+
):
|
|
252
|
+
"""
|
|
253
|
+
Initialize DB2 connection.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
host: DB2 hostname
|
|
257
|
+
port: DB2 port (default: 50000)
|
|
258
|
+
database: Database name
|
|
259
|
+
user: DB2 username
|
|
260
|
+
password: DB2 password
|
|
261
|
+
connect_timeout: Connection timeout in seconds
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
import ibm_db
|
|
265
|
+
import ibm_db_dbi
|
|
266
|
+
except ImportError:
|
|
267
|
+
raise ImportError(
|
|
268
|
+
"ibm_db and ibm_db_dbi are required for DB2 connections. "
|
|
269
|
+
"Install with: pip install ibm_db ibm_db_dbi"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
conn_str = (
|
|
273
|
+
f"DATABASE={database};"
|
|
274
|
+
f"HOSTNAME={host};"
|
|
275
|
+
f"PORT={port};"
|
|
276
|
+
f"PROTOCOL=TCPIP;"
|
|
277
|
+
f"UID={user};"
|
|
278
|
+
f"PWD={password};"
|
|
279
|
+
f"ConnectTimeout={connect_timeout};"
|
|
280
|
+
)
|
|
281
|
+
raw_conn = ibm_db.connect(conn_str, "", "")
|
|
282
|
+
self.connection = ibm_db_dbi.Connection(raw_conn)
|
|
283
|
+
|
|
284
|
+
def fetch_all(self, query: str, params: tuple = ()) -> list[dict[str, Any]]:
|
|
285
|
+
"""Execute query and return list of dictionaries."""
|
|
286
|
+
cursor = self.connection.cursor()
|
|
287
|
+
cursor.execute(query, params)
|
|
288
|
+
|
|
289
|
+
# Get column names (convert to lowercase for consistency)
|
|
290
|
+
columns = [desc[0].lower() for desc in cursor.description]
|
|
291
|
+
|
|
292
|
+
# Fetch all rows
|
|
293
|
+
rows = cursor.fetchall()
|
|
294
|
+
|
|
295
|
+
# Convert to list of dictionaries
|
|
296
|
+
result = [dict(zip(columns, row)) for row in rows]
|
|
297
|
+
|
|
298
|
+
cursor.close()
|
|
299
|
+
return result
|
|
300
|
+
|
|
301
|
+
def close(self):
|
|
302
|
+
"""Close DB2 connection."""
|
|
303
|
+
if self.connection:
|
|
304
|
+
self.connection.close()
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class InformixClient(DatabaseClient):
|
|
308
|
+
"""
|
|
309
|
+
IBM Informix database client wrapper.
|
|
310
|
+
|
|
311
|
+
Installation:
|
|
312
|
+
pip install IfxPy IfxPyDbi
|
|
313
|
+
|
|
314
|
+
Usage:
|
|
315
|
+
informix = InformixClient(
|
|
316
|
+
host="informix.example.com",
|
|
317
|
+
port=9088,
|
|
318
|
+
database="stores",
|
|
319
|
+
server="informix_tcp",
|
|
320
|
+
user="informix",
|
|
321
|
+
password="password"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
results = informix.fetch_all("SELECT * FROM customer ORDER BY customer_num")
|
|
325
|
+
informix.close()
|
|
326
|
+
|
|
327
|
+
Column Names:
|
|
328
|
+
Automatically converts column names to lowercase for consistency.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def __init__(
|
|
332
|
+
self,
|
|
333
|
+
host: str,
|
|
334
|
+
port: int,
|
|
335
|
+
database: str,
|
|
336
|
+
server: str,
|
|
337
|
+
user: str,
|
|
338
|
+
password: str,
|
|
339
|
+
connect_timeout: int = 10
|
|
340
|
+
):
|
|
341
|
+
"""
|
|
342
|
+
Initialize Informix connection.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
host: Informix hostname
|
|
346
|
+
port: Informix port (default: 9088)
|
|
347
|
+
database: Database name
|
|
348
|
+
server: Informix server name (e.g., "informix_tcp")
|
|
349
|
+
user: Informix username
|
|
350
|
+
password: Informix password
|
|
351
|
+
connect_timeout: Connection timeout in seconds
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
import IfxPy
|
|
355
|
+
import IfxPyDbi
|
|
356
|
+
except ImportError:
|
|
357
|
+
raise ImportError(
|
|
358
|
+
"IfxPy and IfxPyDbi are required for Informix connections. "
|
|
359
|
+
"Install with: pip install IfxPy IfxPyDbi"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
conn_str = (
|
|
363
|
+
f"SERVER={server};"
|
|
364
|
+
f"DATABASE={database};"
|
|
365
|
+
f"HOST={host};"
|
|
366
|
+
f"SERVICE={port};"
|
|
367
|
+
f"UID={user};"
|
|
368
|
+
f"PWD={password};"
|
|
369
|
+
f"PROTOCOL=onsoctcp;"
|
|
370
|
+
f"ConnectTimeout={connect_timeout};"
|
|
371
|
+
)
|
|
372
|
+
raw_conn = IfxPy.connect(conn_str, "", "")
|
|
373
|
+
self.connection = IfxPyDbi.Connection(raw_conn)
|
|
374
|
+
|
|
375
|
+
def fetch_all(self, query: str, params: tuple = ()) -> list[dict[str, Any]]:
|
|
376
|
+
"""Execute query and return list of dictionaries."""
|
|
377
|
+
cursor = self.connection.cursor()
|
|
378
|
+
cursor.execute(query, params)
|
|
379
|
+
|
|
380
|
+
# Get column names (convert to lowercase for consistency)
|
|
381
|
+
columns = [desc[0].lower() for desc in cursor.description]
|
|
382
|
+
|
|
383
|
+
# Fetch all rows
|
|
384
|
+
rows = cursor.fetchall()
|
|
385
|
+
|
|
386
|
+
# Convert to list of dictionaries
|
|
387
|
+
result = [dict(zip(columns, row)) for row in rows]
|
|
388
|
+
|
|
389
|
+
cursor.close()
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
def close(self):
|
|
393
|
+
"""Close Informix connection."""
|
|
394
|
+
if self.connection:
|
|
395
|
+
self.connection.close()
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# Context manager support for all clients
|
|
399
|
+
class DatabaseClientContextManager:
|
|
400
|
+
"""Context manager wrapper for database clients."""
|
|
401
|
+
|
|
402
|
+
def __init__(self, client: DatabaseClient):
|
|
403
|
+
self.client = client
|
|
404
|
+
|
|
405
|
+
def __enter__(self):
|
|
406
|
+
return self.client
|
|
407
|
+
|
|
408
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
409
|
+
self.client.close()
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# Make all clients usable as context managers
|
|
414
|
+
OracleClient.__enter__ = lambda self: self
|
|
415
|
+
OracleClient.__exit__ = lambda self, *args: self.close() or False
|
|
416
|
+
|
|
417
|
+
SQLServerClient.__enter__ = lambda self: self
|
|
418
|
+
SQLServerClient.__exit__ = lambda self, *args: self.close() or False
|
|
419
|
+
|
|
420
|
+
DB2Client.__enter__ = lambda self: self
|
|
421
|
+
DB2Client.__exit__ = lambda self, *args: self.close() or False
|
|
422
|
+
|
|
423
|
+
InformixClient.__enter__ = lambda self: self
|
|
424
|
+
InformixClient.__exit__ = lambda self, *args: self.close() or False
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""IBM Informix driver — requires: pip install IfxPy IfxPyDbi.
|
|
2
|
+
|
|
3
|
+
Uses ``?`` as the parameter placeholder in queries.
|
|
4
|
+
Schema and table names in Informix system catalogs are case-sensitive but
|
|
5
|
+
typically stored in lowercase.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from utils.db_client import BaseDbClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InformixClient(BaseDbClient):
|
|
17
|
+
"""BaseDbClient implementation for IBM Informix."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
host: str,
|
|
22
|
+
port: int = 9088,
|
|
23
|
+
database: str = "",
|
|
24
|
+
server: str = "",
|
|
25
|
+
user: str = "",
|
|
26
|
+
password: str = "",
|
|
27
|
+
connect_timeout: int = 10,
|
|
28
|
+
logger=None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Initialize Informix client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
host: Informix server hostname
|
|
35
|
+
port: Informix server port (default: 9088)
|
|
36
|
+
database: Database name
|
|
37
|
+
server: Informix server name (e.g., "informix_tcp")
|
|
38
|
+
user: Database username
|
|
39
|
+
password: Database password
|
|
40
|
+
connect_timeout: Connection timeout in seconds
|
|
41
|
+
logger: Optional logger instance
|
|
42
|
+
"""
|
|
43
|
+
self.host = host
|
|
44
|
+
self.port = port
|
|
45
|
+
self.database = database
|
|
46
|
+
self.server = server
|
|
47
|
+
self.user = user
|
|
48
|
+
self.password = password
|
|
49
|
+
self.connect_timeout = connect_timeout
|
|
50
|
+
self.logger = logger
|
|
51
|
+
self._conn = None
|
|
52
|
+
|
|
53
|
+
def connect(self) -> None:
|
|
54
|
+
try:
|
|
55
|
+
import IfxPy # noqa: PLC0415
|
|
56
|
+
import IfxPyDbi # noqa: PLC0415
|
|
57
|
+
except ImportError as exc:
|
|
58
|
+
raise ImportError(
|
|
59
|
+
"IfxPy and IfxPyDbi are required for Informix connections. "
|
|
60
|
+
"Install with: pip install IfxPy IfxPyDbi"
|
|
61
|
+
) from exc
|
|
62
|
+
|
|
63
|
+
if self._conn is None:
|
|
64
|
+
conn_str = (
|
|
65
|
+
f"SERVER={self.server};"
|
|
66
|
+
f"DATABASE={self.database};"
|
|
67
|
+
f"HOST={self.host};"
|
|
68
|
+
f"SERVICE={self.port};"
|
|
69
|
+
f"UID={self.user};"
|
|
70
|
+
f"PWD={self.password};"
|
|
71
|
+
f"PROTOCOL=onsoctcp;"
|
|
72
|
+
f"ConnectTimeout={self.connect_timeout};"
|
|
73
|
+
)
|
|
74
|
+
if self.logger:
|
|
75
|
+
self.logger.info(
|
|
76
|
+
"Connecting to Informix %s:%s/%s (server=%s)",
|
|
77
|
+
self.host,
|
|
78
|
+
self.port,
|
|
79
|
+
self.database,
|
|
80
|
+
self.server,
|
|
81
|
+
)
|
|
82
|
+
raw_conn = IfxPy.connect(conn_str, "", "")
|
|
83
|
+
self._conn = IfxPyDbi.Connection(raw_conn)
|
|
84
|
+
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
if self._conn is not None:
|
|
87
|
+
self._conn.close()
|
|
88
|
+
self._conn = None
|
|
89
|
+
if self.logger:
|
|
90
|
+
self.logger.info("Connection closed: %s/%s", self.host, self.database)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _row_to_dict(cursor, row) -> dict:
|
|
94
|
+
"""Convert row tuple to dictionary with lowercase column names."""
|
|
95
|
+
return {desc[0].lower(): val for desc, val in zip(cursor.description, row)}
|
|
96
|
+
|
|
97
|
+
def fetch_all(self, sql: str, params=None) -> list[dict]:
|
|
98
|
+
start = time.perf_counter()
|
|
99
|
+
self._ensure_connected()
|
|
100
|
+
cur = self._conn.cursor()
|
|
101
|
+
cur.execute(sql, params or ())
|
|
102
|
+
rows = [self._row_to_dict(cur, r) for r in cur.fetchall()]
|
|
103
|
+
cur.close()
|
|
104
|
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
105
|
+
if self.logger:
|
|
106
|
+
self.logger.info(
|
|
107
|
+
"fetch_all returned %s rows in %.1fms | %s",
|
|
108
|
+
len(rows),
|
|
109
|
+
elapsed_ms,
|
|
110
|
+
sql[:120],
|
|
111
|
+
)
|
|
112
|
+
return rows
|
|
113
|
+
|
|
114
|
+
def fetch_one(self, sql: str, params=None) -> dict | None:
|
|
115
|
+
self._ensure_connected()
|
|
116
|
+
cur = self._conn.cursor()
|
|
117
|
+
cur.execute(sql, params or ())
|
|
118
|
+
row = cur.fetchone()
|
|
119
|
+
result = self._row_to_dict(cur, row) if row else None
|
|
120
|
+
cur.close()
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
def fetch_scalar(self, sql: str, params=None) -> Any:
|
|
124
|
+
self._ensure_connected()
|
|
125
|
+
cur = self._conn.cursor()
|
|
126
|
+
cur.execute(sql, params or ())
|
|
127
|
+
row = cur.fetchone()
|
|
128
|
+
cur.close()
|
|
129
|
+
return row[0] if row else None
|
|
130
|
+
|
|
131
|
+
def table_exists(self, table: str, schema: str = "") -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Check if a table exists in the database.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
table: Table name
|
|
137
|
+
schema: Schema/owner name (default: current user)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if table exists, False otherwise
|
|
141
|
+
"""
|
|
142
|
+
schema_name = schema or self.user
|
|
143
|
+
sql = """
|
|
144
|
+
SELECT COUNT(*)
|
|
145
|
+
FROM systables
|
|
146
|
+
WHERE tabname = ?
|
|
147
|
+
AND owner = ?
|
|
148
|
+
AND tabtype = 'T'
|
|
149
|
+
"""
|
|
150
|
+
return bool(self.fetch_scalar(sql, (table.lower(), schema_name.lower())))
|
|
151
|
+
|
|
152
|
+
def get_column_names(self, table: str, schema: str = "") -> list[str]:
|
|
153
|
+
"""
|
|
154
|
+
Get column names for a table in order.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
table: Table name
|
|
158
|
+
schema: Schema/owner name (default: current user)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List of column names in lowercase
|
|
162
|
+
"""
|
|
163
|
+
schema_name = schema or self.user
|
|
164
|
+
sql = """
|
|
165
|
+
SELECT c.colname
|
|
166
|
+
FROM syscolumns c
|
|
167
|
+
JOIN systables t ON c.tabid = t.tabid
|
|
168
|
+
WHERE t.tabname = ?
|
|
169
|
+
AND t.owner = ?
|
|
170
|
+
AND t.tabtype = 'T'
|
|
171
|
+
ORDER BY c.colno
|
|
172
|
+
"""
|
|
173
|
+
rows = self.fetch_all(sql, (table.lower(), schema_name.lower()))
|
|
174
|
+
return [r["colname"].lower() for r in rows]
|
|
175
|
+
|
|
176
|
+
def _open_cursor(self):
|
|
177
|
+
self._ensure_connected()
|
|
178
|
+
return self._conn.cursor()
|
|
179
|
+
|
|
180
|
+
def _rows_to_dicts(self, cursor, rows: list) -> list[dict]:
|
|
181
|
+
return [self._row_to_dict(cursor, row) for row in rows]
|
|
182
|
+
|
|
183
|
+
def _ensure_connected(self) -> None:
|
|
184
|
+
if self._conn is None:
|
|
185
|
+
self.connect()
|
|
@@ -7,6 +7,7 @@ Supported values for ``db_type`` (case-insensitive):
|
|
|
7
7
|
sqlserver — pyodbc + ODBC Driver 17/18 on the OS
|
|
8
8
|
db2 — ibm_db + ibm_db_dbi
|
|
9
9
|
oracle — oracledb
|
|
10
|
+
informix — IfxPy + IfxPyDbi
|
|
10
11
|
snowflake — snowflake-connector-python
|
|
11
12
|
|
|
12
13
|
Each driver lazily imports its library inside ``connect()``, so uninstalled
|
|
@@ -17,7 +18,7 @@ from __future__ import annotations
|
|
|
17
18
|
|
|
18
19
|
from utils.db_client import BaseDbClient
|
|
19
20
|
|
|
20
|
-
_SUPPORTED = {"postgresql", "sqlite", "mysql", "sqlserver", "db2", "oracle", "snowflake"}
|
|
21
|
+
_SUPPORTED = {"postgresql", "sqlite", "mysql", "sqlserver", "db2", "oracle", "informix", "snowflake"}
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class DbClientFactory:
|
|
@@ -122,6 +123,19 @@ class DbClientFactory:
|
|
|
122
123
|
logger=logger,
|
|
123
124
|
)
|
|
124
125
|
|
|
126
|
+
if dt == "informix":
|
|
127
|
+
from utils.db_drivers.informix import InformixClient
|
|
128
|
+
return InformixClient(
|
|
129
|
+
host=kwargs["host"],
|
|
130
|
+
port=int(kwargs.get("port", 9088)),
|
|
131
|
+
database=kwargs.get("database", ""),
|
|
132
|
+
server=kwargs.get("server", ""),
|
|
133
|
+
user=kwargs.get("user", ""),
|
|
134
|
+
password=kwargs.get("password", ""),
|
|
135
|
+
connect_timeout=timeout,
|
|
136
|
+
logger=logger,
|
|
137
|
+
)
|
|
138
|
+
|
|
125
139
|
# dt == "snowflake"
|
|
126
140
|
from utils.db_drivers.snowflake import SnowflakeClient
|
|
127
141
|
return SnowflakeClient(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|