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

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