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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dbobj3
3
- Version: 0.7.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.7.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.7.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.7.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
- try:
32
- #raise ImportError
33
- import MySQLdb
34
- import MySQLdb.cursors
35
- import MySQLdb.converters
36
- # MySQLdb dropped Python3.x support
37
- # - For Centos/Python2.x use
38
- # - yum install mysql-devel, MySQL-python
39
- # - pip install mysql-python
40
- # - For Python3.x use the fork MySQLdb
41
- # - pip install mysqlclient ( developed by pymysql devs )
42
- using_mysqlclient = True
43
- lstLoadedDrivers.append({"MySQLdb": "Loaded (MySQL)"})
44
- except ImportError:
45
- lstLoadedDrivers.append({"MySQLdb": "Not Loaded (MySQL)"})
46
-
47
- 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
48
55
  try:
49
- # pymysql is a pure python implementation of MySQLdb
50
- # Linux:
51
- import pymysql as MySQLdb
52
- import MySQLdb.cursors
53
- import MySQLdb.converters
54
- # Windows:
55
- #import pymysql
56
- #pymysql.install_as_MySQLdb()
57
- #import pymysql.cursors
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({"pymysql": "Loaded (MySQL)"})
61
- except ImportError:
62
- #print('Python {0}: could neither import pymysql nor MySQLdb'.format(pyversion))
63
- lstLoadedDrivers.append({"pymysql": "Not Loaded (MySQL)"})
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 == "MYSQL"):
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
- pass
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
- self.connHandle = MySQLdb.connect(host=prmHost,db=prmDb,user=prmUser,passwd=prmPass,port=prmPort)
474
- self.connHandle.autocommit(True)
475
- if(self.connHandle):
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.setConnectFail()
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
- return self.mySqlGetCursor()
751
+ self.cursor = self.mySqlGetCursor()
616
752
  elif(self.dbType == "MSSQL"):
617
- return self.mssqlGetCursor()
753
+ self.cursor = self.mssqlGetCursor()
618
754
  elif(self.dbType == "ORACLE"):
619
- return self.oracleGetCursor()
755
+ self.cursor = self.oracleGetCursor()
620
756
  elif(self.dbType == "SQLITE" or self.dbType == "SQLITE3"):
621
- return self.sqlite3GetCursor()
757
+ self.cursor = self.sqlite3GetCursor()
622
758
  elif(self.dbType == "PROGRESS"):
623
- return self.progressGetCursor()
759
+ self.cursor = self.progressGetCursor()
624
760
  elif(self.dbType == "API"):
625
- return self.apiGetCursor()
761
+ self.cursor = self.apiGetCursor()
626
762
  elif(self.dbType == "JDBC"):
627
- return self.jdbcGetCursor()
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
- #self.connHandle._execute_command(COMMAND.COM_PING, "")
894
- #self.connHandle._read_ok_packet()
895
- self.connHandle.ping()
896
- except Exception as e:
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
- 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()
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 args:
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"] == 0):
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