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.
- rda_python_common/PgDBI.py +48 -7
- rda_python_common/PgFile.py +1 -1
- rda_python_common/PgLOG.py +37 -22
- rda_python_common/PgOPT.py +2 -2
- rda_python_common/PgSIG.py +2 -2
- rda_python_common/__init__.py +1 -1
- rda_python_common/pg_dbi.py +77 -21
- rda_python_common/pg_file.py +19 -10
- rda_python_common/pg_log.py +54 -33
- rda_python_common/pg_opt.py +2 -2
- rda_python_common/pg_sig.py +96 -81
- rda_python_common/pg_util.py +36 -89
- {rda_python_common-2.1.10.dist-info → rda_python_common-3.0.0.dist-info}/METADATA +125 -25
- rda_python_common-3.0.0.dist-info/RECORD +28 -0
- rda_python_common-2.1.10.dist-info/RECORD +0 -28
- {rda_python_common-2.1.10.dist-info → rda_python_common-3.0.0.dist-info}/WHEEL +0 -0
- {rda_python_common-2.1.10.dist-info → rda_python_common-3.0.0.dist-info}/entry_points.txt +0 -0
- {rda_python_common-2.1.10.dist-info → rda_python_common-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {rda_python_common-2.1.10.dist-info → rda_python_common-3.0.0.dist-info}/top_level.txt +0 -0
rda_python_common/PgDBI.py
CHANGED
|
@@ -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
|
|
443
|
-
pgerror = pgerr
|
|
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 = {'
|
|
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 = "
|
|
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:
|
rda_python_common/PgFile.py
CHANGED
|
@@ -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['
|
|
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
|
#
|
rda_python_common/PgLOG.py
CHANGED
|
@@ -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['
|
|
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
|
-
|
|
104
|
-
'
|
|
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
|
-
|
|
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['
|
|
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['
|
|
321
|
-
if sender == PGLOG['
|
|
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['
|
|
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['
|
|
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['
|
|
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['
|
|
1189
|
-
return "sudo -u {} {}".format(PGLOG['
|
|
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['
|
|
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['
|
|
1215
|
-
return "sudo -u {} {} {}".format(PGLOG['
|
|
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['
|
|
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['
|
|
1235
|
-
(not asuser or asuser == PGLOG['
|
|
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['
|
|
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['
|
|
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['
|
|
1285
|
+
if PGLOG['CURUID'] == PGLOG['COMMONUSER']: PGLOG['SETUID'] = PGLOG['COMMONUSER']
|
|
1271
1286
|
|
|
1272
1287
|
PGLOG['HOSTNAME'] = get_host()
|
|
1273
1288
|
for htype in HOSTTYPES:
|
rda_python_common/PgOPT.py
CHANGED
|
@@ -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['
|
|
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['
|
|
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']
|
rda_python_common/PgSIG.py
CHANGED
|
@@ -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['
|
|
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['
|
|
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
|
rda_python_common/__init__.py
CHANGED
rda_python_common/pg_dbi.py
CHANGED
|
@@ -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
|
|
27
|
-
|
|
28
|
-
|
|
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):
|
|
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 (
|
|
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
|
|
566
|
-
pgerror = pgerr
|
|
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 = {'
|
|
651
|
-
|
|
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 = "
|
|
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
|
|
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
|
|
1369
|
-
|
|
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
|
|
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.
|
rda_python_common/pg_file.py
CHANGED
|
@@ -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['
|
|
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
|
-
|
|
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
|
-
|
|
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
|