dbobj3 0.7.0__tar.gz → 0.9.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.
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dbobj3"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "High-level MySQL/MSSQL/Oracle access module"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.6"
|
|
@@ -16,7 +16,7 @@ dependencies = [
|
|
|
16
16
|
packages = ["src/dbobj3.py"]
|
|
17
17
|
|
|
18
18
|
[tool.bumpversion]
|
|
19
|
-
current_version = "0.
|
|
19
|
+
current_version = "0.9.0"
|
|
20
20
|
commit = true
|
|
21
21
|
tag = true
|
|
22
22
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)"
|
|
@@ -21,47 +21,147 @@ Example:
|
|
|
21
21
|
rsServices.next()
|
|
22
22
|
|
|
23
23
|
"""
|
|
24
|
-
__version__ = "0.
|
|
24
|
+
__version__ = "0.9.0"
|
|
25
25
|
|
|
26
|
+
import os
|
|
27
|
+
import importlib # needed for shim
|
|
26
28
|
from sys import version as sys_version, exit
|
|
29
|
+
from datetime import datetime, date, time
|
|
27
30
|
pyversion = sys_version[0:sys_version.find(' ')]
|
|
28
31
|
using_mysqlclient, using_pymysql = False, False
|
|
29
32
|
|
|
30
33
|
lstLoadedDrivers = []
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
force_pymysql = os.environ.get("DB_FORCE_PYMYSQL","0") == "1"
|
|
35
|
+
drv_env = os.environ.get("MYSQL_DRIVER","").lower() # optional: mysqlclient | pymysql
|
|
36
|
+
MySQLdb = None # placeholder so global name exists
|
|
37
|
+
pymysql = None # placeholder
|
|
38
|
+
|
|
39
|
+
def _select_mysql_driver():
|
|
40
|
+
global using_mysqlclient, using_pymysql, MySQLdb, pymysql
|
|
41
|
+
if force_pymysql or drv_env == "pymysql":
|
|
42
|
+
import pymysql
|
|
43
|
+
pymysql.install_as_MySQLdb()
|
|
44
|
+
import MySQLdb, MySQLdb.cursors, MySQLdb.converters
|
|
45
|
+
# MySQLdb dropped Python3.x support
|
|
46
|
+
# - For Centos/Python2.x use
|
|
47
|
+
# - yum install mysql-devel, MySQL-python
|
|
48
|
+
# - pip install mysql-python
|
|
49
|
+
# - For Python3.x use the fork MySQLdb
|
|
50
|
+
# - pip install mysqlclient ( developed by pymysql devs )
|
|
51
|
+
using_pymysql = True
|
|
52
|
+
lstLoadedDrivers.append({"PyMySQL":"Active"})
|
|
53
|
+
print("MySQL driver: PyMySQL (forced)")
|
|
54
|
+
return
|
|
48
55
|
try:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
#import pymysql.converters
|
|
56
|
+
import MySQLdb, MySQLdb.cursors, MySQLdb.converters
|
|
57
|
+
using_mysqlclient = True
|
|
58
|
+
lstLoadedDrivers.append({"mysqlclient":"Active"})
|
|
59
|
+
print("MySQL driver: mysqlclient")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"mysqlclient import failed ({e}); falling back to PyMySQL")
|
|
62
|
+
import pymysql
|
|
63
|
+
pymysql.install_as_MySQLdb()
|
|
64
|
+
import MySQLdb, MySQLdb.cursors, MySQLdb.converters
|
|
59
65
|
using_pymysql = True
|
|
60
|
-
lstLoadedDrivers.append({"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
lstLoadedDrivers.append({"PyMySQL":"Active"})
|
|
67
|
+
print("MySQL driver: PyMySQL (fallback)")
|
|
68
|
+
|
|
69
|
+
# Minimal shim
|
|
70
|
+
def _ensure_constants_shim():
|
|
71
|
+
"""
|
|
72
|
+
Inject only minimal missing constants when using PyMySQL.
|
|
73
|
+
Avoid constructing synthetic CAPABILITIES to prevent >32-bit signed values.
|
|
74
|
+
Safe to call multiple times.
|
|
75
|
+
"""
|
|
76
|
+
if not using_pymysql:
|
|
77
|
+
return
|
|
78
|
+
try:
|
|
79
|
+
import importlib
|
|
80
|
+
const_pkg = importlib.import_module("MySQLdb.constants")
|
|
81
|
+
# COMMAND (only COM_PING needed for legacy ping code)
|
|
82
|
+
if not hasattr(const_pkg, "COMMAND"):
|
|
83
|
+
print("Injecting minimal MySQLdb.constants.COMMAND shim for COM_PING")
|
|
84
|
+
class _DummyCOMMAND:
|
|
85
|
+
COM_PING = 0x0e
|
|
86
|
+
const_pkg.COMMAND = _DummyCOMMAND
|
|
87
|
+
else:
|
|
88
|
+
if not hasattr(const_pkg.COMMAND, "COM_PING"):
|
|
89
|
+
print("Injecting MySQLdb.constants.COMMAND.COM_PING shim")
|
|
90
|
+
const_pkg.COMMAND.COM_PING = 0x0e
|
|
91
|
+
else:
|
|
92
|
+
pass
|
|
93
|
+
#print(f'COMMAND.COM_PING exists: {const_pkg.COMMAND.COM_PING}')
|
|
94
|
+
# SERVER_STATUS minimal
|
|
95
|
+
if not hasattr(const_pkg, "SERVER_STATUS"):
|
|
96
|
+
class _DummySERVER_STATUS:
|
|
97
|
+
SERVER_STATUS_IN_TRANS = 0x0001
|
|
98
|
+
SERVER_STATUS_AUTOCOMMIT = 0x0002
|
|
99
|
+
const_pkg.SERVER_STATUS = _DummySERVER_STATUS
|
|
100
|
+
# CLIENT flags: only ensure PLUGIN_AUTH if absent; do NOT build CAPABILITIES
|
|
101
|
+
importlib_client = importlib.import_module("MySQLdb.constants.CLIENT")
|
|
102
|
+
if not hasattr(importlib_client, "PLUGIN_AUTH"):
|
|
103
|
+
importlib_client.PLUGIN_AUTH = 0x00080000
|
|
104
|
+
# If CAPABILITIES exists already, leave it untouched. Do NOT create if missing.
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print("MySQL minimal shim error:", e)
|
|
107
|
+
|
|
108
|
+
_select_mysql_driver()
|
|
109
|
+
_ensure_constants_shim()
|
|
110
|
+
|
|
111
|
+
def _build_pymysql_conv():
|
|
112
|
+
"""
|
|
113
|
+
Return a PyMySQL conversion dict that parses DATETIME / TIMESTAMP / DATE
|
|
114
|
+
into real Python datetime/date objects (mimicking mysqlclient behavior).
|
|
115
|
+
"""
|
|
116
|
+
if not using_pymysql:
|
|
117
|
+
return None
|
|
118
|
+
from pymysql.constants import FIELD_TYPE
|
|
119
|
+
from pymysql.converters import conversions
|
|
120
|
+
conv = conversions.copy()
|
|
121
|
+
|
|
122
|
+
def _parse_dt(val):
|
|
123
|
+
if val is None:
|
|
124
|
+
return None
|
|
125
|
+
v = val.strip()
|
|
126
|
+
# Try common formats (truncate fractional seconds)
|
|
127
|
+
base = v.split('.')[0]
|
|
128
|
+
for fmt in ("%Y-%m-%d %H:%M:%S",
|
|
129
|
+
"%Y-%m-%d %H:%M",
|
|
130
|
+
"%Y-%m-%dT%H:%M:%S",
|
|
131
|
+
"%Y-%m-%d"):
|
|
132
|
+
try:
|
|
133
|
+
return datetime.strptime(base, fmt)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
# ISO fallback
|
|
137
|
+
try:
|
|
138
|
+
return datetime.fromisoformat(v.replace('Z',''))
|
|
139
|
+
except Exception:
|
|
140
|
+
return v # leave as original string if unparseable
|
|
141
|
+
|
|
142
|
+
conv[FIELD_TYPE.DATETIME] = _parse_dt
|
|
143
|
+
conv[FIELD_TYPE.TIMESTAMP] = _parse_dt
|
|
144
|
+
conv[FIELD_TYPE.DATE] = (lambda v: date.fromisoformat(v) if v else None)
|
|
145
|
+
return conv
|
|
146
|
+
|
|
147
|
+
# helper: unified connection state check
|
|
148
|
+
def _is_mysql_connected(handle):
|
|
149
|
+
try:
|
|
150
|
+
# mysqlclient has .open attr; PyMySQL does not
|
|
151
|
+
# both should support .ping() method
|
|
152
|
+
if not ( handle and hasattr(handle, "ping") and callable(handle.ping) ):
|
|
153
|
+
return False
|
|
154
|
+
else:
|
|
155
|
+
# mysqlclient: ping() raises exception if not connected
|
|
156
|
+
db_ping_res = None
|
|
157
|
+
db_ping_res = handle.ping()
|
|
158
|
+
if using_mysqlclient:
|
|
159
|
+
print("mysqlclient ping() result:", db_ping_res)
|
|
160
|
+
return True
|
|
161
|
+
else:
|
|
162
|
+
return True
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
65
165
|
|
|
66
166
|
try:
|
|
67
167
|
import pymssql
|
|
@@ -142,12 +242,12 @@ class dbObjCursor:
|
|
|
142
242
|
self.create(self.dbType)
|
|
143
243
|
|
|
144
244
|
def __del__(self):
|
|
145
|
-
if(self.dbType
|
|
245
|
+
if(self.dbType in ("MYSQL", "ORACLE")):
|
|
146
246
|
try:
|
|
147
247
|
if self.cursor and self.cursor.connection and self.cursor.connection.open:
|
|
148
248
|
self.cursor.close()
|
|
149
|
-
except:
|
|
150
|
-
|
|
249
|
+
except Exception as e:
|
|
250
|
+
print(str(e))
|
|
151
251
|
|
|
152
252
|
def create(self, prmDBType="MYSQL"):
|
|
153
253
|
if(prmDBType=="MYSQL"):
|
|
@@ -382,10 +482,11 @@ class dbObjConnection:
|
|
|
382
482
|
driverClass="",
|
|
383
483
|
prmApiBaseUrl="",
|
|
384
484
|
prmApiKey="",
|
|
385
|
-
prmApiKeyName="sk"
|
|
485
|
+
prmApiKeyName="sk",
|
|
486
|
+
prmReuseCursor=False):
|
|
386
487
|
self.set_env( envOracle=envOracle, envProgress=envProgress, driver=driver )
|
|
387
488
|
self.EOF = 1
|
|
388
|
-
self.connHandle = 0
|
|
489
|
+
self.connHandle = None # was 0; use None for clarity
|
|
389
490
|
self.dbType = prmDBType.upper()
|
|
390
491
|
self.msgObj = dbConnMessage()
|
|
391
492
|
self.lastInsId = 0
|
|
@@ -395,6 +496,8 @@ class dbObjConnection:
|
|
|
395
496
|
self.using_pymysql = using_pymysql
|
|
396
497
|
self.drivers = lstLoadedDrivers
|
|
397
498
|
self.driverClass = driverClass
|
|
499
|
+
self.reuseCursor = prmReuseCursor
|
|
500
|
+
self.cursor = None
|
|
398
501
|
if(prmDBType == "API"):
|
|
399
502
|
self.apiBaseUrl = prmApiBaseUrl
|
|
400
503
|
self.apiKeyName = prmApiKeyName
|
|
@@ -470,13 +573,33 @@ class dbObjConnection:
|
|
|
470
573
|
|
|
471
574
|
def mySqlConnect(self, prmHost, prmDb, prmUser, prmPass, prmPort=3306):
|
|
472
575
|
try:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if
|
|
576
|
+
|
|
577
|
+
kwargs = dict(host=prmHost, db=prmDb, user=prmUser, passwd=prmPass, port=prmPort)
|
|
578
|
+
if using_pymysql:
|
|
579
|
+
conv = _build_pymysql_conv()
|
|
580
|
+
if conv:
|
|
581
|
+
kwargs["conv"] = conv
|
|
582
|
+
# Ensure utf8mb4 for PyMySQL
|
|
583
|
+
kwargs.setdefault("charset", "utf8mb4")
|
|
584
|
+
kwargs.setdefault("autocommit", True)
|
|
585
|
+
self.connHandle = MySQLdb.connect(**kwargs)
|
|
586
|
+
# Uniform autocommit enabling (mysqlclient vs pymysql)
|
|
587
|
+
try:
|
|
588
|
+
# mysqlclient
|
|
589
|
+
self.connHandle.autocommit(True)
|
|
590
|
+
except Exception:
|
|
591
|
+
# PyMySQL
|
|
592
|
+
try:
|
|
593
|
+
self.connHandle.autocommit = True
|
|
594
|
+
except Exception:
|
|
595
|
+
pass
|
|
596
|
+
if _is_mysql_connected(self.connHandle):
|
|
476
597
|
self.setConnectSuccess()
|
|
477
598
|
else:
|
|
478
|
-
self.
|
|
599
|
+
self.connHandle = None
|
|
600
|
+
self.setConnectFail("Unknown connection state")
|
|
479
601
|
except Exception as e:
|
|
602
|
+
self.connHandle = None
|
|
480
603
|
self.setConnectFail(e)
|
|
481
604
|
return self.msgObj
|
|
482
605
|
|
|
@@ -610,21 +733,35 @@ class dbObjConnection:
|
|
|
610
733
|
print('Connection to {0} Restored.'.format(self.dbType))
|
|
611
734
|
return self.msgObj
|
|
612
735
|
|
|
736
|
+
def ensure_mysql_connection(self):
|
|
737
|
+
# If connection invalid attempt reconnect (MySQL only)
|
|
738
|
+
if self.dbType == "MYSQL" and not _is_mysql_connected(self.connHandle):
|
|
739
|
+
self.reconnect()
|
|
740
|
+
if not _is_mysql_connected(self.connHandle):
|
|
741
|
+
raise RuntimeError(f"Cannot ensure connection: MySQL connection dead (host={self.host}, db={self.db})")
|
|
742
|
+
|
|
613
743
|
def getCursor(self):
|
|
744
|
+
# Reuse existing cursor if requested and still valid
|
|
745
|
+
if self.cursor and self.reuseCursor and _is_mysql_connected(self.connHandle):
|
|
746
|
+
return self.cursor
|
|
747
|
+
# If connection invalid attempt reconnect (MySQL only)
|
|
748
|
+
self.ensure_mysql_connection()
|
|
749
|
+
# proceed
|
|
614
750
|
if(self.dbType == "MYSQL"):
|
|
615
|
-
|
|
751
|
+
self.cursor = self.mySqlGetCursor()
|
|
616
752
|
elif(self.dbType == "MSSQL"):
|
|
617
|
-
|
|
753
|
+
self.cursor = self.mssqlGetCursor()
|
|
618
754
|
elif(self.dbType == "ORACLE"):
|
|
619
|
-
|
|
755
|
+
self.cursor = self.oracleGetCursor()
|
|
620
756
|
elif(self.dbType == "SQLITE" or self.dbType == "SQLITE3"):
|
|
621
|
-
|
|
757
|
+
self.cursor = self.sqlite3GetCursor()
|
|
622
758
|
elif(self.dbType == "PROGRESS"):
|
|
623
|
-
|
|
759
|
+
self.cursor = self.progressGetCursor()
|
|
624
760
|
elif(self.dbType == "API"):
|
|
625
|
-
|
|
761
|
+
self.cursor = self.apiGetCursor()
|
|
626
762
|
elif(self.dbType == "JDBC"):
|
|
627
|
-
|
|
763
|
+
self.cursor = self.jdbcGetCursor()
|
|
764
|
+
return self.cursor
|
|
628
765
|
|
|
629
766
|
def setCursorSuccess(self, cursor):
|
|
630
767
|
if cursor:
|
|
@@ -639,11 +776,14 @@ class dbObjConnection:
|
|
|
639
776
|
|
|
640
777
|
def mySqlGetCursor(self):
|
|
641
778
|
try:
|
|
779
|
+
if not _is_mysql_connected(self.connHandle):
|
|
780
|
+
raise RuntimeError("MySQL connection not established; cannot create cursor")
|
|
642
781
|
cursor = dbObjCursor(self.connHandle, self.dbType)
|
|
643
782
|
self.setCursorSuccess(cursor)
|
|
644
783
|
return cursor
|
|
645
784
|
except Exception as e:
|
|
646
785
|
self.setCursorFail(e)
|
|
786
|
+
raise
|
|
647
787
|
|
|
648
788
|
def mssqlGetCursor(self):
|
|
649
789
|
try:
|
|
@@ -863,42 +1003,20 @@ class dbObjConnection:
|
|
|
863
1003
|
self.jdbcPing()
|
|
864
1004
|
|
|
865
1005
|
def mySqlPing(self, reconnect=True):
|
|
866
|
-
"""
|
|
867
|
-
Check if the server is alive.
|
|
868
|
-
|
|
869
|
-
:param reconnect: If the connection is closed, reconnect.
|
|
870
|
-
:raise Error: If the connection is closed and reconnect=False.
|
|
871
|
-
"""
|
|
872
|
-
if using_pymysql:
|
|
873
|
-
self.connHandle.ping( reconnect )
|
|
874
|
-
return
|
|
875
|
-
elif( not self.connHandle.open ):
|
|
876
|
-
if reconnect:
|
|
877
|
-
self.connect()
|
|
878
|
-
reconnect = False
|
|
879
|
-
else:
|
|
880
|
-
raise Exception("Already closed")
|
|
881
|
-
|
|
882
|
-
''' This doesn't work with mysqlclient
|
|
883
|
-
if self.connHandle._sock is None:
|
|
884
|
-
if reconnect:
|
|
885
|
-
self.connect()
|
|
886
|
-
#self.connHandle.connect()
|
|
887
|
-
reconnect = False
|
|
888
|
-
else:
|
|
889
|
-
raise Exception("Already closed")
|
|
890
|
-
'''
|
|
891
|
-
|
|
892
1006
|
try:
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if reconnect:
|
|
898
|
-
self.connect()
|
|
899
|
-
self.ping( False )
|
|
1007
|
+
if _is_mysql_connected(self.connHandle):
|
|
1008
|
+
return
|
|
1009
|
+
if not reconnect:
|
|
1010
|
+
raise RuntimeError("MySQL connection not established; cannot ping")
|
|
900
1011
|
else:
|
|
901
|
-
|
|
1012
|
+
self.reconnect()
|
|
1013
|
+
if not _is_mysql_connected(self.connHandle):
|
|
1014
|
+
raise RuntimeError("MySQL connection not established; cannot ping")
|
|
1015
|
+
except Exception as e:
|
|
1016
|
+
self.setConnectFail(e)
|
|
1017
|
+
raise
|
|
1018
|
+
else:
|
|
1019
|
+
self.setConnectSuccess()
|
|
902
1020
|
|
|
903
1021
|
def setMySqlExecuteFail(self, mySqlCursor, prmSql, e, *args):
|
|
904
1022
|
self.msgObj.success = False
|
|
@@ -929,10 +1047,14 @@ class dbObjConnection:
|
|
|
929
1047
|
finally:
|
|
930
1048
|
return mySqlCursor
|
|
931
1049
|
|
|
932
|
-
def mySqlExecute(self, prmSql, *args):
|
|
1050
|
+
def mySqlExecute(self, prmSql, *args, **kw):
|
|
933
1051
|
mySqlCursor = self.getCursor()
|
|
934
1052
|
try:
|
|
935
|
-
if
|
|
1053
|
+
if not hasattr(self.connHandle, 'open') or self.connHandle.open==0 :
|
|
1054
|
+
self.reconnect()
|
|
1055
|
+
if kw:
|
|
1056
|
+
mySqlCursor.cursor.execute(prmSql, kw)
|
|
1057
|
+
elif args:
|
|
936
1058
|
mySqlCursor.cursor.execute(prmSql, args)
|
|
937
1059
|
else:
|
|
938
1060
|
mySqlCursor.cursor.execute(prmSql)
|
|
@@ -1063,7 +1185,7 @@ class dbObjConnection:
|
|
|
1063
1185
|
if(r.status_code == requests.codes.ok):
|
|
1064
1186
|
retVal = r.json()
|
|
1065
1187
|
#print(retVal)
|
|
1066
|
-
if(retVal["error"]
|
|
1188
|
+
if(retVal["error"].success):
|
|
1067
1189
|
apiCursor.lstRecords = retVal["data"]
|
|
1068
1190
|
apiCursor.numRows = len(apiCursor.lstRecords)
|
|
1069
1191
|
if(apiCursor.numRows > 0):
|
|
File without changes
|