rda-python-common 2.1.11__py3-none-any.whl → 3.0.1__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 +54 -9
- 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 +88 -24
- rda_python_common/pg_file.py +1 -1
- rda_python_common/pg_log.py +46 -23
- rda_python_common/pg_opt.py +2 -2
- rda_python_common/pg_sig.py +3 -3
- rda_python_common/pg_util.py +36 -89
- {rda_python_common-2.1.11.dist-info → rda_python_common-3.0.1.dist-info}/METADATA +124 -24
- rda_python_common-3.0.1.dist-info/RECORD +28 -0
- rda_python_common-2.1.11.dist-info/RECORD +0 -28
- {rda_python_common-2.1.11.dist-info → rda_python_common-3.0.1.dist-info}/WHEEL +0 -0
- {rda_python_common-2.1.11.dist-info → rda_python_common-3.0.1.dist-info}/entry_points.txt +0 -0
- {rda_python_common-2.1.11.dist-info → rda_python_common-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {rda_python_common-2.1.11.dist-info → rda_python_common-3.0.1.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
|
|
@@ -287,8 +328,12 @@ def starttran():
|
|
|
287
328
|
pgconnect(0, 0, False)
|
|
288
329
|
else:
|
|
289
330
|
try:
|
|
290
|
-
|
|
291
|
-
|
|
331
|
+
# Liveness probe: psycopg2's isolation_level getter could raise on a
|
|
332
|
+
# dead connection, but psycopg3's is a cached attribute that never
|
|
333
|
+
# touches the server. A trivial round-trip detects a broken
|
|
334
|
+
# connection under either driver.
|
|
335
|
+
pgdb.cursor().execute("SELECT 1")
|
|
336
|
+
except PgSQL.Error as e:
|
|
292
337
|
pgconnect(0, 0, False)
|
|
293
338
|
if pgdb.closed:
|
|
294
339
|
pgconnect(0, 0, False)
|
|
@@ -439,8 +484,8 @@ def check_dberror(pgerr, pgcnt, sqlstr, ary, logact = PGDBI['ERRLOG']):
|
|
|
439
484
|
|
|
440
485
|
ret = PgLOG.FAILURE
|
|
441
486
|
|
|
442
|
-
pgcode = pgerr
|
|
443
|
-
pgerror = pgerr
|
|
487
|
+
pgcode = get_pgcode(pgerr)
|
|
488
|
+
pgerror = get_pgerror(pgerr)
|
|
444
489
|
dberror = "{} {}".format(pgcode, pgerror) if pgcode and pgerror else str(pgerr)
|
|
445
490
|
if pgcnt < PgLOG.PGLOG['DBRETRY']:
|
|
446
491
|
if not pgcode:
|
|
@@ -517,7 +562,7 @@ def pgconnect(reconnect = 0, pgcnt = 0, autocommit = True):
|
|
|
517
562
|
reconnect = 0 # initial connection
|
|
518
563
|
|
|
519
564
|
while True:
|
|
520
|
-
config = {'
|
|
565
|
+
config = {'dbname' : PGDBI['DBNAME'],
|
|
521
566
|
'user' : PGDBI['LNNAME']}
|
|
522
567
|
if PGDBI['DBSHOST'] == PgLOG.PGLOG['HOSTNAME']:
|
|
523
568
|
config['host'] = 'localhost'
|
|
@@ -526,7 +571,7 @@ def pgconnect(reconnect = 0, pgcnt = 0, autocommit = True):
|
|
|
526
571
|
if not PGDBI['DBPORT']: PGDBI['DBPORT'] = get_dbport(PGDBI['DBNAME'])
|
|
527
572
|
if PGDBI['DBPORT']: config['port'] = PGDBI['DBPORT']
|
|
528
573
|
config['password'] = '***'
|
|
529
|
-
sqlstr = "
|
|
574
|
+
sqlstr = "{}.connect(**{})".format(PG_DRIVER, config)
|
|
530
575
|
config['password'] = get_pgpass_password()
|
|
531
576
|
if PgLOG.PGLOG['DBGLEVEL']: PgLOG.pgdbg(1000, sqlstr)
|
|
532
577
|
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.
|
|
@@ -358,8 +410,12 @@ class PgDBI(PgLOG):
|
|
|
358
410
|
self.pgconnect(0, 0, False)
|
|
359
411
|
else:
|
|
360
412
|
try:
|
|
361
|
-
|
|
362
|
-
|
|
413
|
+
# Liveness probe: psycopg2's isolation_level getter could raise on a
|
|
414
|
+
# dead connection, but psycopg3's is a cached attribute that never
|
|
415
|
+
# touches the server. A trivial round-trip detects a broken
|
|
416
|
+
# connection under either driver.
|
|
417
|
+
self.pgdb.cursor().execute("SELECT 1")
|
|
418
|
+
except PgSQL.Error as e:
|
|
363
419
|
self.pgconnect(0, 0, False)
|
|
364
420
|
if self.pgdb.closed:
|
|
365
421
|
self.pgconnect(0, 0, False)
|
|
@@ -481,7 +537,11 @@ class PgDBI(PgLOG):
|
|
|
481
537
|
dberror (str): Full database error string to inspect.
|
|
482
538
|
logact (int): Logging action flags forwarded to add_new_table().
|
|
483
539
|
"""
|
|
484
|
-
|
|
540
|
+
# The caller (check_dberror) only invokes this after pgcode == '42P01', so
|
|
541
|
+
# match just the relation message. psycopg2's pgerror includes the
|
|
542
|
+
# 'ERROR: ' severity prefix while psycopg3's message_primary does not, so
|
|
543
|
+
# anchoring on that prefix only matched under psycopg2.
|
|
544
|
+
ms = re.search(r'relation "(.+)" does not exist', dberror)
|
|
485
545
|
if ms:
|
|
486
546
|
tname = ms.group(1)
|
|
487
547
|
self.add_new_table(tname, logact = logact)
|
|
@@ -544,14 +604,15 @@ class PgDBI(PgLOG):
|
|
|
544
604
|
return tbname
|
|
545
605
|
|
|
546
606
|
def check_dberror(self, pgerr, pgcnt, sqlstr, ary, logact = None):
|
|
547
|
-
"""Classify a psycopg2 error and decide whether to retry or abort.
|
|
607
|
+
"""Classify a psycopg/psycopg2 error and decide whether to retry or abort.
|
|
548
608
|
|
|
549
609
|
Handles connection errors (08xxx, 57xxx), lock errors (55xxx), aborted
|
|
550
610
|
transactions (25P02), and missing-table errors (42P01 with ADDTBL flag).
|
|
551
611
|
Retries up to PGLOG['DBRETRY'] times; exits after that threshold.
|
|
552
612
|
|
|
553
613
|
Args:
|
|
554
|
-
pgerr (
|
|
614
|
+
pgerr (PgSQL.Error): The caught database exception
|
|
615
|
+
(psycopg.Error or psycopg2.Error).
|
|
555
616
|
pgcnt (int): Current retry count (0-based).
|
|
556
617
|
sqlstr (str): SQL statement that caused the error, for logging.
|
|
557
618
|
ary: Bound values that were passed to the statement, for logging.
|
|
@@ -562,8 +623,8 @@ class PgDBI(PgLOG):
|
|
|
562
623
|
"""
|
|
563
624
|
if logact is None: logact = self.PGDBI['ERRLOG']
|
|
564
625
|
ret = self.FAILURE
|
|
565
|
-
pgcode = pgerr
|
|
566
|
-
pgerror = pgerr
|
|
626
|
+
pgcode = get_pgcode(pgerr)
|
|
627
|
+
pgerror = get_pgerror(pgerr)
|
|
567
628
|
dberror = "{} {}".format(pgcode, pgerror) if pgcode and pgerror else str(pgerr)
|
|
568
629
|
if pgcnt < self.PGLOG['DBRETRY']:
|
|
569
630
|
if not pgcode:
|
|
@@ -640,15 +701,15 @@ class PgDBI(PgLOG):
|
|
|
640
701
|
autocommit (bool): Whether to enable autocommit on the new connection.
|
|
641
702
|
|
|
642
703
|
Returns:
|
|
643
|
-
connection | int: psycopg2 connection on success, self.FAILURE on error.
|
|
704
|
+
connection | int: psycopg/psycopg2 connection on success, self.FAILURE on error.
|
|
644
705
|
"""
|
|
645
706
|
if self.pgdb:
|
|
646
707
|
if reconnect and not self.pgdb.closed: return self.pgdb # no need reconnect
|
|
647
708
|
elif reconnect:
|
|
648
709
|
reconnect = 0 # initial connection
|
|
649
710
|
while True:
|
|
650
|
-
config = {'
|
|
651
|
-
|
|
711
|
+
config = {'dbname': self.PGDBI['DBNAME'],
|
|
712
|
+
'user': self.PGDBI['LNNAME']}
|
|
652
713
|
if self.PGDBI['DBSHOST'] == self.PGLOG['HOSTNAME']:
|
|
653
714
|
config['host'] = 'localhost'
|
|
654
715
|
else:
|
|
@@ -656,7 +717,7 @@ class PgDBI(PgLOG):
|
|
|
656
717
|
if not self.PGDBI['DBPORT']: self.PGDBI['DBPORT'] = self.get_dbport(self.PGDBI['DBNAME'])
|
|
657
718
|
if self.PGDBI['DBPORT']: config['port'] = self.PGDBI['DBPORT']
|
|
658
719
|
config['password'] = '***'
|
|
659
|
-
sqlstr = "
|
|
720
|
+
sqlstr = "{}.connect(**{})".format(PG_DRIVER, config)
|
|
660
721
|
config['password'] = self.get_pgpass_password()
|
|
661
722
|
if self.PGLOG['DBGLEVEL']: self.pgdbg(1000, sqlstr)
|
|
662
723
|
try:
|
|
@@ -675,7 +736,7 @@ class PgDBI(PgLOG):
|
|
|
675
736
|
errors. The search path includes PGDBI['SCPATH'] when it differs from SCNAME.
|
|
676
737
|
|
|
677
738
|
Returns:
|
|
678
|
-
cursor | int: psycopg2 cursor on success, self.FAILURE on error.
|
|
739
|
+
cursor | int: psycopg/psycopg2 cursor on success, self.FAILURE on error.
|
|
679
740
|
"""
|
|
680
741
|
pgcur = None
|
|
681
742
|
if not self.pgdb:
|
|
@@ -924,7 +985,8 @@ class PgDBI(PgLOG):
|
|
|
924
985
|
"""Insert multiple records into a database table efficiently.
|
|
925
986
|
|
|
926
987
|
When getid is set, executes individual inserts to capture each returned ID.
|
|
927
|
-
Otherwise uses
|
|
988
|
+
Otherwise uses execute_values() (psycopg2's bulk helper, or the
|
|
989
|
+
executemany()-based shim on psycopg v3) for a single bulk INSERT.
|
|
928
990
|
|
|
929
991
|
Args:
|
|
930
992
|
tablename (str): Target table name.
|
|
@@ -1365,8 +1427,9 @@ class PgDBI(PgLOG):
|
|
|
1365
1427
|
def pgmupdt(self, tablename, records, cnddicts, logact = None):
|
|
1366
1428
|
"""Update multiple rows using parallel value and condition dicts.
|
|
1367
1429
|
|
|
1368
|
-
Uses
|
|
1369
|
-
|
|
1430
|
+
Uses execute_batch() (psycopg2's bulk helper, or the executemany()-based
|
|
1431
|
+
shim on psycopg v3) for efficient bulk updates. The number of values in
|
|
1432
|
+
records and cnddicts must match.
|
|
1370
1433
|
|
|
1371
1434
|
Args:
|
|
1372
1435
|
tablename (str): Target table name.
|
|
@@ -1508,7 +1571,8 @@ class PgDBI(PgLOG):
|
|
|
1508
1571
|
def pgmdel(self, tablename, cnddicts, logact = None):
|
|
1509
1572
|
"""Delete multiple rows using a multi-value condition dict.
|
|
1510
1573
|
|
|
1511
|
-
Uses
|
|
1574
|
+
Uses execute_batch() (psycopg2's bulk helper, or the executemany()-based
|
|
1575
|
+
shim on psycopg v3) for efficient bulk deletes.
|
|
1512
1576
|
|
|
1513
1577
|
Args:
|
|
1514
1578
|
tablename (str): Target table name.
|
rda_python_common/pg_file.py
CHANGED
|
@@ -987,7 +987,7 @@ class PgFile(PgUtil, PgSIG):
|
|
|
987
987
|
return self.FINISH
|
|
988
988
|
return self.FAILURE
|
|
989
989
|
|
|
990
|
-
# 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']
|
|
991
991
|
# file - file name (mandatory)
|
|
992
992
|
# info - gathered file info with option 14, None means file not exists
|
|
993
993
|
def reset_local_info(self, file, info = None, logact = 0):
|
rda_python_common/pg_log.py
CHANGED
|
@@ -81,7 +81,7 @@ class PgLOG:
|
|
|
81
81
|
EMLSUM = (0x08000) # record as email summary
|
|
82
82
|
EMEROL = (0x10000) # record error as email only
|
|
83
83
|
EMLALL = (0x1D208) # all email acts
|
|
84
|
-
DOSUDO = (0x20000) # add 'sudo -u self.PGLOG['
|
|
84
|
+
DOSUDO = (0x20000) # add 'sudo -u self.PGLOG['COMMONUSER']'
|
|
85
85
|
NOTLOG = (0x40000) # do not log any thing
|
|
86
86
|
OVRIDE = (0x80000) # do override existing file or record
|
|
87
87
|
NOWAIT = (0x100000) # do not wait on globus task to finish
|
|
@@ -124,9 +124,8 @@ class PgLOG:
|
|
|
124
124
|
'SETUID': '', # the login name for suid if it is different to the CURUID
|
|
125
125
|
'FILEMODE': 0o664, # default 8-base file mode
|
|
126
126
|
'EXECMODE': 0o775, # default 8-base executable file mode or directory mode
|
|
127
|
-
|
|
128
|
-
'
|
|
129
|
-
'SUDOGDEX': 0, # 1 to allow sudo to self.PGLOG['GDEXUSER']
|
|
127
|
+
# COMMONUSER and ADMINUSER are set below via SETPGLOG (env overrides PG<KEY>)
|
|
128
|
+
'SUDOGDEX': 0, # 1 to allow sudo to self.PGLOG['COMMONUSER']
|
|
130
129
|
'HOSTNAME': '', # current host name the process in running on
|
|
131
130
|
'OBJCTSTR': "object",
|
|
132
131
|
'BACKUPNM': "quasar",
|
|
@@ -161,10 +160,6 @@ class PgLOG:
|
|
|
161
160
|
'EMLSRVR': "ndir.ucar.edu", # UCAR email server and port
|
|
162
161
|
'EMLPORT': 25
|
|
163
162
|
}
|
|
164
|
-
self.PGLOG['RDAUSER'] = self.PGLOG['GDEXUSER']
|
|
165
|
-
self.PGLOG['RDAGRP'] = self.PGLOG['GDEXGRP']
|
|
166
|
-
self.PGLOG['RDAEMAIL'] = self.PGLOG['GDEXEMAIL']
|
|
167
|
-
self.PGLOG['SUDORDA'] = self.PGLOG['SUDOGDEX']
|
|
168
163
|
self.HOSTTYPES = {
|
|
169
164
|
'rda': 'dsg_mach',
|
|
170
165
|
'crlogin': 'dav',
|
|
@@ -188,6 +183,13 @@ class PgLOG:
|
|
|
188
183
|
self.set_common_pglog()
|
|
189
184
|
self.OUTPUT = None
|
|
190
185
|
|
|
186
|
+
def SETPGLOG(self, key, default):
|
|
187
|
+
"""Set ``self.PGLOG[key]`` from environment variable ``PG<key>`` or
|
|
188
|
+
fall back to ``default`` if the variable is unset. Used to make
|
|
189
|
+
per-environment overrides (e.g. PGCOMMONUSER, PGADMINUSER) survive
|
|
190
|
+
package upgrades."""
|
|
191
|
+
self.PGLOG[key] = os.environ.get('PG' + key, default)
|
|
192
|
+
|
|
191
193
|
def open_output(self, outfile=None):
|
|
192
194
|
"""Open the result output destination.
|
|
193
195
|
|
|
@@ -392,14 +394,14 @@ class PgLOG:
|
|
|
392
394
|
docc = False if cc else True
|
|
393
395
|
if not sender:
|
|
394
396
|
sender = self.PGLOG['CURUID']
|
|
395
|
-
if sender != self.PGLOG['
|
|
396
|
-
if sender == self.PGLOG['
|
|
397
|
+
if sender != self.PGLOG['COMMONUSER']: docc = False
|
|
398
|
+
if sender == self.PGLOG['COMMONUSER']: sender = self.PGLOG['ADMINUSER']
|
|
397
399
|
if sender.find('@') == -1: sender += "@ucar.edu"
|
|
398
400
|
if not receiver:
|
|
399
401
|
receiver = self.PGLOG['EMLADDR'] if self.PGLOG['EMLADDR'] else self.PGLOG['CURUID']
|
|
400
|
-
if receiver == self.PGLOG['
|
|
402
|
+
if receiver == self.PGLOG['COMMONUSER']: receiver = self.PGLOG['ADMINUSER']
|
|
401
403
|
if receiver.find('@') == -1: receiver += "@ucar.edu"
|
|
402
|
-
if docc and not re.match(self.PGLOG['
|
|
404
|
+
if docc and not re.match(self.PGLOG['COMMONUSER'], sender): self.add_carbon_copy(sender, 1)
|
|
403
405
|
emlmsg = EmailMessage()
|
|
404
406
|
emlmsg.set_content(msg)
|
|
405
407
|
emlmsg['From'] = sender
|
|
@@ -1324,10 +1326,17 @@ class PgLOG:
|
|
|
1324
1326
|
self.PGLOG['PGBATCH'] = ''
|
|
1325
1327
|
self.PGLOG['CURBID'] = 0
|
|
1326
1328
|
|
|
1327
|
-
|
|
1328
|
-
def get_command(cmdstr=None):
|
|
1329
|
+
def get_command(self, cmdstr=None):
|
|
1329
1330
|
"""Return the base command name, stripping directory and ``.py``/``.pl`` extension.
|
|
1330
1331
|
|
|
1332
|
+
When invoked via the pywrapper setuid C wrapper, ``sys.argv[0]`` is the
|
|
1333
|
+
resolved Python script path (e.g. ``/.../setuid_rdacp``) rather than the
|
|
1334
|
+
alias the user typed (e.g. ``rdacp``); the kernel discards argv[0] when
|
|
1335
|
+
handling the script's shebang. When the basename starts with
|
|
1336
|
+
``setuid_`` and the effective user equals ``self.PGLOG['COMMONUSER']``,
|
|
1337
|
+
this process was started via pywrapper, so the ``setuid_`` prefix is
|
|
1338
|
+
stripped to recover the logical command name.
|
|
1339
|
+
|
|
1331
1340
|
Args:
|
|
1332
1341
|
cmdstr: Path string. Defaults to ``sys.argv[0]``.
|
|
1333
1342
|
|
|
@@ -1336,6 +1345,10 @@ class PgLOG:
|
|
|
1336
1345
|
"""
|
|
1337
1346
|
if not cmdstr: cmdstr = sys.argv[0]
|
|
1338
1347
|
cmdstr = op.basename(cmdstr)
|
|
1348
|
+
if cmdstr.startswith('setuid_'):
|
|
1349
|
+
euser = pwd.getpwuid(os.geteuid()).pw_name
|
|
1350
|
+
if euser == self.PGLOG['COMMONUSER']:
|
|
1351
|
+
cmdstr = cmdstr[len('setuid_'):]
|
|
1339
1352
|
ms = re.match(r'^(.+)\.(py|pl)$', cmdstr)
|
|
1340
1353
|
if ms:
|
|
1341
1354
|
return ms.group(1)
|
|
@@ -1358,11 +1371,11 @@ class PgLOG:
|
|
|
1358
1371
|
"""
|
|
1359
1372
|
cuser = self.PGLOG['SETUID'] if self.PGLOG['SETUID'] else self.PGLOG['CURUID']
|
|
1360
1373
|
if not asuser or cuser == asuser: return cmd
|
|
1361
|
-
if cuser == self.PGLOG['
|
|
1374
|
+
if cuser == self.PGLOG['COMMONUSER']:
|
|
1362
1375
|
wrapper = "pgstart_" + asuser
|
|
1363
1376
|
if self.valid_command(wrapper): return "{} {}".format(wrapper, cmd)
|
|
1364
|
-
elif self.PGLOG['SUDOGDEX'] and asuser == self.PGLOG['
|
|
1365
|
-
return "sudo -u {} {}".format(self.PGLOG['
|
|
1377
|
+
elif self.PGLOG['SUDOGDEX'] and asuser == self.PGLOG['COMMONUSER']:
|
|
1378
|
+
return "sudo -u {} {}".format(self.PGLOG['COMMONUSER'], cmd) # sudo as user gdexdata
|
|
1366
1379
|
return cmd
|
|
1367
1380
|
|
|
1368
1381
|
def get_remote_command(self, cmd, host, asuser=None):
|
|
@@ -1389,8 +1402,8 @@ class PgLOG:
|
|
|
1389
1402
|
Sync command string (e.g. ``"synccasper"`` or ``"casper-sync"``).
|
|
1390
1403
|
"""
|
|
1391
1404
|
host = self.get_short_host(host)
|
|
1392
|
-
if (not (self.PGLOG['SETUID'] and self.PGLOG['SETUID'] == self.PGLOG['
|
|
1393
|
-
(not asuser or asuser == self.PGLOG['
|
|
1405
|
+
if (not (self.PGLOG['SETUID'] and self.PGLOG['SETUID'] == self.PGLOG['COMMONUSER']) and
|
|
1406
|
+
(not asuser or asuser == self.PGLOG['COMMONUSER'])):
|
|
1394
1407
|
return "sync" + host
|
|
1395
1408
|
return host + "-sync"
|
|
1396
1409
|
|
|
@@ -1407,7 +1420,7 @@ class PgLOG:
|
|
|
1407
1420
|
if cuid != self.PGLOG['EUID'] or cuid != self.PGLOG['RUID']:
|
|
1408
1421
|
os.setreuid(cuid, cuid)
|
|
1409
1422
|
self.PGLOG['SETUID'] = pwd.getpwuid(cuid).pw_name
|
|
1410
|
-
if not (self.PGLOG['SETUID'] == self.PGLOG['
|
|
1423
|
+
if not (self.PGLOG['SETUID'] == self.PGLOG['COMMONUSER'] or cuid == self.PGLOG['RUID']):
|
|
1411
1424
|
self.set_specialist_environments(self.PGLOG['SETUID'])
|
|
1412
1425
|
self.PGLOG['CURUID'] == self.PGLOG['SETUID'] # set CURUID to a specific specialist
|
|
1413
1426
|
|
|
@@ -1421,18 +1434,28 @@ class PgLOG:
|
|
|
1421
1434
|
|
|
1422
1435
|
Called automatically by :meth:`__init__`.
|
|
1423
1436
|
"""
|
|
1424
|
-
|
|
1437
|
+
# resolve common/admin user from environment (PGCOMMONUSER / PGADMINUSER)
|
|
1438
|
+
self.SETPGLOG("COMMONUSER", "gdexdata")
|
|
1439
|
+
self.SETPGLOG("ADMINUSER", "zji")
|
|
1440
|
+
self.PGLOG['RDAUSER'] = self.PGLOG['COMMONUSER']
|
|
1441
|
+
self.PGLOG['RDAGRP'] = self.PGLOG['GDEXGRP']
|
|
1442
|
+
self.PGLOG['RDAEMAIL'] = self.PGLOG['ADMINUSER']
|
|
1443
|
+
self.PGLOG['SUDORDA'] = self.PGLOG['SUDOGDEX']
|
|
1444
|
+
# backwards-compat aliases (deprecated: use COMMONUSER / ADMINUSER)
|
|
1445
|
+
self.PGLOG['GDEXUSER'] = self.PGLOG['COMMONUSER']
|
|
1446
|
+
self.PGLOG['GDEXEMAIL'] = self.PGLOG['ADMINUSER']
|
|
1447
|
+
self.PGLOG['CURDIR'] = os.getcwd()
|
|
1425
1448
|
# set current user id
|
|
1426
1449
|
self.PGLOG['RUID'] = os.getuid()
|
|
1427
1450
|
self.PGLOG['EUID'] = os.geteuid()
|
|
1428
1451
|
self.PGLOG['CURUID'] = pwd.getpwuid(self.PGLOG['RUID']).pw_name
|
|
1429
1452
|
try:
|
|
1430
|
-
self.PGLOG['RDAUID'] = self.PGLOG['GDEXUID'] = pwd.getpwnam(self.PGLOG['
|
|
1453
|
+
self.PGLOG['RDAUID'] = self.PGLOG['GDEXUID'] = pwd.getpwnam(self.PGLOG['COMMONUSER']).pw_uid
|
|
1431
1454
|
self.PGLOG['RDAGID'] = self.PGLOG['GDEXGID'] = grp.getgrnam(self.PGLOG['GDEXGRP']).gr_gid
|
|
1432
1455
|
except KeyError:
|
|
1433
1456
|
self.PGLOG['RDAUID'] = self.PGLOG['GDEXUID'] = 0
|
|
1434
1457
|
self.PGLOG['RDAGID'] = self.PGLOG['GDEXGID'] = 0
|
|
1435
|
-
if self.PGLOG['CURUID'] == self.PGLOG['
|
|
1458
|
+
if self.PGLOG['CURUID'] == self.PGLOG['COMMONUSER']: self.PGLOG['SETUID'] = self.PGLOG['COMMONUSER']
|
|
1436
1459
|
self.PGLOG['HOSTNAME'] = self.get_host()
|
|
1437
1460
|
for htype in self.HOSTTYPES:
|
|
1438
1461
|
ms = re.match(r'^{}(-|\d|$)'.format(htype), self.PGLOG['HOSTNAME'])
|
rda_python_common/pg_opt.py
CHANGED
|
@@ -649,7 +649,7 @@ class PgOPT(PgFile):
|
|
|
649
649
|
dataset in params), or logs a fatal error and exits.
|
|
650
650
|
"""
|
|
651
651
|
if not logname: logname = (self.params['LN'] if 'LN' in self.params else self.PGLOG['CURUID'])
|
|
652
|
-
if logname == self.PGLOG['
|
|
652
|
+
if logname == self.PGLOG['COMMONUSER']: return 1
|
|
653
653
|
dsids = {}
|
|
654
654
|
if dsid:
|
|
655
655
|
dsids[dsid] = 1
|
|
@@ -1847,7 +1847,7 @@ class PgOPT(PgFile):
|
|
|
1847
1847
|
exclude = (einfo['SENDER'] if errmsg else einfo['RECEIVER'])
|
|
1848
1848
|
if not errmsg and pgcntl and pgcntl['ccemail']:
|
|
1849
1849
|
self.add_carbon_copy(pgcntl['ccemail'], 1, exclude, pgrqst['specialist'])
|
|
1850
|
-
if self.PGLOG['CURUID'] != pgrqst['specialist'] and self.PGLOG['CURUID'] != self.PGLOG['
|
|
1850
|
+
if self.PGLOG['CURUID'] != pgrqst['specialist'] and self.PGLOG['CURUID'] != self.PGLOG['COMMONUSER']:
|
|
1851
1851
|
self.add_carbon_copy(self.PGLOG['CURUID'], 1, exclude)
|
|
1852
1852
|
if 'CC' in self.params: self.add_carbon_copy(self.params['CC'], 0, exclude)
|
|
1853
1853
|
einfo['CCD'] = self.PGLOG['CCDADDR']
|
rda_python_common/pg_sig.py
CHANGED
|
@@ -1183,14 +1183,14 @@ class PgSIG(PgDBI):
|
|
|
1183
1183
|
ms = re.match(r'^(\S+)', bcmd)
|
|
1184
1184
|
aname = ms.group(1) if ms else bcmd
|
|
1185
1185
|
curuid = self.PGLOG['CURUID']
|
|
1186
|
-
|
|
1186
|
+
commonuser = self.PGLOG['COMMONUSER']
|
|
1187
1187
|
for i in range(2):
|
|
1188
1188
|
for proc in psutil.process_iter(['pid', 'ppid', 'username', 'cmdline']):
|
|
1189
1189
|
try:
|
|
1190
1190
|
info = proc.info
|
|
1191
1191
|
if info.get('ppid') != 1: continue
|
|
1192
1192
|
uid = info.get('username')
|
|
1193
|
-
if uid != curuid and uid !=
|
|
1193
|
+
if uid != curuid and uid != commonuser: continue
|
|
1194
1194
|
cmdline = info.get('cmdline') or []
|
|
1195
1195
|
if not cmdline: continue
|
|
1196
1196
|
line = ' '.join(cmdline)
|
|
@@ -1199,7 +1199,7 @@ class PgSIG(PgDBI):
|
|
|
1199
1199
|
bid = info['pid']
|
|
1200
1200
|
if bid in self.CBIDS: return -1
|
|
1201
1201
|
acmd = line[idx+len(aname):]
|
|
1202
|
-
if uid ==
|
|
1202
|
+
if uid == commonuser:
|
|
1203
1203
|
acmd = re.sub(r'^\.(pl|py)\s+', '', acmd, 1)
|
|
1204
1204
|
if re.match(r'^{}{}'.format(aname, acmd), bcmd): continue
|
|
1205
1205
|
self.CBIDS[bid] = bcmd
|
rda_python_common/pg_util.py
CHANGED
|
@@ -14,6 +14,8 @@ import time
|
|
|
14
14
|
import datetime
|
|
15
15
|
import calendar
|
|
16
16
|
import glob
|
|
17
|
+
import bisect
|
|
18
|
+
import functools
|
|
17
19
|
from os import path as op
|
|
18
20
|
from .pg_log import PgLOG
|
|
19
21
|
|
|
@@ -419,11 +421,7 @@ class PgUtil(PgLOG):
|
|
|
419
421
|
list: Mixed int/str parts of the split datetime.
|
|
420
422
|
"""
|
|
421
423
|
if not isinstance(sdt, str): sdt = str(sdt)
|
|
422
|
-
|
|
423
|
-
acnt = len(adt)
|
|
424
|
-
for i in range(acnt):
|
|
425
|
-
if adt[i].isdigit(): adt[i] = int(adt[i])
|
|
426
|
-
return adt
|
|
424
|
+
return [int(x) if x.isdigit() else x for x in re.split(sep, sdt)]
|
|
427
425
|
|
|
428
426
|
# date: given date in format of fromfmt
|
|
429
427
|
# tofmt: date formats; ex. "Month D, YYYY"
|
|
@@ -547,14 +545,8 @@ class PgUtil(PgLOG):
|
|
|
547
545
|
# adjust second/minute/hour values out of range
|
|
548
546
|
for i in range(3):
|
|
549
547
|
if tms[i] != None and tms[i+1] != None:
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
tms[i] += ups[i]
|
|
553
|
-
tms[i+1] -= 1
|
|
554
|
-
elif tms[i] >= ups[i]:
|
|
555
|
-
while tms[i] >= ups[i]:
|
|
556
|
-
tms[i] -= ups[i]
|
|
557
|
-
tms[i+1] += 1
|
|
548
|
+
carry, tms[i] = divmod(tms[i], ups[i])
|
|
549
|
+
tms[i+1] += carry
|
|
558
550
|
sdt = self.fmtdate(yr, mn, dy, tofmt)
|
|
559
551
|
# format second/minute/hour values
|
|
560
552
|
for i in range(3):
|
|
@@ -592,14 +584,8 @@ class PgUtil(PgLOG):
|
|
|
592
584
|
"""
|
|
593
585
|
if not tofmt: tofmt = "YYYY-MM-DD:HH"
|
|
594
586
|
if hr != None and dy != None: # adjust hour value out of range
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
hr += 24
|
|
598
|
-
dy -= 1
|
|
599
|
-
elif hr > 23:
|
|
600
|
-
while hr > 23:
|
|
601
|
-
hr -= 24
|
|
602
|
-
dy += 1
|
|
587
|
+
carry, hr = divmod(hr, 24)
|
|
588
|
+
dy += carry
|
|
603
589
|
datehour = self.fmtdate(yr, mn, dy, tofmt)
|
|
604
590
|
if hr != None:
|
|
605
591
|
ms = re.search(self.DATEFMTS['H'], datehour, re.I)
|
|
@@ -733,9 +719,8 @@ class PgUtil(PgLOG):
|
|
|
733
719
|
"""
|
|
734
720
|
if not sdt: return [None, None]
|
|
735
721
|
if not isinstance(sdt, str): sdt = str(sdt)
|
|
736
|
-
adt =
|
|
737
|
-
|
|
738
|
-
if acnt == 1: adt.append('00:00:00')
|
|
722
|
+
adt = sdt.split(' ')
|
|
723
|
+
if len(adt) == 1: adt.append('00:00:00')
|
|
739
724
|
return adt
|
|
740
725
|
|
|
741
726
|
# convert given date/time to unix epoch time; -1 if cannot
|
|
@@ -1209,10 +1194,8 @@ class PgUtil(PgLOG):
|
|
|
1209
1194
|
cnt2 = len(lst2)
|
|
1210
1195
|
if unique:
|
|
1211
1196
|
for i in range(cnt2):
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
if j >= cnt1:
|
|
1215
|
-
lst1.append(lst2[i])
|
|
1197
|
+
if lst2[i] not in lst1:
|
|
1198
|
+
lst1.append(lst2[i])
|
|
1216
1199
|
else:
|
|
1217
1200
|
lst1.extend(lst2)
|
|
1218
1201
|
return lst1
|
|
@@ -1312,7 +1295,8 @@ class PgUtil(PgLOG):
|
|
|
1312
1295
|
rec.append(val)
|
|
1313
1296
|
rec.append(i) # add column to cache the row index
|
|
1314
1297
|
srecs.append(rec)
|
|
1315
|
-
srecs
|
|
1298
|
+
srecs.sort(key=functools.cmp_to_key(
|
|
1299
|
+
lambda a, b: PgUtil.cmp_records(a, b, desc, fcnt, nums)))
|
|
1316
1300
|
# sort pgrecs according the cached row index column in ordered srecs
|
|
1317
1301
|
rets = {}
|
|
1318
1302
|
for fld in pgrecs:
|
|
@@ -1335,10 +1319,13 @@ class PgUtil(PgLOG):
|
|
|
1335
1319
|
Returns:
|
|
1336
1320
|
int: Positive when date1 > date2, negative when date1 < date2.
|
|
1337
1321
|
"""
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1322
|
+
epoch = datetime.date(1970, 1, 1)
|
|
1323
|
+
def _to_date(d):
|
|
1324
|
+
if not d: return epoch
|
|
1325
|
+
ms = re.match(r'^(\d+)-(\d+)-(\d+)', str(d))
|
|
1326
|
+
if not ms: return epoch
|
|
1327
|
+
return datetime.date(int(ms.group(1)), int(ms.group(2)), int(ms.group(3)))
|
|
1328
|
+
return (_to_date(date1) - _to_date(date2)).days
|
|
1342
1329
|
|
|
1343
1330
|
# Return: the number of seconds bewteen time1 and time2
|
|
1344
1331
|
@staticmethod
|
|
@@ -1557,15 +1544,10 @@ class PgUtil(PgLOG):
|
|
|
1557
1544
|
if ms:
|
|
1558
1545
|
(syr, smn) = ms.groups()
|
|
1559
1546
|
nyr = int(syr) + yr
|
|
1560
|
-
nmn = int(smn) + mn
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
nmn += 12
|
|
1565
|
-
else:
|
|
1566
|
-
while nmn > 12:
|
|
1567
|
-
nyr += 1
|
|
1568
|
-
nmn -= 12
|
|
1547
|
+
nmn = int(smn) + mn - 1 # shift to 0-indexed for divmod
|
|
1548
|
+
extra, nmn = divmod(nmn, 12)
|
|
1549
|
+
nyr += extra
|
|
1550
|
+
nmn += 1 # back to 1-indexed
|
|
1569
1551
|
ym = "{:04}{:02}".format(nyr, nmn)
|
|
1570
1552
|
return ym
|
|
1571
1553
|
|
|
@@ -1729,14 +1711,7 @@ class PgUtil(PgLOG):
|
|
|
1729
1711
|
if ms:
|
|
1730
1712
|
shr = ms.group(1)
|
|
1731
1713
|
hr = int(shr) + nhour
|
|
1732
|
-
|
|
1733
|
-
while hr < 0:
|
|
1734
|
-
dy -= 1
|
|
1735
|
-
hr += 24
|
|
1736
|
-
else:
|
|
1737
|
-
while hr > 23:
|
|
1738
|
-
dy += 1
|
|
1739
|
-
hr -= 24
|
|
1714
|
+
dy, hr = divmod(hr, 24)
|
|
1740
1715
|
shour = "{:02}".format(hr)
|
|
1741
1716
|
if shr != shour: stime = re.sub(shr, shour, stime, 1)
|
|
1742
1717
|
if dy: sdate = self.adddate(sdate, 0, 0, dy)
|
|
@@ -1765,14 +1740,8 @@ class PgUtil(PgLOG):
|
|
|
1765
1740
|
if nhour != None:
|
|
1766
1741
|
if isinstance(nhour, str): nhour = int(nhour)
|
|
1767
1742
|
hr += nhour
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
dy -= 1
|
|
1771
|
-
hr += 24
|
|
1772
|
-
else:
|
|
1773
|
-
while hr > 23:
|
|
1774
|
-
dy += 1
|
|
1775
|
-
hr -= 24
|
|
1743
|
+
carry, hr = divmod(hr, 24)
|
|
1744
|
+
dy += carry
|
|
1776
1745
|
if nhour != None: nhour = hr
|
|
1777
1746
|
if yr or mn or dy: sdate = self.adddate(sdate, yr, mn, dy)
|
|
1778
1747
|
return [sdate, nhour]
|
|
@@ -1799,7 +1768,7 @@ class PgUtil(PgLOG):
|
|
|
1799
1768
|
str: Resulting datetime string in 'YYYY-MM-DD HH:MM:SS' format.
|
|
1800
1769
|
"""
|
|
1801
1770
|
if sdatetime and not isinstance(sdatetime, str): sdatetime = str(sdatetime)
|
|
1802
|
-
(sdate, stime) =
|
|
1771
|
+
(sdate, stime) = sdatetime.split(' ', 1)
|
|
1803
1772
|
if hh or nn or ss: (sdate, stime) = self.addtime(sdate, stime, hh, nn, ss)
|
|
1804
1773
|
if nf:
|
|
1805
1774
|
sdate = self.addmonth(sdate, mm, nf)
|
|
@@ -1837,14 +1806,8 @@ class PgUtil(PgLOG):
|
|
|
1837
1806
|
tms[1] += int(ms.group(2))
|
|
1838
1807
|
tms[0] += int(ms.group(3))
|
|
1839
1808
|
for i in range(3):
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
tms[i] += ups[i]
|
|
1843
|
-
tms[i+1] -= 1
|
|
1844
|
-
elif tms[i] >= ups[i]:
|
|
1845
|
-
while tms[i] >= ups[i]:
|
|
1846
|
-
tms[i] -= ups[i]
|
|
1847
|
-
tms[i+1] += 1
|
|
1809
|
+
carry, tms[i] = divmod(tms[i], ups[i])
|
|
1810
|
+
tms[i+1] += carry
|
|
1848
1811
|
stime = "{:02}:{:02}:{:02}".format(tms[2], tms[1], tms[0])
|
|
1849
1812
|
if tms[3]: sdate = self.adddate(sdate, 0, 0, tms[3])
|
|
1850
1813
|
return [sdate, stime]
|
|
@@ -1985,7 +1948,7 @@ class PgUtil(PgLOG):
|
|
|
1985
1948
|
"""
|
|
1986
1949
|
if sdatetime and not isinstance(sdatetime, str): sdatetime = str(sdatetime)
|
|
1987
1950
|
if not (unit and unit in 'YMWDHNS'): return sdatetime
|
|
1988
|
-
(sdate, stime) =
|
|
1951
|
+
(sdate, stime) = sdatetime.split(' ', 1)
|
|
1989
1952
|
if unit in 'HNS':
|
|
1990
1953
|
stime = self.endtime(stime, unit)
|
|
1991
1954
|
else:
|
|
@@ -2011,7 +1974,7 @@ class PgUtil(PgLOG):
|
|
|
2011
1974
|
for val in values:
|
|
2012
1975
|
if val is None: continue
|
|
2013
1976
|
sval = str(val)
|
|
2014
|
-
if sval and
|
|
1977
|
+
if sval and '\n' not in sval:
|
|
2015
1978
|
slen = len(sval)
|
|
2016
1979
|
if slen > clen: clen = slen
|
|
2017
1980
|
return clen
|
|
@@ -2199,21 +2162,8 @@ class PgUtil(PgLOG):
|
|
|
2199
2162
|
Returns:
|
|
2200
2163
|
int: Index of the matching element, or -1 when not found.
|
|
2201
2164
|
"""
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
for midx in range(lidx, hidx):
|
|
2205
|
-
if key == list[midx]:
|
|
2206
|
-
ret = midx
|
|
2207
|
-
break
|
|
2208
|
-
else:
|
|
2209
|
-
midx = (lidx + hidx) // 2
|
|
2210
|
-
if key == list[midx]:
|
|
2211
|
-
ret = midx
|
|
2212
|
-
elif key < list[midx]:
|
|
2213
|
-
ret = PgUtil.asearch(lidx, midx, key, list)
|
|
2214
|
-
else:
|
|
2215
|
-
ret = PgUtil.asearch(midx + 1, hidx, key, list)
|
|
2216
|
-
return ret
|
|
2165
|
+
idx = bisect.bisect_left(list, key, lidx, hidx)
|
|
2166
|
+
return idx if idx < hidx and list[idx] == key else -1
|
|
2217
2167
|
|
|
2218
2168
|
# lidx: lower index limit (including)
|
|
2219
2169
|
# hidx: higher index limit (excluding)
|
|
@@ -2362,14 +2312,11 @@ class PgUtil(PgLOG):
|
|
|
2362
2312
|
buffer = f.read(blocksize)
|
|
2363
2313
|
# Check for null bytes (a strong indicator of a binary file)
|
|
2364
2314
|
if not buffer or b'\0' in buffer: return 0
|
|
2365
|
-
|
|
2315
|
+
text_set = frozenset(
|
|
2366
2316
|
b'\t\n\r\f\v' + # Whitespace characters
|
|
2367
2317
|
bytes(range(32, 127)) # Printable ASCII characters
|
|
2368
2318
|
)
|
|
2369
|
-
non_text_count =
|
|
2370
|
-
for byte in buffer:
|
|
2371
|
-
if byte not in text_characters:
|
|
2372
|
-
non_text_count += 1 # Count non-text characters
|
|
2319
|
+
non_text_count = sum(b not in text_set for b in buffer)
|
|
2373
2320
|
# If a significant portion of the buffer consists of non-text characters,
|
|
2374
2321
|
# it's likely a binary file.
|
|
2375
2322
|
return 1 if((non_text_count/len(buffer)) < threshhold) else 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rda_python_common
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: RDA Python common library codes shared by other RDA python packages
|
|
5
5
|
Author-email: Zaihua Ji <zji@ucar.edu>
|
|
6
6
|
Project-URL: Homepage, https://github.com/NCAR/rda-python-common
|
|
@@ -11,23 +11,45 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
11
11
|
Requires-Python: >=3.7
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist:
|
|
14
|
+
Requires-Dist: psycopg
|
|
15
15
|
Requires-Dist: psutil
|
|
16
16
|
Requires-Dist: rda-python-globus
|
|
17
17
|
Requires-Dist: unidecode
|
|
18
18
|
Requires-Dist: hvac
|
|
19
|
+
Provides-Extra: psycopg2
|
|
20
|
+
Requires-Dist: psycopg2; extra == "psycopg2"
|
|
21
|
+
Provides-Extra: psycopg2-binary
|
|
22
|
+
Requires-Dist: psycopg2-binary; extra == "psycopg2-binary"
|
|
19
23
|
Dynamic: license-file
|
|
20
24
|
|
|
21
25
|
# rda-python-common
|
|
22
26
|
|
|
23
27
|
Python common library codes to be shared by other RDA python utility programs.
|
|
24
28
|
|
|
25
|
-
##
|
|
29
|
+
## Environment setup
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
Create a Python environment first; the install command in the next section
|
|
32
|
+
runs inside whichever environment you activate here.
|
|
33
|
+
|
|
34
|
+
### Option A — Python venv (DECS machines)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
|
|
38
|
+
source $ENVHOME/bin/activate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Option B — Conda (DAV/Casper)
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
```bash
|
|
44
|
+
conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
|
|
45
|
+
conda activate $ENVHOME
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Installing rda-python-common
|
|
49
|
+
|
|
50
|
+
Pick whichever install mode fits your workflow. All four pull in the
|
|
51
|
+
transitive dependencies (`psycopg`, `rda-python-globus`, `unidecode`,
|
|
52
|
+
`hvac`) automatically.
|
|
31
53
|
|
|
32
54
|
For local development, clone this repo alongside your project and install it
|
|
33
55
|
in editable mode so that changes are picked up without re-installing:
|
|
@@ -38,6 +60,15 @@ cd rda-python-common
|
|
|
38
60
|
pip install -e .
|
|
39
61
|
```
|
|
40
62
|
|
|
63
|
+
To test a specific branch (e.g. an in-progress feature or fix branch), pass
|
|
64
|
+
`-b/--branch` to `git clone`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone -b <branch-name> https://github.com/NCAR/rda-python-common.git
|
|
68
|
+
cd rda-python-common
|
|
69
|
+
pip install -e .
|
|
70
|
+
```
|
|
71
|
+
|
|
41
72
|
For a regular (non-editable) install from a checkout:
|
|
42
73
|
|
|
43
74
|
```bash
|
|
@@ -50,10 +81,75 @@ For a production install on a system that uses the published distribution:
|
|
|
50
81
|
pip install rda_python_common
|
|
51
82
|
```
|
|
52
83
|
|
|
53
|
-
|
|
54
|
-
|
|
84
|
+
### PostgreSQL driver: psycopg v3 (default) and psycopg2 (fallback)
|
|
85
|
+
|
|
86
|
+
`rda-python-common` uses **psycopg v3** by default. `pg_dbi.py`
|
|
87
|
+
auto-detects which driver is installed at import time and prefers psycopg v3
|
|
88
|
+
when both are present; no code changes are needed to switch drivers.
|
|
55
89
|
|
|
56
|
-
|
|
90
|
+
The required dependency is the base `psycopg` package, which works whether
|
|
91
|
+
psycopg was compiled from source or installed via a binary wheel. If psycopg
|
|
92
|
+
is not available on your system, install whichever driver works:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install psycopg || pip install psycopg2
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
To explicitly install the legacy psycopg2 driver:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install "rda_python_common[psycopg2]" # build from source
|
|
102
|
+
pip install "rda_python_common[psycopg2-binary]" # pre-built wheel
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Configuration: COMMONUSER and ADMINUSER
|
|
106
|
+
|
|
107
|
+
`PGLOG['COMMONUSER']` is the shared common user that setuid-wrapped programs
|
|
108
|
+
execute as (default `gdexdata`), and `PGLOG['ADMINUSER']` is the admin
|
|
109
|
+
specialist user that receives email notifications and is permitted to invoke
|
|
110
|
+
`pgstart_<user>` (default `zji`).
|
|
111
|
+
|
|
112
|
+
Both values are initialized via the `SETPGLOG(key, default)` helper, which
|
|
113
|
+
reads the environment variable `PG<KEY>` and falls back to the supplied
|
|
114
|
+
default when the variable is unset:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# pg_log.py (class-based)
|
|
118
|
+
self.SETPGLOG("COMMONUSER", "gdexdata") # reads $PGCOMMONUSER
|
|
119
|
+
self.SETPGLOG("ADMINUSER", "zji") # reads $PGADMINUSER
|
|
120
|
+
|
|
121
|
+
# PgLOG.py (module-level) exposes the same helper as a function
|
|
122
|
+
SETPGLOG("COMMONUSER", "gdexdata")
|
|
123
|
+
SETPGLOG("ADMINUSER", "zji")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
To override the defaults per environment **once** so the values persist
|
|
127
|
+
across `pip install --upgrade`, set the environment variables:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
export PGCOMMONUSER=gdexdata # overrides PGLOG['COMMONUSER']
|
|
131
|
+
export PGADMINUSER=zji # overrides PGLOG['ADMINUSER']
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Place these `export` lines in `$ENVHOME/bin/activate` (venv), or set them as
|
|
135
|
+
conda environment variables so they are applied whenever the environment is
|
|
136
|
+
activated:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
conda env config vars set PGCOMMONUSER=gdexdata PGADMINUSER=zji
|
|
140
|
+
conda activate $ENVHOME # reactivate to pick up the values
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
If the variables are unset, the built-in defaults (`gdexdata` / `zji`) are
|
|
144
|
+
used, preserving existing behavior.
|
|
145
|
+
|
|
146
|
+
## Using rda-python-common in another RDA python repo
|
|
147
|
+
|
|
148
|
+
`rda-python-common` is the foundation that every other `rda-python-*` repo
|
|
149
|
+
builds on. Once it is installed in the active environment, consuming it from
|
|
150
|
+
a new or existing repo takes three short steps.
|
|
151
|
+
|
|
152
|
+
### 1. Declare it as a dependency in your project
|
|
57
153
|
|
|
58
154
|
Add `rda_python_common` to the `dependencies` list of your project's
|
|
59
155
|
`pyproject.toml` so that downstream installs pull it in automatically:
|
|
@@ -72,9 +168,10 @@ This is the same pattern used by `rda-python-dsarch`, `rda-python-dsupdt`,
|
|
|
72
168
|
`rda-python-dsrqst`, `rda-python-dscheck`, `rda-python-metrics`, and
|
|
73
169
|
`rda-python-miscs`.
|
|
74
170
|
|
|
75
|
-
###
|
|
171
|
+
### 2. Import the modules you need
|
|
76
172
|
|
|
77
|
-
Two import styles are supported (see [Usage examples](#usage-examples) below
|
|
173
|
+
Two import styles are supported (see [Usage examples](#usage-examples) below
|
|
174
|
+
for fuller patterns):
|
|
78
175
|
|
|
79
176
|
```python
|
|
80
177
|
# Preferred for new code -- import the class from the lower-case module
|
|
@@ -86,26 +183,26 @@ from rda_python_common import PgLOG, PgDBI
|
|
|
86
183
|
PgLOG.pglog("hello", PgLOG.LOGWRN)
|
|
87
184
|
```
|
|
88
185
|
|
|
89
|
-
###
|
|
186
|
+
### 3. Verify the install
|
|
90
187
|
|
|
91
188
|
```bash
|
|
92
189
|
python -c "import rda_python_common; print(rda_python_common.__version__)"
|
|
93
190
|
```
|
|
94
191
|
|
|
95
|
-
You should see the installed version (currently `
|
|
192
|
+
You should see the installed version (currently `3.0.0`). If the import
|
|
96
193
|
fails, double-check that the active Python environment is the one where you
|
|
97
194
|
ran `pip install`.
|
|
98
195
|
|
|
99
196
|
## Modules
|
|
100
197
|
|
|
101
|
-
All shared functionality lives under `src/rda_python_common/` and is organised
|
|
102
|
-
a single-inheritance class hierarchy. Each module defines exactly
|
|
103
|
-
later classes extend earlier ones, so an application that
|
|
104
|
-
top-of-chain class (typically `PgOPT` or `PgCMD`) gets every
|
|
105
|
-
object.
|
|
198
|
+
All shared functionality lives under `src/rda_python_common/` and is organised
|
|
199
|
+
as a (mostly) single-inheritance class hierarchy. Each module defines exactly
|
|
200
|
+
one class; later classes extend earlier ones, so an application that
|
|
201
|
+
instantiates the top-of-chain class (typically `PgOPT` or `PgCMD`) gets every
|
|
202
|
+
helper through one object.
|
|
106
203
|
|
|
107
|
-
|
|
108
|
-
converging on the same child
|
|
204
|
+
The inheritance tree below is read top-down; the two multi-inheritance joins
|
|
205
|
+
are shown as two arrows converging on the same child:
|
|
109
206
|
|
|
110
207
|
```
|
|
111
208
|
PgLOG
|
|
@@ -142,6 +239,8 @@ The tree is single inheritance everywhere except at two join points:
|
|
|
142
239
|
operations (`PgDBI`) it needs to keep the shared `wfile` table and the
|
|
143
240
|
per-dataset `wfile_<dsid>` partitions in sync.
|
|
144
241
|
|
|
242
|
+
Each class lives in its own module. Walking the tree from the root:
|
|
243
|
+
|
|
145
244
|
- **`pg_log.py`** — `PgLOG`. Root of the hierarchy. Provides the central
|
|
146
245
|
logging facility (bit-mask `logact` flags such as `MSGLOG`, `WARNLG`,
|
|
147
246
|
`ERRLOG`, `EXITLG`), e-mail dispatch, system-command execution, process
|
|
@@ -165,7 +264,8 @@ The tree is single inheritance everywhere except at two join points:
|
|
|
165
264
|
long-running batch jobs coordinate cleanly.
|
|
166
265
|
|
|
167
266
|
- **`pg_dbi.py`** — `PgDBI(PgLOG)`. PostgreSQL database interface built on
|
|
168
|
-
`
|
|
267
|
+
`psycopg` (v3 by default, with `psycopg2` as an opt-in fallback). Wraps
|
|
268
|
+
connection management, batch `INSERT`/`SELECT`/
|
|
169
269
|
`UPDATE`/`DELETE`, transaction control, and credential lookup from
|
|
170
270
|
`.pgpass` or OpenBao. All RDA tools talk to the `rdadb` database through
|
|
171
271
|
this class.
|
|
@@ -199,9 +299,9 @@ The tree is single inheritance everywhere except at two join points:
|
|
|
199
299
|
|
|
200
300
|
## Usage examples
|
|
201
301
|
|
|
202
|
-
|
|
203
|
-
either instantiate it directly or
|
|
204
|
-
state and methods.
|
|
302
|
+
The patterns below show the typical ways the classes above are used in
|
|
303
|
+
practice. Import the class you need, then either instantiate it directly or
|
|
304
|
+
subclass it to add application-specific state and methods.
|
|
205
305
|
|
|
206
306
|
### 1. Direct instantiation — use the helpers as-is
|
|
207
307
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
rda_python_common/PgCMD.py,sha256=EYjG2Z4zEnvsXE1z-jt5UaNoEKxnOYYiMMzvW6HrKA4,20597
|
|
2
|
+
rda_python_common/PgDBI.py,sha256=e8skOqqIOPQ3_O33b-LFTVfnHRZYe6hYoS8-MoQJF6E,78398
|
|
3
|
+
rda_python_common/PgFile.py,sha256=y_Gxi-54cyb1jpJRKNl3OlOm8I3WG7JuvgANWJ7PDE8,99372
|
|
4
|
+
rda_python_common/PgLOG.py,sha256=l6PXrgCPZ_tmYKSEufoHfWBfCBzVuwcHXpbk3aYA7_w,55892
|
|
5
|
+
rda_python_common/PgLock.py,sha256=12i84nsGBuifSyPnm8IR63LvHvRuVU573D5QKFlHdOI,22623
|
|
6
|
+
rda_python_common/PgOPT.py,sha256=tJ8qb2515HaMPR-oLIg8BKalI1BxcmIHcSb0UEYx2K4,56248
|
|
7
|
+
rda_python_common/PgSIG.py,sha256=dVENQ6tvnoa8fSZ8sYenMP4IVKExtU7fdSdKhSLxlWE,35807
|
|
8
|
+
rda_python_common/PgSplit.py,sha256=SSg5_Qu5PqP44EkqebO-V_cErNcdE2QtORgFHQ7RqlQ,8822
|
|
9
|
+
rda_python_common/PgUtil.py,sha256=OqESKCd72b9g8m8jwjPJhXDtPYlW6G8oSOhwChvz2Cg,48600
|
|
10
|
+
rda_python_common/__init__.py,sha256=WTpasPcnTQMDxRbPXv6vTbQQv8bITdUSkZ_RfdyBtig,994
|
|
11
|
+
rda_python_common/pg_cmd.py,sha256=PiQaAeb7l92LceqMwSzLpz9nD5pDRHJcEZxTDn-HeCs,33255
|
|
12
|
+
rda_python_common/pg_dbi.py,sha256=73DAROFqQF1HDAVCX7t92gRrOnIFyGLpe8RPYLWDVgo,119524
|
|
13
|
+
rda_python_common/pg_file.py,sha256=nwu1DTD6Pgi4yUFt_IJwvYvMbVMW1eEIf1N5EZjM1o8,162232
|
|
14
|
+
rda_python_common/pg_lock.py,sha256=31EaVDjCkcx3-n8-KnzG18R8Pz7Z6KyFsEqcml6Iq5c,32702
|
|
15
|
+
rda_python_common/pg_log.py,sha256=lzhjEL7FaGjHfl4cmlMuIZVAb5x0fSgiI8AShe0sAHw,82146
|
|
16
|
+
rda_python_common/pg_opt.py,sha256=evf0ZtNeYyRjGyTIghD6wI5u7eDbiDkDudLYRk5z0QA,82500
|
|
17
|
+
rda_python_common/pg_password.py,sha256=X-eIDwdqBhtrhrbDTNWle-0JtWsyIVZdDOZaBu7cFHM,2343
|
|
18
|
+
rda_python_common/pg_sig.py,sha256=o4L9qjrgOIAASXntY6kHfEGV2TXuFL3mTczqkemY9oQ,53572
|
|
19
|
+
rda_python_common/pg_split.py,sha256=aAWKUZPmZ-LQ_fJ3DSKzPKxJw0fMDJ2fgP8ZT629M30,16375
|
|
20
|
+
rda_python_common/pg_util.py,sha256=3_-baqCI9j5bkC2Uw3hRJZtYwdr0FQC6mtgbeIhLwyQ,86045
|
|
21
|
+
rda_python_common/pgpassword.py,sha256=aTQaO59QokWrv2EKom4TvKhNW2Mr6aFMMBVG4DCbcKI,5023
|
|
22
|
+
rda_python_common/pgpassword.usg,sha256=Wrle6A8sdlMGx3ZEAmE6TNDPOlG84Whe0T3rCNFzPfY,2013
|
|
23
|
+
rda_python_common-3.0.1.dist-info/licenses/LICENSE,sha256=1dck4EAQwv8QweDWCXDx-4Or0S8YwiCstaso_H57Pno,1097
|
|
24
|
+
rda_python_common-3.0.1.dist-info/METADATA,sha256=9XS840t2_gpk-_vLq7tBcvuJReVMm6uj7eap97iBxgw,14528
|
|
25
|
+
rda_python_common-3.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
26
|
+
rda_python_common-3.0.1.dist-info/entry_points.txt,sha256=pZgVNWspcK-F1TbPav7C3C9NdeHDZMm_25fW9weix00,65
|
|
27
|
+
rda_python_common-3.0.1.dist-info/top_level.txt,sha256=KVQmx7D3DD-jsiheqL8HdTrRE14hpRnZY5_ioMArA5k,18
|
|
28
|
+
rda_python_common-3.0.1.dist-info/RECORD,,
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
rda_python_common/PgCMD.py,sha256=EYjG2Z4zEnvsXE1z-jt5UaNoEKxnOYYiMMzvW6HrKA4,20597
|
|
2
|
-
rda_python_common/PgDBI.py,sha256=sOKQmzF_qmLR_vnj-NnFEYbLI05t2bVbhqlqy46iluA,76424
|
|
3
|
-
rda_python_common/PgFile.py,sha256=7hyDFm40_NjQ_tg5WviMw_waYPdyXCi0DRCf0Av8eNE,99370
|
|
4
|
-
rda_python_common/PgLOG.py,sha256=43YVpMjWEOLpaMUb4YTEYlDTvp7FTmDQ8V3TAKQrC4M,55228
|
|
5
|
-
rda_python_common/PgLock.py,sha256=12i84nsGBuifSyPnm8IR63LvHvRuVU573D5QKFlHdOI,22623
|
|
6
|
-
rda_python_common/PgOPT.py,sha256=Kn4JYezZhZwAn2usiIYHoHiymGHRgsN299dxyXbKwkI,56244
|
|
7
|
-
rda_python_common/PgSIG.py,sha256=eTJJ3XxutsQ3JSg6uYRpH26ZmrVUup3ShqDFLWXLruA,35803
|
|
8
|
-
rda_python_common/PgSplit.py,sha256=SSg5_Qu5PqP44EkqebO-V_cErNcdE2QtORgFHQ7RqlQ,8822
|
|
9
|
-
rda_python_common/PgUtil.py,sha256=OqESKCd72b9g8m8jwjPJhXDtPYlW6G8oSOhwChvz2Cg,48600
|
|
10
|
-
rda_python_common/__init__.py,sha256=58wQYxVa1MSXJMPsqe-4fnC1zvyZKe1UQVuc9Zqem6I,995
|
|
11
|
-
rda_python_common/pg_cmd.py,sha256=PiQaAeb7l92LceqMwSzLpz9nD5pDRHJcEZxTDn-HeCs,33255
|
|
12
|
-
rda_python_common/pg_dbi.py,sha256=q6LT4a4lwo-sM2LEqgrEVkAlh886xeKiz4feqRKw-7c,116238
|
|
13
|
-
rda_python_common/pg_file.py,sha256=NgmF2pAl4955FZzMBIN_hQ8DjyUnxPputafrgamE47g,162230
|
|
14
|
-
rda_python_common/pg_lock.py,sha256=31EaVDjCkcx3-n8-KnzG18R8Pz7Z6KyFsEqcml6Iq5c,32702
|
|
15
|
-
rda_python_common/pg_log.py,sha256=n6Nh5g6cQbahAZv_Fe3xv2VGPJtLcCRw_kb6d8Rs9Fg,80806
|
|
16
|
-
rda_python_common/pg_opt.py,sha256=sXrlzFWpR2XMak6NlA0MPvErBuQnY6gHM5OsHtoeIPQ,82496
|
|
17
|
-
rda_python_common/pg_password.py,sha256=X-eIDwdqBhtrhrbDTNWle-0JtWsyIVZdDOZaBu7cFHM,2343
|
|
18
|
-
rda_python_common/pg_sig.py,sha256=4wxE7NxeJyP8Q961uFPYrQCvlWm6D3Eu5RW9ipIr54s,53564
|
|
19
|
-
rda_python_common/pg_split.py,sha256=aAWKUZPmZ-LQ_fJ3DSKzPKxJw0fMDJ2fgP8ZT629M30,16375
|
|
20
|
-
rda_python_common/pg_util.py,sha256=bjp2civRIhqaBSR8oOtyRzYIZBdwB90SzmJLjRIA7fc,87280
|
|
21
|
-
rda_python_common/pgpassword.py,sha256=aTQaO59QokWrv2EKom4TvKhNW2Mr6aFMMBVG4DCbcKI,5023
|
|
22
|
-
rda_python_common/pgpassword.usg,sha256=Wrle6A8sdlMGx3ZEAmE6TNDPOlG84Whe0T3rCNFzPfY,2013
|
|
23
|
-
rda_python_common-2.1.11.dist-info/licenses/LICENSE,sha256=1dck4EAQwv8QweDWCXDx-4Or0S8YwiCstaso_H57Pno,1097
|
|
24
|
-
rda_python_common-2.1.11.dist-info/METADATA,sha256=i87yfkJjxDheTlHhevL8bnt6Jm9datVnG4vkMaSb6Nk,11093
|
|
25
|
-
rda_python_common-2.1.11.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
26
|
-
rda_python_common-2.1.11.dist-info/entry_points.txt,sha256=pZgVNWspcK-F1TbPav7C3C9NdeHDZMm_25fW9weix00,65
|
|
27
|
-
rda_python_common-2.1.11.dist-info/top_level.txt,sha256=KVQmx7D3DD-jsiheqL8HdTrRE14hpRnZY5_ioMArA5k,18
|
|
28
|
-
rda_python_common-2.1.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|