rda-python-common 2.1.10__py3-none-any.whl → 3.0.0__py3-none-any.whl

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.
@@ -17,12 +17,53 @@ import re
17
17
  import time
18
18
  import hvac
19
19
  from datetime import datetime
20
- import psycopg2 as PgSQL
21
- from psycopg2.extras import execute_values
22
- from psycopg2.extras import execute_batch
23
20
  from os import path as op
24
21
  from . import PgLOG
25
22
 
23
+ # Prefer psycopg (v3); fall back to psycopg2 if v3 is not installed.
24
+ try:
25
+ import psycopg as PgSQL
26
+ PG_DRIVER = 'psycopg3'
27
+
28
+ def execute_values(cursor, sql, argslist, page_size=100):
29
+ """Compatibility shim for psycopg2.extras.execute_values on psycopg3.
30
+
31
+ Rewrites ``VALUES %s`` placeholder to ``VALUES (%s, %s, ...)`` based on
32
+ the column count inferred from the first row, then dispatches to
33
+ psycopg3's ``executemany`` (which already batches efficiently).
34
+ """
35
+ if not argslist: return
36
+ ncol = len(argslist[0])
37
+ row_ph = '(' + ','.join(['%s'] * ncol) + ')'
38
+ new_sql = re.sub(r'(?i)\bVALUES\s+%s\b', 'VALUES ' + row_ph, sql, count=1)
39
+ cursor.executemany(new_sql, argslist)
40
+
41
+ def execute_batch(cursor, sql, argslist, page_size=100):
42
+ """Compatibility shim for psycopg2.extras.execute_batch on psycopg3."""
43
+ cursor.executemany(sql, argslist)
44
+
45
+ def get_pgcode(pgerr):
46
+ """Return SQLSTATE for a psycopg3 error (via err.diag.sqlstate)."""
47
+ diag = getattr(pgerr, 'diag', None)
48
+ return getattr(diag, 'sqlstate', None) if diag is not None else None
49
+
50
+ def get_pgerror(pgerr):
51
+ """Return primary error message for a psycopg3 error (via err.diag.message_primary)."""
52
+ diag = getattr(pgerr, 'diag', None)
53
+ return getattr(diag, 'message_primary', None) if diag is not None else None
54
+ except ImportError:
55
+ import psycopg2 as PgSQL
56
+ from psycopg2.extras import execute_values, execute_batch
57
+ PG_DRIVER = 'psycopg2'
58
+
59
+ def get_pgcode(pgerr):
60
+ """Return SQLSTATE for a psycopg2 error (via err.pgcode)."""
61
+ return getattr(pgerr, 'pgcode', None)
62
+
63
+ def get_pgerror(pgerr):
64
+ """Return primary error message for a psycopg2 error (via err.pgerror)."""
65
+ return getattr(pgerr, 'pgerror', None)
66
+
26
67
  pgdb = None # reference to a connected database object
27
68
  curtran = 0 # 0 - no transaction, 1 - in transaction
28
69
  NMISSES = [] # array of mising userno
@@ -439,8 +480,8 @@ def check_dberror(pgerr, pgcnt, sqlstr, ary, logact = PGDBI['ERRLOG']):
439
480
 
440
481
  ret = PgLOG.FAILURE
441
482
 
442
- pgcode = pgerr.pgcode
443
- pgerror = pgerr.pgerror
483
+ pgcode = get_pgcode(pgerr)
484
+ pgerror = get_pgerror(pgerr)
444
485
  dberror = "{} {}".format(pgcode, pgerror) if pgcode and pgerror else str(pgerr)
445
486
  if pgcnt < PgLOG.PGLOG['DBRETRY']:
446
487
  if not pgcode:
@@ -517,7 +558,7 @@ def pgconnect(reconnect = 0, pgcnt = 0, autocommit = True):
517
558
  reconnect = 0 # initial connection
518
559
 
519
560
  while True:
520
- config = {'database' : PGDBI['DBNAME'],
561
+ config = {'dbname' : PGDBI['DBNAME'],
521
562
  'user' : PGDBI['LNNAME']}
522
563
  if PGDBI['DBSHOST'] == PgLOG.PGLOG['HOSTNAME']:
523
564
  config['host'] = 'localhost'
@@ -526,7 +567,7 @@ def pgconnect(reconnect = 0, pgcnt = 0, autocommit = True):
526
567
  if not PGDBI['DBPORT']: PGDBI['DBPORT'] = get_dbport(PGDBI['DBNAME'])
527
568
  if PGDBI['DBPORT']: config['port'] = PGDBI['DBPORT']
528
569
  config['password'] = '***'
529
- sqlstr = "psycopg2.connect(**{})".format(config)
570
+ sqlstr = "{}.connect(**{})".format(PG_DRIVER, config)
530
571
  config['password'] = get_pgpass_password()
531
572
  if PgLOG.PGLOG['DBGLEVEL']: PgLOG.pgdbg(1000, sqlstr)
532
573
  try:
@@ -788,7 +788,7 @@ def delete_backup_file(file, endpoint = None, logact = 0):
788
788
  return PgLOG.FAILURE
789
789
 
790
790
  #
791
- # reset local file/directory information to make them writable for PgLOG.PGLOG['GDEXUSER']
791
+ # reset local file/directory information to make them writable for PgLOG.PGLOG['COMMONUSER']
792
792
  # file - file name (mandatory)
793
793
  # info - gathered file info with option 14, None means file not exists
794
794
  #
@@ -60,7 +60,7 @@ MISLOG = (0x00811) # cannot access logfile
60
60
  EMLSUM = (0x08000) # record as email summary
61
61
  EMEROL = (0x10000) # record error as email only
62
62
  EMLALL = (0x1D208) # all email acts
63
- DOSUDO = (0x20000) # add 'sudo -u PGLOG['GDEXUSER']'
63
+ DOSUDO = (0x20000) # add 'sudo -u PGLOG['COMMONUSER']'
64
64
  NOTLOG = (0x40000) # do not log any thing
65
65
  OVRIDE = (0x80000) # do override existing file or record
66
66
  NOWAIT = (0x100000) # do not wait on globus task to finish
@@ -100,9 +100,8 @@ PGLOG = { # more defined in untaint_suid() with environment variables
100
100
  'BACKROOT': "/DRDATA/DECS", # backup path for desaster recovering tape on hpss
101
101
  'OLDAROOT': "/FS/DSS", # old root path on hpss
102
102
  'OLDBROOT': "/DRDATA/DSS", # old backup tape on hpss
103
- 'GDEXUSER' : "gdexdata", # common gdex user name
104
- 'GDEXEMAIL' : "zji", # specialist to receipt email intead of common gdex user name
105
- 'SUDOGDEX' : 0, # 1 to allow sudo to PGLOG['GDEXUSER']
103
+ # COMMONUSER and ADMINUSER are set below via SETPGLOG (env overrides PG<KEY>)
104
+ 'SUDOGDEX' : 0, # 1 to allow sudo to PGLOG['COMMONUSER']
106
105
  'HOSTNAME' : '', # current host name the process in running on
107
106
  'OBJCTSTR' : "object",
108
107
  'BACKUPNM' : "quasar",
@@ -140,10 +139,22 @@ PGLOG = { # more defined in untaint_suid() with environment variables
140
139
  'EMLPORT' : 25
141
140
  }
142
141
 
143
- PGLOG['RDAUSER'] = PGLOG['GDEXUSER']
142
+ def SETPGLOG(key, default):
143
+ """Set ``PGLOG[key]`` from environment variable ``PG<key>`` or fall back
144
+ to ``default`` if the variable is unset. Used to make per-environment
145
+ overrides (e.g. PGCOMMONUSER, PGADMINUSER) survive package upgrades."""
146
+ PGLOG[key] = os.environ.get('PG' + key, default)
147
+
148
+ SETPGLOG("COMMONUSER", "gdexdata")
149
+ SETPGLOG("ADMINUSER", "zji")
150
+
151
+ PGLOG['RDAUSER'] = PGLOG['COMMONUSER']
144
152
  PGLOG['RDAGRP'] = PGLOG['GDEXGRP']
145
- PGLOG['RDAEMAIL'] = PGLOG['GDEXEMAIL']
153
+ PGLOG['RDAEMAIL'] = PGLOG['ADMINUSER']
146
154
  PGLOG['SUDORDA'] = PGLOG['SUDOGDEX']
155
+ # backwards-compat aliases (deprecated: use COMMONUSER / ADMINUSER)
156
+ PGLOG['GDEXUSER'] = PGLOG['COMMONUSER']
157
+ PGLOG['GDEXEMAIL'] = PGLOG['ADMINUSER']
147
158
 
148
159
  HOSTTYPES = {
149
160
  'rda' : 'dsg_mach',
@@ -317,15 +328,15 @@ def send_python_email(subject = None, receiver = None, msg = None, sender = None
317
328
  docc = False if cc else True
318
329
  if not sender:
319
330
  sender = PGLOG['CURUID']
320
- if sender != PGLOG['GDEXUSER']: docc = False
321
- if sender == PGLOG['GDEXUSER']: sender = PGLOG['GDEXEMAIL']
331
+ if sender != PGLOG['COMMONUSER']: docc = False
332
+ if sender == PGLOG['COMMONUSER']: sender = PGLOG['ADMINUSER']
322
333
  if sender.find('@') == -1: sender += "@ucar.edu"
323
334
  if not receiver:
324
335
  receiver = PGLOG['EMLADDR'] if PGLOG['EMLADDR'] else PGLOG['CURUID']
325
- if receiver == PGLOG['GDEXUSER']: receiver = PGLOG['GDEXEMAIL']
336
+ if receiver == PGLOG['COMMONUSER']: receiver = PGLOG['ADMINUSER']
326
337
  if receiver.find('@') == -1: receiver += "@ucar.edu"
327
338
 
328
- if docc and not re.match(PGLOG['GDEXUSER'], sender): add_carbon_copy(sender, 1)
339
+ if docc and not re.match(PGLOG['COMMONUSER'], sender): add_carbon_copy(sender, 1)
329
340
  emlmsg = EmailMessage()
330
341
  emlmsg.set_content(msg)
331
342
  emlmsg['From'] = sender
@@ -1167,6 +1178,10 @@ def get_command(cmdstr = None):
1167
1178
 
1168
1179
  if not cmdstr: cmdstr = sys.argv[0]
1169
1180
  cmdstr = op.basename(cmdstr)
1181
+ if cmdstr.startswith('setuid_'):
1182
+ euser = pwd.getpwuid(os.geteuid()).pw_name
1183
+ if euser == PGLOG['COMMONUSER']:
1184
+ cmdstr = cmdstr[len('setuid_'):]
1170
1185
  ms = re.match(r'^(.+)\.(py|pl)$', cmdstr)
1171
1186
  if ms:
1172
1187
  return ms.group(1)
@@ -1182,11 +1197,11 @@ def get_local_command(cmd, asuser = None):
1182
1197
  cuser = PGLOG['SETUID'] if PGLOG['SETUID'] else PGLOG['CURUID']
1183
1198
  if not asuser or cuser == asuser: return cmd
1184
1199
 
1185
- if cuser == PGLOG['GDEXUSER']:
1200
+ if cuser == PGLOG['COMMONUSER']:
1186
1201
  wrapper = "pgstart_" + asuser
1187
1202
  if valid_command(wrapper): return "{} {}".format(wrapper, cmd)
1188
- elif PGLOG['SUDOGDEX'] and asuser == PGLOG['GDEXUSER']:
1189
- return "sudo -u {} {}".format(PGLOG['GDEXUSER'], cmd) # sudo as user gdexdata
1203
+ elif PGLOG['SUDOGDEX'] and asuser == PGLOG['COMMONUSER']:
1204
+ return "sudo -u {} {}".format(PGLOG['COMMONUSER'], cmd) # sudo as user gdexdata
1190
1205
 
1191
1206
  return cmd
1192
1207
 
@@ -1209,12 +1224,12 @@ def get_hpss_command(cmd, asuser = None, hcmd = None):
1209
1224
  if not hcmd: hcmd = 'hsi'
1210
1225
 
1211
1226
  if asuser and cuser != asuser:
1212
- if cuser == PGLOG['GDEXUSER']:
1227
+ if cuser == PGLOG['COMMONUSER']:
1213
1228
  return "{} sudo -u {} {}".format(hcmd, asuser, cmd) # setuid wrapper as user asuser
1214
- elif PGLOG['SUDOGDEX'] and asuser == PGLOG['GDEXUSER']:
1215
- return "sudo -u {} {} {}".format(PGLOG['GDEXUSER'], hcmd, cmd) # sudo as user gdexdata
1229
+ elif PGLOG['SUDOGDEX'] and asuser == PGLOG['COMMONUSER']:
1230
+ return "sudo -u {} {} {}".format(PGLOG['COMMONUSER'], hcmd, cmd) # sudo as user gdexdata
1216
1231
 
1217
- if cuser != PGLOG['GDEXUSER']:
1232
+ if cuser != PGLOG['COMMONUSER']:
1218
1233
  if re.match(r'^ls ', cmd) and hcmd == 'hsi':
1219
1234
  return "hpss" + cmd # use 'hpssls' instead of 'hsi ls'
1220
1235
  elif re.match(r'^htar -tvf', hcmd):
@@ -1231,8 +1246,8 @@ def get_sync_command(host, asuser = None):
1231
1246
 
1232
1247
  host = get_short_host(host)
1233
1248
 
1234
- if (not (PGLOG['SETUID'] and PGLOG['SETUID'] == PGLOG['GDEXUSER']) and
1235
- (not asuser or asuser == PGLOG['GDEXUSER'])):
1249
+ if (not (PGLOG['SETUID'] and PGLOG['SETUID'] == PGLOG['COMMONUSER']) and
1250
+ (not asuser or asuser == PGLOG['COMMONUSER'])):
1236
1251
  return "sync" + host
1237
1252
 
1238
1253
  return host + "-sync"
@@ -1246,7 +1261,7 @@ def set_suid(cuid = 0):
1246
1261
  if cuid != PGLOG['EUID'] or cuid != PGLOG['RUID']:
1247
1262
  os.setreuid(cuid, cuid)
1248
1263
  PGLOG['SETUID'] = pwd.getpwuid(cuid).pw_name
1249
- if not (PGLOG['SETUID'] == PGLOG['GDEXUSER'] or cuid == PGLOG['RUID']):
1264
+ if not (PGLOG['SETUID'] == PGLOG['COMMONUSER'] or cuid == PGLOG['RUID']):
1250
1265
  set_specialist_environments(PGLOG['SETUID'])
1251
1266
  PGLOG['CURUID'] == PGLOG['SETUID'] # set CURUID to a specific specialist
1252
1267
 
@@ -1262,12 +1277,12 @@ def set_common_pglog():
1262
1277
  PGLOG['EUID'] = os.geteuid()
1263
1278
  PGLOG['CURUID'] = pwd.getpwuid(PGLOG['RUID']).pw_name
1264
1279
  try:
1265
- PGLOG['RDAUID'] = PGLOG['GDEXUID'] = pwd.getpwnam(PGLOG['GDEXUSER']).pw_uid
1280
+ PGLOG['RDAUID'] = PGLOG['GDEXUID'] = pwd.getpwnam(PGLOG['COMMONUSER']).pw_uid
1266
1281
  PGLOG['RDAGID'] = PGLOG['GDEXGID'] = grp.getgrnam(PGLOG['GDEXGRP']).gr_gid
1267
1282
  except:
1268
1283
  PGLOG['RDAUID'] = PGLOG['GDEXUID'] = 0
1269
1284
  PGLOG['RDAGID'] = PGLOG['GDEXGID'] = 0
1270
- if PGLOG['CURUID'] == PGLOG['GDEXUSER']: PGLOG['SETUID'] = PGLOG['GDEXUSER']
1285
+ if PGLOG['CURUID'] == PGLOG['COMMONUSER']: PGLOG['SETUID'] = PGLOG['COMMONUSER']
1271
1286
 
1272
1287
  PGLOG['HOSTNAME'] = get_host()
1273
1288
  for htype in HOSTTYPES:
@@ -624,7 +624,7 @@ def set_email_logact():
624
624
  def validate_dsowner(aname, dsid = None, logname = None, pgds = 0, logact = 0):
625
625
 
626
626
  if not logname: logname = (params['LN'] if 'LN' in params else PgLOG.PGLOG['CURUID'])
627
- if logname == PgLOG.PGLOG['GDEXUSER']: return 1
627
+ if logname == PgLOG.PGLOG['COMMONUSER']: return 1
628
628
 
629
629
  dsids = {}
630
630
  if dsid:
@@ -1638,7 +1638,7 @@ def send_request_email_notice(pgrqst, errmsg, fcount, rstat, readyfile = None, p
1638
1638
  exclude = (einfo['SENDER'] if errmsg else einfo['RECEIVER'])
1639
1639
  if not errmsg and pgcntl and pgcntl['ccemail']:
1640
1640
  PgLOG.add_carbon_copy(pgcntl['ccemail'], 1, exclude, pgrqst['specialist'])
1641
- if PgLOG.PGLOG['CURUID'] != pgrqst['specialist'] and PgLOG.PGLOG['CURUID'] != PgLOG.PGLOG['GDEXUSER']:
1641
+ if PgLOG.PGLOG['CURUID'] != pgrqst['specialist'] and PgLOG.PGLOG['CURUID'] != PgLOG.PGLOG['COMMONUSER']:
1642
1642
  PgLOG.add_carbon_copy(PgLOG.PGLOG['CURUID'], 1, exclude)
1643
1643
  if 'CC' in params: PgLOG.add_carbon_copy(params['CC'], 0, exclude)
1644
1644
  einfo['CCD'] = PgLOG.PGLOG['CCDADDR']
@@ -1089,7 +1089,7 @@ def record_background(bcmd, logact = PgLOG.LOGWRN):
1089
1089
  aname = bcmd
1090
1090
 
1091
1091
  mp = r"^\s*(\S+)\s+(\d+)\s+1\s+.*{}(.*)$".format(aname)
1092
- pc = "ps -u {},{} -f | grep ' 1 ' | grep {}".format(PgLOG.PGLOG['CURUID'], PgLOG.PGLOG['GDEXUSER'], aname)
1092
+ pc = "ps -u {},{} -f | grep ' 1 ' | grep {}".format(PgLOG.PGLOG['CURUID'], PgLOG.PGLOG['COMMONUSER'], aname)
1093
1093
  for i in range(2):
1094
1094
  buf = PgLOG.pgsystem(pc, logact, 20+1024)
1095
1095
  if buf:
@@ -1100,7 +1100,7 @@ def record_background(bcmd, logact = PgLOG.LOGWRN):
1100
1100
  (uid, sbid, acmd) = ms.groups()
1101
1101
  bid = int(sbid)
1102
1102
  if bid in CBIDS: return -1
1103
- if uid == PgLOG.PGLOG['GDEXUSER']:
1103
+ if uid == PgLOG.PGLOG['COMMONUSER']:
1104
1104
  acmd = re.sub(r'^\.(pl|py)\s+', '', acmd, 1)
1105
1105
  if re.match(r'^{}{}'.format(aname, acmd), bcmd): continue
1106
1106
  CBIDS[bid] = bcmd
@@ -22,7 +22,7 @@ object that existing callers expect.
22
22
 
23
23
  from . import PgLOG, PgUtil, PgDBI, PgFile, PgLock, PgCMD, PgSIG, PgOPT, PgSplit
24
24
 
25
- __version__ = "2.1.10"
25
+ __version__ = "3.0.0"
26
26
 
27
27
  __all__ = [
28
28
  "PgLOG",
@@ -13,24 +13,75 @@ import re
13
13
  import time
14
14
  import hvac
15
15
  from datetime import datetime
16
- import psycopg2 as PgSQL
17
- from psycopg2.extras import execute_values
18
- from psycopg2.extras import execute_batch
19
16
  from os import path as op
20
17
  from .pg_log import PgLOG
21
18
 
19
+ # Driver selection: prefer psycopg (v3) as the default driver; fall back to
20
+ # psycopg2 when psycopg is not installed. Both drivers share enough surface
21
+ # (connect(**config), Error/OperationalError, cursor.execute/executemany/
22
+ # fetchone/description, connection.commit/rollback/close/autocommit) for this
23
+ # module to use either transparently.
24
+ try:
25
+ import psycopg as PgSQL
26
+ PG_DRIVER = 'psycopg3'
27
+
28
+ # psycopg3 removed extras.execute_values / extras.execute_batch.
29
+ # cursor.executemany() in psycopg3 is efficient by default (it uses prepared
30
+ # statements internally), so the shims below provide matching signatures.
31
+
32
+ def execute_values(cursor, sql, argslist, page_size=100):
33
+ """Driver-neutral shim providing psycopg2.extras.execute_values() on psycopg3.
34
+
35
+ Replaces the single ``VALUES %s`` placeholder in ``sql`` (as expected by
36
+ psycopg2's execute_values) with a per-row ``VALUES (%s, %s, ...)`` tuple
37
+ built from the first row, then calls ``cursor.executemany()``. Lets the
38
+ same call site work under either psycopg (v3) or psycopg2.
39
+ """
40
+ if not argslist: return
41
+ ncol = len(argslist[0])
42
+ row_ph = '(' + ','.join(['%s']*ncol) + ')'
43
+ new_sql = re.sub(r'(?i)\bVALUES\s+%s\b', 'VALUES ' + row_ph, sql, count=1)
44
+ cursor.executemany(new_sql, argslist)
45
+
46
+ def execute_batch(cursor, sql, argslist, page_size=100):
47
+ """Driver-neutral shim providing psycopg2.extras.execute_batch() on psycopg3."""
48
+ cursor.executemany(sql, argslist)
49
+
50
+ def get_pgcode(pgerr):
51
+ """Return the 5-char SQLSTATE code from a psycopg3 error, or None."""
52
+ diag = getattr(pgerr, 'diag', None)
53
+ return getattr(diag, 'sqlstate', None) if diag is not None else None
54
+
55
+ def get_pgerror(pgerr):
56
+ """Return the primary error message from a psycopg3 error, or None."""
57
+ diag = getattr(pgerr, 'diag', None)
58
+ return getattr(diag, 'message_primary', None) if diag is not None else None
59
+ except ImportError:
60
+ import psycopg2 as PgSQL
61
+ from psycopg2.extras import execute_values, execute_batch
62
+ PG_DRIVER = 'psycopg2'
63
+
64
+ def get_pgcode(pgerr):
65
+ """Return the 5-char SQLSTATE code from a psycopg2 error, or None."""
66
+ return getattr(pgerr, 'pgcode', None)
67
+
68
+ def get_pgerror(pgerr):
69
+ """Return the server error message from a psycopg2 error, or None."""
70
+ return getattr(pgerr, 'pgerror', None)
71
+
22
72
  class PgDBI(PgLOG):
23
73
  """PostgreSQL Database Interface layer extending PgLOG.
24
74
 
25
75
  Provides a high-level API for connecting to and querying PostgreSQL databases
26
- using psycopg2. Supports single and batch INSERT, SELECT, UPDATE, and DELETE
27
- operations, transaction management, schema introspection, user lookups, usage
28
- tracking, and credential retrieval from .pgpass or OpenBao.
76
+ using psycopg (v3) when available, falling back to psycopg2. Supports single
77
+ and batch INSERT, SELECT, UPDATE, and DELETE operations, transaction
78
+ management, schema introspection, user lookups, usage tracking, and
79
+ credential retrieval from .pgpass or OpenBao.
29
80
 
30
81
  Inherits all logging and utility helpers from PgLOG.
31
82
 
32
83
  Instance Attributes:
33
- pgdb (connection | None): Active psycopg2 connection, or None when disconnected.
84
+ pgdb (connection | None): Active psycopg/psycopg2 connection, or None when disconnected.
34
85
  curtran (int): Transaction counter: 0 = idle, >0 = inside a transaction.
35
86
  NMISSES (list): Cached list of scientist IDs (userno) not found in the DB.
36
87
  LMISSES (list): Cached list of login names not found in the DB.
@@ -40,7 +91,8 @@ class PgDBI(PgLOG):
40
91
  SYSDOWN (dict): Cache of system-down status records keyed by hostname.
41
92
  PGDBI (dict): Active connection and configuration parameters.
42
93
  PGSIGNS (list): Special comparison sign tokens recognised by get_field_condition().
43
- CHCODE (int): psycopg2 type code for CHAR columns (used to strip trailing spaces).
94
+ CHCODE (int): Driver type code for CHAR columns (used to strip trailing spaces);
95
+ set from psycopg/psycopg2 depending on which driver is in use.
44
96
  DBPORTS (dict): Mapping of database names to non-default TCP port numbers.
45
97
  DBPASS (dict): Credentials loaded from .pgpass, keyed by (host, port, db, user).
46
98
  DBBAOS (dict): Credentials loaded from OpenBao, keyed by database name.
@@ -544,14 +596,15 @@ class PgDBI(PgLOG):
544
596
  return tbname
545
597
 
546
598
  def check_dberror(self, pgerr, pgcnt, sqlstr, ary, logact = None):
547
- """Classify a psycopg2 error and decide whether to retry or abort.
599
+ """Classify a psycopg/psycopg2 error and decide whether to retry or abort.
548
600
 
549
601
  Handles connection errors (08xxx, 57xxx), lock errors (55xxx), aborted
550
602
  transactions (25P02), and missing-table errors (42P01 with ADDTBL flag).
551
603
  Retries up to PGLOG['DBRETRY'] times; exits after that threshold.
552
604
 
553
605
  Args:
554
- pgerr (psycopg2.Error): The caught database exception.
606
+ pgerr (PgSQL.Error): The caught database exception
607
+ (psycopg.Error or psycopg2.Error).
555
608
  pgcnt (int): Current retry count (0-based).
556
609
  sqlstr (str): SQL statement that caused the error, for logging.
557
610
  ary: Bound values that were passed to the statement, for logging.
@@ -562,8 +615,8 @@ class PgDBI(PgLOG):
562
615
  """
563
616
  if logact is None: logact = self.PGDBI['ERRLOG']
564
617
  ret = self.FAILURE
565
- pgcode = pgerr.pgcode
566
- pgerror = pgerr.pgerror
618
+ pgcode = get_pgcode(pgerr)
619
+ pgerror = get_pgerror(pgerr)
567
620
  dberror = "{} {}".format(pgcode, pgerror) if pgcode and pgerror else str(pgerr)
568
621
  if pgcnt < self.PGLOG['DBRETRY']:
569
622
  if not pgcode:
@@ -640,15 +693,15 @@ class PgDBI(PgLOG):
640
693
  autocommit (bool): Whether to enable autocommit on the new connection.
641
694
 
642
695
  Returns:
643
- connection | int: psycopg2 connection on success, self.FAILURE on error.
696
+ connection | int: psycopg/psycopg2 connection on success, self.FAILURE on error.
644
697
  """
645
698
  if self.pgdb:
646
699
  if reconnect and not self.pgdb.closed: return self.pgdb # no need reconnect
647
700
  elif reconnect:
648
701
  reconnect = 0 # initial connection
649
702
  while True:
650
- config = {'database': self.PGDBI['DBNAME'],
651
- 'user': self.PGDBI['LNNAME']}
703
+ config = {'dbname': self.PGDBI['DBNAME'],
704
+ 'user': self.PGDBI['LNNAME']}
652
705
  if self.PGDBI['DBSHOST'] == self.PGLOG['HOSTNAME']:
653
706
  config['host'] = 'localhost'
654
707
  else:
@@ -656,7 +709,7 @@ class PgDBI(PgLOG):
656
709
  if not self.PGDBI['DBPORT']: self.PGDBI['DBPORT'] = self.get_dbport(self.PGDBI['DBNAME'])
657
710
  if self.PGDBI['DBPORT']: config['port'] = self.PGDBI['DBPORT']
658
711
  config['password'] = '***'
659
- sqlstr = "psycopg2.connect(**{})".format(config)
712
+ sqlstr = "{}.connect(**{})".format(PG_DRIVER, config)
660
713
  config['password'] = self.get_pgpass_password()
661
714
  if self.PGLOG['DBGLEVEL']: self.pgdbg(1000, sqlstr)
662
715
  try:
@@ -675,7 +728,7 @@ class PgDBI(PgLOG):
675
728
  errors. The search path includes PGDBI['SCPATH'] when it differs from SCNAME.
676
729
 
677
730
  Returns:
678
- cursor | int: psycopg2 cursor on success, self.FAILURE on error.
731
+ cursor | int: psycopg/psycopg2 cursor on success, self.FAILURE on error.
679
732
  """
680
733
  pgcur = None
681
734
  if not self.pgdb:
@@ -924,7 +977,8 @@ class PgDBI(PgLOG):
924
977
  """Insert multiple records into a database table efficiently.
925
978
 
926
979
  When getid is set, executes individual inserts to capture each returned ID.
927
- Otherwise uses psycopg2 execute_values() for a single bulk INSERT.
980
+ Otherwise uses execute_values() (psycopg2's bulk helper, or the
981
+ executemany()-based shim on psycopg v3) for a single bulk INSERT.
928
982
 
929
983
  Args:
930
984
  tablename (str): Target table name.
@@ -1365,8 +1419,9 @@ class PgDBI(PgLOG):
1365
1419
  def pgmupdt(self, tablename, records, cnddicts, logact = None):
1366
1420
  """Update multiple rows using parallel value and condition dicts.
1367
1421
 
1368
- Uses psycopg2 execute_batch() for efficient bulk updates. The number of
1369
- values in records and cnddicts must match.
1422
+ Uses execute_batch() (psycopg2's bulk helper, or the executemany()-based
1423
+ shim on psycopg v3) for efficient bulk updates. The number of values in
1424
+ records and cnddicts must match.
1370
1425
 
1371
1426
  Args:
1372
1427
  tablename (str): Target table name.
@@ -1508,7 +1563,8 @@ class PgDBI(PgLOG):
1508
1563
  def pgmdel(self, tablename, cnddicts, logact = None):
1509
1564
  """Delete multiple rows using a multi-value condition dict.
1510
1565
 
1511
- Uses psycopg2 execute_batch() for efficient bulk deletes.
1566
+ Uses execute_batch() (psycopg2's bulk helper, or the executemany()-based
1567
+ shim on psycopg v3) for efficient bulk deletes.
1512
1568
 
1513
1569
  Args:
1514
1570
  tablename (str): Target table name.
@@ -19,6 +19,7 @@ import re
19
19
  import time
20
20
  import glob
21
21
  import json
22
+ import hashlib
22
23
  from .pg_util import PgUtil
23
24
  from .pg_sig import PgSIG
24
25
 
@@ -986,7 +987,7 @@ class PgFile(PgUtil, PgSIG):
986
987
  return self.FINISH
987
988
  return self.FAILURE
988
989
 
989
- # reset local file/directory information to make them writable for self.PGLOG['GDEXUSER']
990
+ # reset local file/directory information to make them writable for self.PGLOG['COMMONUSER']
990
991
  # file - file name (mandatory)
991
992
  # info - gathered file info with option 14, None means file not exists
992
993
  def reset_local_info(self, file, info = None, logact = 0):
@@ -2874,24 +2875,32 @@ class PgFile(PgUtil, PgSIG):
2874
2875
  (with None for missing files) for multiple files, or None
2875
2876
  on failure.
2876
2877
  """
2877
- cmd = 'md5sum '
2878
2878
  if count > 0:
2879
2879
  checksum = [None]*count
2880
2880
  for i in range(count):
2881
2881
  if op.isfile(file[i]):
2882
- chksm = self.pgsystem(cmd + file[i], logact, 20)
2883
- if chksm:
2884
- ms = re.search(r'(\w{32})', chksm)
2885
- if ms: checksum[i] = ms.group(1)
2882
+ checksum[i] = self._file_md5(file[i], logact)
2886
2883
  else:
2887
2884
  checksum = None
2888
2885
  if op.isfile(file):
2889
- chksm = self.pgsystem(cmd + file, logact, 20)
2890
- if chksm:
2891
- ms = re.search(r'(\w{32})', chksm)
2892
- if ms: checksum = ms.group(1)
2886
+ checksum = self._file_md5(file, logact)
2893
2887
  return checksum
2894
2888
 
2889
+ def _file_md5(self, path, logact=0):
2890
+ """Compute MD5 hex digest of *path*, reading in 1 MiB chunks.
2891
+
2892
+ Returns the hex digest string, or None on read error.
2893
+ """
2894
+ try:
2895
+ h = hashlib.md5()
2896
+ with open(path, 'rb') as fh:
2897
+ for chunk in iter(lambda: fh.read(1048576), b''):
2898
+ h.update(chunk)
2899
+ return h.hexdigest()
2900
+ except OSError as e:
2901
+ self.pglog("Error md5sum {}: {}".format(path, str(e)), logact)
2902
+ return None
2903
+
2895
2904
  # Evaluate md5 checksums and compare them for two given files
2896
2905
  # file1, file2: file names
2897
2906
  # Return: 0 if same and 1 if not