rda-python-common 3.0.24__tar.gz → 3.0.26__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.24/src/rda_python_common.egg-info → rda_python_common-3.0.26}/PKG-INFO +2 -2
  2. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/README.md +1 -1
  3. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/pyproject.toml +1 -1
  4. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/__init__.py +1 -1
  5. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pg_file.py +133 -25
  6. {rda_python_common-3.0.24 → rda_python_common-3.0.26/src/rda_python_common.egg-info}/PKG-INFO +2 -2
  7. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/LICENSE +0 -0
  8. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/setup.cfg +0 -0
  9. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/PgCMD.py +0 -0
  10. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/PgDBI.py +0 -0
  11. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/PgFile.py +0 -0
  12. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/PgLOG.py +0 -0
  13. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/PgLock.py +0 -0
  14. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/PgOPT.py +0 -0
  15. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/PgSIG.py +0 -0
  16. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/PgSplit.py +0 -0
  17. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/PgUtil.py +0 -0
  18. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pg_cmd.py +0 -0
  19. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pg_dbi.py +0 -0
  20. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pg_lock.py +0 -0
  21. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pg_log.py +0 -0
  22. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pg_opt.py +0 -0
  23. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pg_password.py +0 -0
  24. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pg_sig.py +0 -0
  25. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pg_split.py +0 -0
  26. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pg_util.py +0 -0
  27. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pgpassword.py +0 -0
  28. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common/pgpassword.usg +0 -0
  29. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common.egg-info/SOURCES.txt +0 -0
  30. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common.egg-info/dependency_links.txt +0 -0
  31. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common.egg-info/entry_points.txt +0 -0
  32. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common.egg-info/requires.txt +0 -0
  33. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/src/rda_python_common.egg-info/top_level.txt +0 -0
  34. {rda_python_common-3.0.24 → rda_python_common-3.0.26}/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.24
3
+ Version: 3.0.26
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.24`). If the import
192
+ You should see the installed version (currently `3.0.26`). 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.24`). If the import
168
+ You should see the installed version (currently `3.0.26`). 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.24"
7
+ version = "3.0.26"
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.24"
25
+ __version__ = "3.0.26"
26
26
 
27
27
  __all__ = [
28
28
  "PgLOG",
@@ -794,7 +794,7 @@ class PgFile(PgUtil, PgSIG):
794
794
  # tofile - target file name
795
795
  # fromfile - source file name
796
796
  # bucket - bucket name on Object store
797
- def object_copy_local(self, tofile, fromfile, bucket = None, logact = 0):
797
+ def object_copy_local(self, tofile, fromfile, bucket = None, logact = 0, finfo = None):
798
798
  """Download a file from the object store to the local filesystem.
799
799
 
800
800
  Changes to the target directory, downloads using isd_s3_cli, verifies size,
@@ -807,31 +807,42 @@ class PgFile(PgUtil, PgSIG):
807
807
  fromfile (str): Object key (source path in the bucket).
808
808
  bucket (str | None): Source bucket; defaults to PGLOG['OBJCTBKT'].
809
809
  logact (int): Logging action flags; default 0.
810
+ finfo (dict | None): check_object_file() info for fromfile, from a call
811
+ with opt bit 1, when the caller already has it;
812
+ saves listing the object a second time.
810
813
 
811
814
  Returns:
812
815
  int: self.SUCCESS on success, self.FAILURE on error.
813
816
  """
814
817
  ret = self.FAILURE
815
818
  if not bucket: bucket = self.PGLOG['OBJCTBKT']
816
- finfo = self.check_object_file(fromfile, bucket, 0, logact)
819
+ # opt 1 adds the LastModified the 'lo' listing already carries, which
820
+ # object_get_local needs to tell whether tofile is staged already
821
+ if not finfo: finfo = self.check_object_file(fromfile, bucket, 1, logact)
817
822
  if not finfo:
818
823
  if finfo != None: return ret
819
824
  return self.lmsg(fromfile, "{}-{} to copy to {}".format(self.OHOST, self.PGLOG['MISSFILE'], tofile), logact)
820
- if not finfo['isfile']: return self.object_copy_local_directory(tofile, fromfile, bucket, logact)
821
- return self.object_get_local(tofile, fromfile, finfo['data_size'], bucket, logact)
825
+ if not finfo['isfile']:
826
+ return self.object_copy_local_directory(tofile, fromfile, bucket, logact, finfo.get('flist'))
827
+ return self.object_get_local(tofile, fromfile, finfo, bucket, logact)
822
828
 
823
829
  # Download a single object of a known size to a local file
824
830
  # tofile - target file name
825
831
  # fromfile - source object key name
826
- # fsize - size of the object, to verify the download against
832
+ # finfo - check_object_file() info of the object, to verify the download against
827
833
  # bucket - bucket name on Object store
828
- def object_get_local(self, tofile, fromfile, fsize, bucket, logact = 0):
834
+ def object_get_local(self, tofile, fromfile, finfo, bucket, logact = 0):
829
835
  """Download one object key to a local file and verify its size.
830
836
 
837
+ Nothing is fetched when tofile is already staged at the object's size and
838
+ no older than the object itself, unless logact carries OVRIDE, so the
839
+ second copy into the same file (dsarch makes one when it tars) is free.
840
+
831
841
  Args:
832
842
  tofile (str): Destination local file path.
833
843
  fromfile (str): Object key (source path in the bucket).
834
- fsize (int): Expected size of the object.
844
+ finfo (dict): check_object_file() info of the object; 'data_size' is
845
+ required, 'date_modified'/'time_modified' enable the skip.
835
846
  bucket (str): Source bucket.
836
847
  logact (int): Logging action flags; default 0.
837
848
 
@@ -839,6 +850,13 @@ class PgFile(PgUtil, PgSIG):
839
850
  int: self.SUCCESS on success, self.FAILURE on error.
840
851
  """
841
852
  ret = self.FAILURE
853
+ fsize = finfo['data_size']
854
+ if not logact&self.OVRIDE and 'date_modified' in finfo:
855
+ linfo = self.check_local_file(tofile, 1, logact)
856
+ if linfo and linfo['isfile'] and linfo['data_size'] == fsize and \
857
+ self.cmptime(linfo['date_modified'], linfo['time_modified'],
858
+ finfo['date_modified'], finfo['time_modified']) >= 0:
859
+ return self.SUCCESS
842
860
  ocmd = self.OBJCTCMD
843
861
  cmd = "{} go -k {} -b {}".format(ocmd, fromfile, bucket)
844
862
  fromname = op.basename(fromfile)
@@ -871,23 +889,28 @@ class PgFile(PgUtil, PgSIG):
871
889
  # todir - target local directory name
872
890
  # fromdir - source object key prefix
873
891
  # bucket - bucket name on Object store
874
- def object_copy_local_directory(self, todir, fromdir, bucket = None, logact = 0):
892
+ # flist - member listing from check_object_file(), if the caller has it
893
+ def object_copy_local_directory(self, todir, fromdir, bucket = None, logact = 0, flist = None):
875
894
  """Download every object under a key prefix into a local directory.
876
895
 
877
896
  The object store has no directories; a zarr store and the like is a set of
878
- objects sharing a key prefix. Each object is downloaded individually and
879
- keeps its position relative to the prefix under todir.
897
+ objects sharing a key prefix. The whole prefix comes down in one recursive
898
+ call, which is what makes this usable at all: such a store routinely holds
899
+ thousands of tiny members, and one process per member costs hours where
900
+ the recursive call costs seconds.
880
901
 
881
- A member already staged under todir at the object's size and no older than
882
- the object itself is left alone, so a store whose download was cut short
883
- (by a batch walltime, say) resumes where it stopped instead of starting
884
- over, and a second copy into the same directory costs nothing.
902
+ Nothing is fetched when every member is already staged under todir at the
903
+ object's size and no older than the object itself, so the second copy into
904
+ the same directory (dsarch makes one when it tars) is free.
885
905
 
886
906
  Args:
887
907
  todir (str): Destination local directory path.
888
908
  fromdir (str): Object key prefix (source path in the bucket).
889
909
  bucket (str | None): Source bucket; defaults to PGLOG['OBJCTBKT'].
890
910
  logact (int): Logging action flags; default 0.
911
+ flist (dict | None): Mapping of key → object info with LastModified, as
912
+ check_object_file() keeps it for a key prefix;
913
+ listed here when not given.
891
914
 
892
915
  Returns:
893
916
  int: self.SUCCESS on success, self.FAILURE on error.
@@ -895,25 +918,101 @@ class PgFile(PgUtil, PgSIG):
895
918
  if not bucket: bucket = self.PGLOG['OBJCTBKT']
896
919
  ms = re.match(r'^(.+)/$', fromdir)
897
920
  if ms: fromdir = ms.group(1)
898
- # opt 1 adds the LastModified the 'lo' listing already carries; it is only
899
- # bits 2 and 8 that would cost a metadata call per key
900
- flist = self.object_glob(fromdir, bucket, 1, logact)
901
- if flist == self.FAILURE: return self.FAILURE
921
+ if flist is None:
922
+ # opt 1 adds the LastModified the 'lo' listing already carries; it is only
923
+ # bits 2 and 8 that would cost a metadata call per key
924
+ flist = self.object_glob(fromdir, bucket, 1, logact)
925
+ if flist == self.FAILURE: return self.FAILURE
902
926
  prefix = fromdir + '/'
903
927
  keys = [key for key in flist if key.startswith(prefix)]
904
928
  if not keys:
905
929
  return self.lmsg(fromdir, "{}-{} to copy to {}".format(self.OHOST, self.PGLOG['MISSFILE'], todir), logact)
930
+ # a partly staged directory is downloaded again in full rather than member by
931
+ # member: one member costs about as much as the whole recursive call, so
932
+ # fetching even a handful individually is the slower way round
933
+ if not self.object_local_missing(todir, prefix, keys, flist, 1, logact):
934
+ # every member is current, but the key list is the authoritative one: a local
935
+ # file no longer under the prefix means the store was rewritten in place, and
936
+ # archiving it would keep members the store has dropped. start over instead
937
+ extra = self.local_extra_files(todir, prefix, keys)
938
+ if not extra: return self.SUCCESS
939
+ self.pglog("{}: restage over {} file(s) no longer in {}-{}".format(
940
+ todir, len(extra), self.OHOST, fromdir), logact|self.LOGWRN)
941
+ if not self.delete_local_file(todir, logact): return self.FAILURE
942
+ self.make_local_directory(todir, logact)
943
+ cmd = "{} go -k {} -b {} -ld {} -r".format(self.OBJCTCMD, fromdir, bucket, todir)
944
+ buf = self.pgsystem(cmd, logact, self.CMDBTH)
945
+ # verify on size alone; a member just downloaded is newer than the object by
946
+ # definition, and comparing timestamps here would only expose a clock skew
947
+ miss = self.object_local_missing(todir, prefix, keys, flist, 0, logact)
948
+ if miss:
949
+ return self.errlog("{}-{}: Error download {} of {} member(s) to {}\n{}".format(
950
+ self.OHOST, fromdir, len(miss), len(keys), todir, buf), 'O', 1, logact)
951
+ return self.SUCCESS
952
+
953
+ # List the members of an object key prefix that are not staged under todir yet
954
+ # todir - target local directory name
955
+ # prefix - source object key prefix, including the trailing '/'
956
+ # keys - the member keys to check
957
+ # flist - object_glob() info, keyed by member key
958
+ # chktime - also require the local copy to be no older than the object
959
+ def object_local_missing(self, todir, prefix, keys, flist, chktime = 0, logact = 0):
960
+ """Return the member keys that still need downloading under a key prefix.
961
+
962
+ A local copy counts as present when it matches the object size, and with
963
+ chktime also when it is no older than the object's LastModified.
964
+
965
+ Args:
966
+ todir (str): Destination local directory path.
967
+ prefix (str): Object key prefix including the trailing '/'.
968
+ keys (list): Member keys to check.
969
+ flist (dict): Mapping of key → object info from object_glob().
970
+ chktime (int): Also compare modification times; default 0.
971
+ logact (int): Logging action flags; default 0.
972
+
973
+ Returns:
974
+ list: The member keys not staged under todir.
975
+ """
976
+ miss = []
906
977
  plen = len(prefix)
907
978
  for key in keys:
908
979
  oinfo = flist[key]
909
980
  tofile = "{}/{}".format(todir, key[plen:])
910
981
  linfo = self.check_local_file(tofile, 1, logact)
911
- # an unparsable LastModified leaves date_modified unset; download again then
912
- if linfo and 'date_modified' in oinfo and linfo['data_size'] == oinfo['data_size'] and \
913
- self.cmptime(linfo['date_modified'], linfo['time_modified'],
914
- oinfo['date_modified'], oinfo['time_modified']) >= 0: continue
915
- if not self.object_get_local(tofile, key, oinfo['data_size'], bucket, logact): return self.FAILURE
916
- return self.SUCCESS
982
+ if linfo and linfo['data_size'] == oinfo['data_size']:
983
+ if not chktime: continue
984
+ # an unparsable LastModified leaves date_modified unset; fetch again then
985
+ if 'date_modified' in oinfo and \
986
+ self.cmptime(linfo['date_modified'], linfo['time_modified'],
987
+ oinfo['date_modified'], oinfo['time_modified']) >= 0: continue
988
+ miss.append(key)
989
+ return miss
990
+
991
+ # List the files staged under todir that the object key prefix no longer holds
992
+ # todir - target local directory name
993
+ # prefix - source object key prefix, including the trailing '/'
994
+ # keys - the member keys the object store currently holds
995
+ def local_extra_files(self, todir, prefix, keys):
996
+ """Return the paths under todir that are not members of an object key prefix.
997
+
998
+ Paths are relative to todir, matching the layout a recursive download creates.
999
+
1000
+ Args:
1001
+ todir (str): Local directory holding the staged copy.
1002
+ prefix (str): Object key prefix including the trailing '/'.
1003
+ keys (list): The member keys currently under the prefix.
1004
+
1005
+ Returns:
1006
+ list: Relative paths under todir with no matching member key.
1007
+ """
1008
+ plen = len(prefix)
1009
+ members = set(key[plen:] for key in keys)
1010
+ extra = []
1011
+ for path, dirs, files in os.walk(todir):
1012
+ for fname in files:
1013
+ name = op.relpath(op.join(path, fname), todir)
1014
+ if name not in members: extra.append(name)
1015
+ return extra
917
1016
 
918
1017
  # Copy a remote file to object
919
1018
  # tofile - target object file name
@@ -2376,6 +2475,8 @@ class PgFile(PgUtil, PgSIG):
2376
2475
 
2377
2476
  Strips trailing '/'. Uses opt bit 64 to skip the retry. Fetches metadata
2378
2477
  (uhash) when opt includes bits 2, 4, or 8. Resets ECNTS['O'] on success.
2478
+ Only the key itself, or the keys under it as a directory, count as a
2479
+ match; other keys that merely start with the same characters are ignored.
2379
2480
 
2380
2481
  Args:
2381
2482
  file (str): Object key to check.
@@ -2401,7 +2502,11 @@ class PgFile(PgUtil, PgSIG):
2401
2502
  if buf:
2402
2503
  if re.match(r'^\[\]', buf): break
2403
2504
  if re.match(r'^\[\{', buf):
2404
- ary = json.loads(buf)
2505
+ # 'lo' matches by key prefix, so x.zarr also lists a sibling x.zarr2/...
2506
+ # and a.nc lists a.nc.bak; keep only the key itself and keys under it
2507
+ prefix = file + '/'
2508
+ ary = [a for a in json.loads(buf) if a['Key'] == file or a['Key'].startswith(prefix)]
2509
+ if not ary: break
2405
2510
  cnt = len(ary)
2406
2511
  isdir = (cnt > 1 or ary[0]['Key'] != file)
2407
2512
  # a key prefix has no timestamp of its own; report the newest member's
@@ -2419,6 +2524,9 @@ class PgFile(PgUtil, PgSIG):
2419
2524
  for a in ary:
2420
2525
  size += int(a['Size'])
2421
2526
  ret['data_size'] = size
2527
+ # keep the member listing, as object_glob(file, bucket, 1) builds it,
2528
+ # so a download of the prefix does not list it a second time
2529
+ ret['flist'] = {a['Key']: self.object_file_stat(a, None, 1) for a in ary}
2422
2530
  uhash = None
2423
2531
  break
2424
2532
  if opt&64: return self.FAILURE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_common
3
- Version: 3.0.24
3
+ Version: 3.0.26
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.24`). If the import
192
+ You should see the installed version (currently `3.0.26`). 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