dbobj3 0.8.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dbobj3
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: High-level MySQL/MSSQL/Oracle access module
5
5
  Requires-Python: >=3.6
6
6
  Requires-Dist: pymysql
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dbobj3"
7
- version = "0.8.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.8.0"
19
+ current_version = "0.9.0"
20
20
  commit = true
21
21
  tag = true
22
22
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)"
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python3.9
1
+ #!/usr/bin/env python3.6
2
2
 
3
3
  """
4
4
  High-Level Access to MySQL, MS SQL Server, Oracle Module and more.
@@ -21,48 +21,92 @@ Example:
21
21
  rsServices.next()
22
22
 
23
23
  """
24
- __version__ = "0.8.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
27
29
  from datetime import datetime, date, time
28
30
  pyversion = sys_version[0:sys_version.find(' ')]
29
31
  using_mysqlclient, using_pymysql = False, False
30
32
 
31
33
  lstLoadedDrivers = []
32
- try:
33
- #raise ImportError
34
- import MySQLdb
35
- import MySQLdb.cursors
36
- #import MySQLdb.converters
37
- # MySQLdb dropped Python3.x support
38
- # - For Centos/Python2.x use
39
- # - yum install mysql-devel, MySQL-python
40
- # - pip install mysql-python
41
- # - For Python3.x use the fork MySQLdb
42
- # - pip install mysqlclient ( developed by pymysql devs )
43
- using_mysqlclient = True
44
- lstLoadedDrivers.append({"MySQLdb": "Loaded (MySQL)"})
45
- except ImportError:
46
- lstLoadedDrivers.append({"MySQLdb": "Not Loaded (MySQL)"})
47
-
48
- if not using_mysqlclient:
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
49
55
  try:
50
- # pymysql is a pure python implementation of MySQLdb
51
- # Linux:
52
- import pymysql as MySQLdb
53
- #import MySQLdb.cursors
54
- #import MySQLdb.converters
55
- # Windows:
56
- #import pymysql
57
- #pymysql.install_as_MySQLdb()
58
- #import pymysql.cursors
59
- #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
60
65
  using_pymysql = True
61
- lstLoadedDrivers.append({"pymysql": "Loaded (MySQL)"})
62
- except ImportError:
63
- #print('Python {0}: could neither import pymysql nor MySQLdb'.format(pyversion))
64
- lstLoadedDrivers.append({"pymysql": "Not Loaded (MySQL)"})
65
- #'''
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()
66
110
 
67
111
  def _build_pymysql_conv():
68
112
  """
@@ -100,6 +144,24 @@ def _build_pymysql_conv():
100
144
  conv[FIELD_TYPE.DATE] = (lambda v: date.fromisoformat(v) if v else None)
101
145
  return conv
102
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
103
165
 
104
166
  try:
105
167
  import pymssql
@@ -180,12 +242,12 @@ class dbObjCursor:
180
242
  self.create(self.dbType)
181
243
 
182
244
  def __del__(self):
183
- if(self.dbType == "MYSQL"):
245
+ if(self.dbType in ("MYSQL", "ORACLE")):
184
246
  try:
185
247
  if self.cursor and self.cursor.connection and self.cursor.connection.open:
186
248
  self.cursor.close()
187
- except:
188
- pass
249
+ except Exception as e:
250
+ print(str(e))
189
251
 
190
252
  def create(self, prmDBType="MYSQL"):
191
253
  if(prmDBType=="MYSQL"):
@@ -420,10 +482,11 @@ class dbObjConnection:
420
482
  driverClass="",
421
483
  prmApiBaseUrl="",
422
484
  prmApiKey="",
423
- prmApiKeyName="sk"):
485
+ prmApiKeyName="sk",
486
+ prmReuseCursor=False):
424
487
  self.set_env( envOracle=envOracle, envProgress=envProgress, driver=driver )
425
488
  self.EOF = 1
426
- self.connHandle = 0
489
+ self.connHandle = None # was 0; use None for clarity
427
490
  self.dbType = prmDBType.upper()
428
491
  self.msgObj = dbConnMessage()
429
492
  self.lastInsId = 0
@@ -433,6 +496,8 @@ class dbObjConnection:
433
496
  self.using_pymysql = using_pymysql
434
497
  self.drivers = lstLoadedDrivers
435
498
  self.driverClass = driverClass
499
+ self.reuseCursor = prmReuseCursor
500
+ self.cursor = None
436
501
  if(prmDBType == "API"):
437
502
  self.apiBaseUrl = prmApiBaseUrl
438
503
  self.apiKeyName = prmApiKeyName
@@ -508,6 +573,7 @@ class dbObjConnection:
508
573
 
509
574
  def mySqlConnect(self, prmHost, prmDb, prmUser, prmPass, prmPort=3306):
510
575
  try:
576
+
511
577
  kwargs = dict(host=prmHost, db=prmDb, user=prmUser, passwd=prmPass, port=prmPort)
512
578
  if using_pymysql:
513
579
  conv = _build_pymysql_conv()
@@ -519,17 +585,21 @@ class dbObjConnection:
519
585
  self.connHandle = MySQLdb.connect(**kwargs)
520
586
  # Uniform autocommit enabling (mysqlclient vs pymysql)
521
587
  try:
588
+ # mysqlclient
522
589
  self.connHandle.autocommit(True)
523
590
  except Exception:
591
+ # PyMySQL
524
592
  try:
525
593
  self.connHandle.autocommit = True
526
594
  except Exception:
527
595
  pass
528
- if(self.connHandle):
596
+ if _is_mysql_connected(self.connHandle):
529
597
  self.setConnectSuccess()
530
598
  else:
531
- self.setConnectFail()
599
+ self.connHandle = None
600
+ self.setConnectFail("Unknown connection state")
532
601
  except Exception as e:
602
+ self.connHandle = None
533
603
  self.setConnectFail(e)
534
604
  return self.msgObj
535
605
 
@@ -663,21 +733,35 @@ class dbObjConnection:
663
733
  print('Connection to {0} Restored.'.format(self.dbType))
664
734
  return self.msgObj
665
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
+
666
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
667
750
  if(self.dbType == "MYSQL"):
668
- return self.mySqlGetCursor()
751
+ self.cursor = self.mySqlGetCursor()
669
752
  elif(self.dbType == "MSSQL"):
670
- return self.mssqlGetCursor()
753
+ self.cursor = self.mssqlGetCursor()
671
754
  elif(self.dbType == "ORACLE"):
672
- return self.oracleGetCursor()
755
+ self.cursor = self.oracleGetCursor()
673
756
  elif(self.dbType == "SQLITE" or self.dbType == "SQLITE3"):
674
- return self.sqlite3GetCursor()
757
+ self.cursor = self.sqlite3GetCursor()
675
758
  elif(self.dbType == "PROGRESS"):
676
- return self.progressGetCursor()
759
+ self.cursor = self.progressGetCursor()
677
760
  elif(self.dbType == "API"):
678
- return self.apiGetCursor()
761
+ self.cursor = self.apiGetCursor()
679
762
  elif(self.dbType == "JDBC"):
680
- return self.jdbcGetCursor()
763
+ self.cursor = self.jdbcGetCursor()
764
+ return self.cursor
681
765
 
682
766
  def setCursorSuccess(self, cursor):
683
767
  if cursor:
@@ -692,11 +776,14 @@ class dbObjConnection:
692
776
 
693
777
  def mySqlGetCursor(self):
694
778
  try:
779
+ if not _is_mysql_connected(self.connHandle):
780
+ raise RuntimeError("MySQL connection not established; cannot create cursor")
695
781
  cursor = dbObjCursor(self.connHandle, self.dbType)
696
782
  self.setCursorSuccess(cursor)
697
783
  return cursor
698
784
  except Exception as e:
699
785
  self.setCursorFail(e)
786
+ raise
700
787
 
701
788
  def mssqlGetCursor(self):
702
789
  try:
@@ -797,10 +884,10 @@ class dbObjConnection:
797
884
  self.connHandle.close()
798
885
  self.connHandle = None
799
886
 
800
- def execute(self, prmSql, *args, **kw):
887
+ def execute(self, prmSql, *args):
801
888
  """Execute the SQL and return a record set if applicable"""
802
889
  if(self.dbType == "MYSQL"):
803
- return self.mySqlExecute(prmSql, *args, **kw)
890
+ return self.mySqlExecute(prmSql, *args)
804
891
  if(self.dbType == "MSSQL"):
805
892
  return self.msSqlExecute(prmSql)
806
893
  if(self.dbType == "ORACLE"):
@@ -916,44 +1003,22 @@ class dbObjConnection:
916
1003
  self.jdbcPing()
917
1004
 
918
1005
  def mySqlPing(self, reconnect=True):
919
- """
920
- Check if the server is alive.
921
-
922
- :param reconnect: If the connection is closed, reconnect.
923
- :raise Error: If the connection is closed and reconnect=False.
924
- """
925
- if using_pymysql:
926
- self.connHandle.ping( reconnect )
927
- return
928
- elif( not self.connHandle.open ):
929
- if reconnect:
930
- self.connect()
931
- reconnect = False
932
- else:
933
- raise Exception("Already closed")
934
-
935
- ''' This doesn't work with mysqlclient
936
- if self.connHandle._sock is None:
937
- if reconnect:
938
- self.connect()
939
- #self.connHandle.connect()
940
- reconnect = False
941
- else:
942
- raise Exception("Already closed")
943
- '''
944
-
945
1006
  try:
946
- #self.connHandle._execute_command(COMMAND.COM_PING, "")
947
- #self.connHandle._read_ok_packet()
948
- self.connHandle.ping()
949
- except Exception as e:
950
- if reconnect:
951
- self.connect()
952
- 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")
953
1011
  else:
954
- raise Exception
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()
955
1020
 
956
- def setMySqlExecuteFail(self, mySqlCursor, prmSql, e, *args, **kw):
1021
+ def setMySqlExecuteFail(self, mySqlCursor, prmSql, e, *args):
957
1022
  self.msgObj.success = False
958
1023
  prmSql += '' if not args else ' ({0}) '.format( repr(args) )
959
1024
  self.msgObj.message = "SQL Failed: {0} - {1}".format( prmSql, e )
@@ -967,9 +1032,9 @@ class dbObjConnection:
967
1032
  finally:
968
1033
  return mySqlCursor
969
1034
 
970
- def setMySqlExecuteSuccess(self, mySqlCursor, prmSql, *args, **kw):
1035
+ def setMySqlExecuteSuccess(self, mySqlCursor, prmSql, *args):
971
1036
  self.msgObj.success = True
972
- self.msgObj.message = "SQL Executed: {0} {1}".format(prmSql,'' if not args else repr(args)+ ' ' + repr(kw))
1037
+ self.msgObj.message = "SQL Executed: {0} {1}".format(prmSql,'' if not args else repr(args))
973
1038
  try:
974
1039
  mySqlCursor.numFields = len(mySqlCursor.cursor.description)
975
1040
  if(mySqlCursor.numRows):
@@ -1007,23 +1072,22 @@ class dbObjConnection:
1007
1072
  self.msgObj.message += ' or killed process/connection.'
1008
1073
  self.msgObj.message += ' connHandle={0}, connHandle.open={1}'.format(self.connHandle,self.connHandle.open)
1009
1074
  self.reconnect()
1010
- elif not hasattr(self.connHandle, 'open') or self.connHandle.open==0 or "t connect t" in str(e).lower():
1075
+ elif self.connHandle.open==0 or "Can't connect to" in str(e):
1011
1076
  self.msgObj.message += ' - Database Connection Closed or Never Established.'
1012
1077
  self.msgObj.message += ' connHandle={0}, connHandle.open={1}'.format(self.connHandle,self.connHandle.open)
1013
1078
  self.reconnect()
1014
- return mySqlCursor if not self.msgObj.success else self.mySqlExecute( prmSql, *args, **kw )
1079
+ return mySqlCursor if not self.msgObj.success else self.mySqlExecute( prmSql, *args )
1015
1080
  except Exception as e:
1016
1081
  mySqlCursor = self.setMySqlExecuteFail(mySqlCursor, prmSql, e, *args)
1017
- #if self.connHandle.open==0 or "Can't connect to" in str(e): # This line was failing (self.connHandle was an "int") and so not reconnecting
1018
- if not hasattr(self.connHandle, 'open') or self.connHandle.open==0 or "t connect t" in str(e).lower():
1082
+ if self.connHandle.open==0 or "Can't connect to" in str(e):
1019
1083
  self.msgObj.message += ' - Database Connection Closed or Never Established.'
1020
1084
  self.msgObj.message += ' connHandle={0}, connHandle.open={1}'.format(self.connHandle,self.connHandle.open)
1021
1085
  self.reconnect()
1022
- return mySqlCursor if not self.msgObj.success else self.mySqlExecute( prmSql, *args, **kw )
1086
+ return mySqlCursor if not self.msgObj.success else self.mySqlExecute( prmSql, *args )
1023
1087
  print(self.msgObj.message)
1024
1088
  return mySqlCursor
1025
1089
  else:
1026
- mySqlCursor = self.setMySqlExecuteSuccess(mySqlCursor, prmSql, *args, **kw)
1090
+ mySqlCursor = self.setMySqlExecuteSuccess(mySqlCursor, prmSql, *args)
1027
1091
  return mySqlCursor
1028
1092
 
1029
1093
  def msSqlExecute(self, prmSql):
@@ -1121,7 +1185,7 @@ class dbObjConnection:
1121
1185
  if(r.status_code == requests.codes.ok):
1122
1186
  retVal = r.json()
1123
1187
  #print(retVal)
1124
- if(retVal["error"] == 0):
1188
+ if(retVal["error"].success):
1125
1189
  apiCursor.lstRecords = retVal["data"]
1126
1190
  apiCursor.numRows = len(apiCursor.lstRecords)
1127
1191
  if(apiCursor.numRows > 0):
@@ -1191,14 +1255,14 @@ class dbObjConnection:
1191
1255
  self.msgObj.message += ' - Database Connection Closed or Never Established.'
1192
1256
  self.msgObj.message += ' connHandle={0}, connHandle.open={1}'.format(self.connHandle,self.connHandle.open)
1193
1257
  self.reconnect()
1194
- return mySqlCursor if not self.msgObj.success else self.mySqlExecuteMany( prmSql, *args )
1258
+ return mySqlCursor if not self.msgObj.success else self.mySqlExecute( prmSql, *args )
1195
1259
  except Exception as e:
1196
1260
  mySqlCursor = self.setMySqlExecuteFail(mySqlCursor, prmSql, e, *args)
1197
1261
  if self.connHandle.open==0 or "Can't connect to" in str(e):
1198
1262
  self.msgObj.message += ' - Database Connection Closed or Never Established.'
1199
1263
  self.msgObj.message += ' connHandle={0}, connHandle.open={1}'.format(self.connHandle,self.connHandle.open)
1200
1264
  self.reconnect()
1201
- return mySqlCursor if not self.msgObj.success else self.mySqlExecuteMany( prmSql, *args )
1265
+ return mySqlCursor if not self.msgObj.success else self.mySqlExecute( prmSql, *args )
1202
1266
  print(self.msgObj.message)
1203
1267
  return mySqlCursor
1204
1268
  else:
File without changes