rda-python-common 3.0.27__tar.gz → 3.0.29__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {rda_python_common-3.0.27/src/rda_python_common.egg-info → rda_python_common-3.0.29}/PKG-INFO +2 -2
  2. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/README.md +1 -1
  3. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/pyproject.toml +1 -1
  4. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/__init__.py +1 -1
  5. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pg_file.py +86 -0
  6. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pg_sig.py +41 -6
  7. {rda_python_common-3.0.27 → rda_python_common-3.0.29/src/rda_python_common.egg-info}/PKG-INFO +2 -2
  8. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/LICENSE +0 -0
  9. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/setup.cfg +0 -0
  10. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/PgCMD.py +0 -0
  11. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/PgDBI.py +0 -0
  12. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/PgFile.py +0 -0
  13. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/PgLOG.py +0 -0
  14. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/PgLock.py +0 -0
  15. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/PgOPT.py +0 -0
  16. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/PgSIG.py +0 -0
  17. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/PgSplit.py +0 -0
  18. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/PgUtil.py +0 -0
  19. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pg_cmd.py +0 -0
  20. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pg_dbi.py +0 -0
  21. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pg_lock.py +0 -0
  22. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pg_log.py +0 -0
  23. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pg_opt.py +0 -0
  24. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pg_password.py +0 -0
  25. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pg_split.py +0 -0
  26. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pg_util.py +0 -0
  27. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pgpassword.py +0 -0
  28. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common/pgpassword.usg +0 -0
  29. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common.egg-info/SOURCES.txt +0 -0
  30. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common.egg-info/dependency_links.txt +0 -0
  31. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common.egg-info/entry_points.txt +0 -0
  32. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common.egg-info/requires.txt +0 -0
  33. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/src/rda_python_common.egg-info/top_level.txt +0 -0
  34. {rda_python_common-3.0.27 → rda_python_common-3.0.29}/test/test_common.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_common
3
- Version: 3.0.27
3
+ Version: 3.0.29
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
@@ -189,7 +189,7 @@ PgLOG.pglog("hello", PgLOG.LOGWRN)
189
189
  python -c "import rda_python_common; print(rda_python_common.__version__)"
190
190
  ```
191
191
 
192
- You should see the installed version (currently `3.0.27`). If the import
192
+ You should see the installed version (currently `3.0.29`). If the import
193
193
  fails, double-check that the active Python environment is the one where you
194
194
  ran `pip install`.
195
195
 
@@ -165,7 +165,7 @@ PgLOG.pglog("hello", PgLOG.LOGWRN)
165
165
  python -c "import rda_python_common; print(rda_python_common.__version__)"
166
166
  ```
167
167
 
168
- You should see the installed version (currently `3.0.27`). If the import
168
+ You should see the installed version (currently `3.0.29`). If the import
169
169
  fails, double-check that the active Python environment is the one where you
170
170
  ran `pip install`.
171
171
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rda_python_common"
7
- version = "3.0.27"
7
+ version = "3.0.29"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -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__ = "3.0.27"
25
+ __version__ = "3.0.29"
26
26
 
27
27
  __all__ = [
28
28
  "PgLOG",
@@ -410,6 +410,92 @@ class PgFile(PgUtil, PgSIG):
410
410
  self.errlog("Error Execute: {}\n{}".format(cmd, buf), 'O', loop, logact)
411
411
  return self.FAILURE
412
412
 
413
+ # Upload a local directory to object under a key prefix
414
+ # todir - target object key prefix, without the trailing '/'
415
+ # fromdir - source local directory name
416
+ def local_copy_object_directory(self, todir, fromdir, bucket = None, meta = None, logact = 0):
417
+ """Upload every file under a local directory to an object key prefix.
418
+
419
+ The whole directory goes up in one recursive call, the upload twin of
420
+ object_copy_local_directory(): one process per file costs several seconds
421
+ of start-up each, which dominates for a directory of many files. Each file
422
+ lands at '{todir}/{path relative to fromdir}'.
423
+
424
+ Nothing is uploaded when every file is already under the prefix at its local
425
+ size (unless OVRIDE is set); otherwise the whole directory is uploaded again,
426
+ since the recursive call cannot be limited to the missing files.
427
+
428
+ Args:
429
+ todir (str): Object key prefix (destination path in the bucket); not empty.
430
+ fromdir (str): Source local directory path.
431
+ bucket (str | None): Target bucket; defaults to PGLOG['OBJCTBKT'].
432
+ meta (dict | None): Extra metadata key/value pairs to attach to each object.
433
+ logact (int): Logging action flags; default 0.
434
+
435
+ Returns:
436
+ int: self.SUCCESS on success, self.FAILURE on error.
437
+ """
438
+ if not bucket: bucket = self.PGLOG['OBJCTBKT']
439
+ if meta is None: meta = {}
440
+ if 'user' not in meta: meta['user'] = self.PGLOG['CURUID']
441
+ if 'group' not in meta: meta['group'] = self.PGLOG['GDEXGRP']
442
+ uinfo = json.dumps(meta)
443
+ ms = re.match(r'^(.+)/$', fromdir)
444
+ if ms: fromdir = ms.group(1)
445
+ ms = re.match(r'^(.+)/$', todir)
446
+ if ms: todir = ms.group(1)
447
+ # the same walk isd_s3_cli makes to pick the files: no symlinked directories
448
+ lfiles = {}
449
+ for path, dirs, files in os.walk(fromdir):
450
+ for fname in files:
451
+ lfile = op.join(path, fname)
452
+ lfiles[op.relpath(lfile, fromdir)] = op.getsize(lfile)
453
+ if not lfiles:
454
+ return self.lmsg(fromdir, "{} to copy to {}-{}".format(self.PGLOG['MISSFILE'], self.OHOST, todir), logact)
455
+ prefix = todir + '/'
456
+ if not logact&self.OVRIDE:
457
+ flist = self.object_glob(todir, bucket, 0, logact)
458
+ if flist == self.FAILURE: return self.FAILURE
459
+ miss = self.object_upload_missing(prefix, lfiles, flist)
460
+ if not miss:
461
+ self.pglog("{}-{}-{}: all {} file(s) exist already, skip uploading".format(
462
+ self.OHOST, bucket, todir, len(lfiles)), logact&~self.EXITLG)
463
+ return self.SUCCESS
464
+ # -db too: an upload goes to the default bucket whatever -b says
465
+ cmd = "{} -db {} um -ld {} -b {} -kp {} -r -md '{}'".format(
466
+ self.OBJCTCMD, bucket, fromdir, bucket, todir, uinfo)
467
+ buf = self.pgsystem(cmd, logact, self.CMDBTH)
468
+ flist = self.object_glob(todir, bucket, 0, logact)
469
+ if flist == self.FAILURE: return self.FAILURE
470
+ miss = self.object_upload_missing(prefix, lfiles, flist)
471
+ if miss:
472
+ return self.errlog("{}-{}: Error upload {} of {} file(s) to {}-{}\n{}".format(
473
+ fromdir, miss[0], len(miss), len(lfiles), self.OHOST, todir, buf), 'O', 1, logact)
474
+ self.pglog("{}: {} file(s) uploaded to {}-{}-{}".format(
475
+ fromdir, len(lfiles), self.OHOST, bucket, todir), logact&~self.EXITLG)
476
+ return self.SUCCESS
477
+
478
+ # List the local files not uploaded under an object key prefix yet
479
+ # prefix - target object key prefix, including the trailing '/'
480
+ # lfiles - local file sizes, keyed by path relative to the source directory
481
+ # flist - object_glob() info, keyed by object key
482
+ def object_upload_missing(self, prefix, lfiles, flist):
483
+ """Return the local files that have no object of the same size under a key prefix.
484
+
485
+ Args:
486
+ prefix (str): Object key prefix including the trailing '/'.
487
+ lfiles (dict): Mapping of relative path → local file size.
488
+ flist (dict): Mapping of key → object info from object_glob().
489
+
490
+ Returns:
491
+ list: The relative paths still to upload.
492
+ """
493
+ miss = []
494
+ for name in lfiles:
495
+ oinfo = flist.get(prefix + name)
496
+ if not (oinfo and oinfo['data_size'] == lfiles[name]): miss.append(name)
497
+ return miss
498
+
413
499
  # Copy multiple files from a Globus endpoint to another
414
500
  # tofiles - target file name list, echo name leading with /dsnnn.n/ on Quasar and
415
501
  # leading with /data/ or /decsdata/ on local glade disk
@@ -52,7 +52,11 @@ class PgSIG(PgDBI):
52
52
  'MPROC': 1, # default number of multiple processes
53
53
  'BPROC': 1, # default number of multiple background processes
54
54
  'ETIME': 20, # default error waiting time (in seconds)
55
- 'WTIME': 120, # default waiting time (in seconds)
55
+ 'WTIME': 120, # default waiting time (in seconds), the cap of an adaptive wait
56
+ 'WTMIN': 5, # shortest adaptive wait (in seconds) for child/background processes
57
+ 'WTINC': 5, # seconds added to the adaptive wait after each loop still waiting
58
+ 'CWTIME': 0, # current adaptive wait (in seconds), 0 before the first wait
59
+ 'WLOOP': -1, # loop count of the last adaptive wait sleep
56
60
  'DTIME': 600, # the daemon record refresh time (in seconds)
57
61
  'RTIME': 2400, # the web rda config unlocking and unconfigured system down waiting time (in seconds)
58
62
  'CTIME': 4800, # the lock cleaning & configued system down waiting time (in seconds)
@@ -1281,21 +1285,52 @@ class PgSIG(PgDBI):
1281
1285
  if wtime: time.sleep(wtime)
1282
1286
  return wtime
1283
1287
 
1284
- # show wait message every dintv and then sleep for PGSIG['WTIME']
1288
+ # show wait message every 30 loops and then sleep for an adaptive wait time
1285
1289
  def show_wait_message(self, loop, msg, logact=None, dowait=0):
1286
1290
  """Log a wait message every 30 loops and optionally sleep.
1287
1291
 
1288
1292
  Args:
1289
- loop (int): Current loop iteration count.
1293
+ loop (int): Current loop iteration count, 0 for the first sleep of a wait.
1290
1294
  msg (str): Message to log.
1291
1295
  logact (int, optional): Log action flags. Defaults to ``LOGWRN``.
1292
- dowait (int, optional): If non-zero, sleep for ``PGSIG['WTIME']``
1293
- seconds. Defaults to 0.
1296
+ dowait (int, optional): If non-zero, sleep for the adaptive wait time
1297
+ from ``adaptive_wait_time()``, at most ``PGSIG['WTIME']`` seconds.
1298
+ Defaults to 0.
1294
1299
  """
1295
1300
  if logact is None: logact = self.LOGWRN
1296
1301
  if loop > 0 and (loop%30) == 0:
1297
1302
  self.pglog("{} at {}".format(msg, self.current_datetime()), logact)
1298
- if dowait: time.sleep(self.PGSIG['WTIME'])
1303
+ if dowait: time.sleep(self.adaptive_wait_time(loop))
1304
+
1305
+ # get the adaptive time to sleep for one loop of a wait for processes
1306
+ def adaptive_wait_time(self, loop):
1307
+ """Return how long to sleep for one loop of a wait for child/background processes.
1308
+
1309
+ The wait starts at ``PGSIG['WTMIN']`` seconds and grows by ``PGSIG['WTINC']``
1310
+ after each loop that still finds no process done, capped at the polling time
1311
+ ``PGSIG['WTIME']``, so long-running processes are not checked too often while
1312
+ short ones are not waited on for the full polling time. The current wait
1313
+ carries over to the next wait; it goes back to ``WTMIN`` when the previous
1314
+ wait was over after its first sleep, since the processes are short. Set
1315
+ ``PGSIG['WTMIN']`` to ``WTIME`` or larger for the old fixed wait.
1316
+
1317
+ Args:
1318
+ loop (int): Loop count of the current wait, 0 for its first sleep.
1319
+
1320
+ Returns:
1321
+ int: Seconds to sleep.
1322
+ """
1323
+ wmax = self.PGSIG['WTIME']
1324
+ wmin = min(self.PGSIG['WTMIN'], wmax)
1325
+ wtime = self.PGSIG['CWTIME']
1326
+ if loop == 0:
1327
+ if not wtime or self.PGSIG['WLOOP'] == 0: wtime = wmin
1328
+ else:
1329
+ wtime += self.PGSIG['WTINC']
1330
+ wtime = max(wmin, min(wtime, wmax))
1331
+ self.PGSIG['CWTIME'] = wtime
1332
+ self.PGSIG['WLOOP'] = loop
1333
+ return wtime
1299
1334
 
1300
1335
  # register a time out function to raise a time out error
1301
1336
  @contextmanager
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_common
3
- Version: 3.0.27
3
+ Version: 3.0.29
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
@@ -189,7 +189,7 @@ PgLOG.pglog("hello", PgLOG.LOGWRN)
189
189
  python -c "import rda_python_common; print(rda_python_common.__version__)"
190
190
  ```
191
191
 
192
- You should see the installed version (currently `3.0.27`). If the import
192
+ You should see the installed version (currently `3.0.29`). If the import
193
193
  fails, double-check that the active Python environment is the one where you
194
194
  ran `pip install`.
195
195