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.
Files changed (93) hide show
  1. {pyplaykit-1.7.0/pyplaykit.egg-info → pyplaykit-1.8.0}/PKG-INFO +1 -1
  2. {pyplaykit-1.7.0 → pyplaykit-1.8.0/pyplaykit.egg-info}/PKG-INFO +1 -1
  3. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit.egg-info/SOURCES.txt +2 -0
  4. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyproject.toml +1 -1
  5. pyplaykit-1.8.0/utils/database_clients.py +424 -0
  6. pyplaykit-1.8.0/utils/db_drivers/informix.py +185 -0
  7. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_factory.py +15 -1
  8. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/MANIFEST.in +0 -0
  9. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/README.md +0 -0
  10. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/components/__init__.py +0 -0
  11. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/components/base_component.py +0 -0
  12. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/components/data_table.py +0 -0
  13. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/components/form_field.py +0 -0
  14. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/components/modal.py +0 -0
  15. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/config/config.yaml +0 -0
  16. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/config/environments.yaml +0 -0
  17. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/config/projects/customs_modernization.yaml +0 -0
  18. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/config/projects/dispatcho.yaml +0 -0
  19. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/config/projects/timelyquote.yaml +0 -0
  20. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/core/__init__.py +0 -0
  21. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/core/base_page.py +0 -0
  22. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/core/base_test.py +0 -0
  23. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/core/browser_factory.py +0 -0
  24. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/core/playwright_manager.py +0 -0
  25. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/dist_pkg/__init__.py +0 -0
  26. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/dist_pkg/distribution.py +0 -0
  27. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/integrations/__init__.py +0 -0
  28. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/integrations/adapters.py +0 -0
  29. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/orchestration/__init__.py +0 -0
  30. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/orchestration/enforcer.py +0 -0
  31. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/orchestration/planner.py +0 -0
  32. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/__init__.py +0 -0
  33. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/dashboard_page.py +0 -0
  34. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/login_page.py +0 -0
  35. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/orangehrm_login_page.py +0 -0
  36. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/saucedemo_inventory_page.py +0 -0
  37. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pages/saucedemo_login_page.py +0 -0
  38. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/__init__.py +0 -0
  39. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/api_plugin.py +0 -0
  40. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/base.py +0 -0
  41. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/data_plugin.py +0 -0
  42. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/registry.py +0 -0
  43. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/plugins/security_plugin.py +0 -0
  44. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit/__init__.py +0 -0
  45. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit/cli.py +0 -0
  46. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit/plugin.py +0 -0
  47. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit.egg-info/dependency_links.txt +0 -0
  48. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit.egg-info/entry_points.txt +0 -0
  49. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit.egg-info/requires.txt +0 -0
  50. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/pyplaykit.egg-info/top_level.txt +0 -0
  51. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/resilience/__init__.py +0 -0
  52. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/resilience/locator_recovery.py +0 -0
  53. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/setup.cfg +0 -0
  54. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/api/ecom/auth_payloads.json +0 -0
  55. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/api/ecom/product_payloads.json +0 -0
  56. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/api_test_data.json +0 -0
  57. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/data_validation_test_data.json +0 -0
  58. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/login_test_data.json +0 -0
  59. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/orangehrm_login_data.json +0 -0
  60. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/test_data/ui_test_data.json +0 -0
  61. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/__init__.py +0 -0
  62. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/api_client.py +0 -0
  63. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/api_test_helper.py +0 -0
  64. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/assertion_helper.py +0 -0
  65. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/auth_manager.py +0 -0
  66. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/data_comparison_report.py +0 -0
  67. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/data_comparison_utils.py +0 -0
  68. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/data_loader.py +0 -0
  69. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/data_source.py +0 -0
  70. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/data_validator.py +0 -0
  71. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_client.py +0 -0
  72. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/__init__.py +0 -0
  73. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/db2.py +0 -0
  74. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/mysql.py +0 -0
  75. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/oracle.py +0 -0
  76. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/snowflake.py +0 -0
  77. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/sqlite.py +0 -0
  78. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_drivers/sqlserver.py +0 -0
  79. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_fixtures.py +0 -0
  80. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_pool.py +0 -0
  81. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/db_validation_helper.py +0 -0
  82. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/environment_validator.py +0 -0
  83. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/file_reader.py +0 -0
  84. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/graphql_client.py +0 -0
  85. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/logger.py +0 -0
  86. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/observability.py +0 -0
  87. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/response_validator.py +0 -0
  88. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/screenshot_helper.py +0 -0
  89. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/soap_client.py +0 -0
  90. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/tdm.py +0 -0
  91. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/transformation_validator.py +0 -0
  92. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/ui_db_validator.py +0 -0
  93. {pyplaykit-1.7.0 → pyplaykit-1.8.0}/utils/yaml_config_reader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyplaykit
3
- Version: 1.7.0
3
+ Version: 1.8.0
4
4
  Summary: Enterprise Python + Playwright automation framework
5
5
  Author-email: Shan Konduru <shankonduru@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyplaykit
3
- Version: 1.7.0
3
+ Version: 1.8.0
4
4
  Summary: Enterprise Python + Playwright automation framework
5
5
  Author-email: Shan Konduru <shankonduru@gmail.com>
6
6
  License: MIT
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyplaykit"
7
- version = "1.7.0"
7
+ version = "1.8.0"
8
8
  description = "Enterprise Python + Playwright automation framework"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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