rda-python-common 3.0.14__tar.gz → 3.0.16__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.14/src/rda_python_common.egg-info → rda_python_common-3.0.16}/PKG-INFO +2 -2
  2. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/README.md +1 -1
  3. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/pyproject.toml +1 -1
  4. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/__init__.py +1 -1
  5. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pg_dbi.py +111 -23
  6. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pg_file.py +61 -2
  7. {rda_python_common-3.0.14 → rda_python_common-3.0.16/src/rda_python_common.egg-info}/PKG-INFO +2 -2
  8. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/LICENSE +0 -0
  9. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/setup.cfg +0 -0
  10. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/PgCMD.py +0 -0
  11. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/PgDBI.py +0 -0
  12. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/PgFile.py +0 -0
  13. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/PgLOG.py +0 -0
  14. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/PgLock.py +0 -0
  15. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/PgOPT.py +0 -0
  16. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/PgSIG.py +0 -0
  17. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/PgSplit.py +0 -0
  18. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/PgUtil.py +0 -0
  19. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pg_cmd.py +0 -0
  20. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pg_lock.py +0 -0
  21. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pg_log.py +0 -0
  22. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pg_opt.py +0 -0
  23. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pg_password.py +0 -0
  24. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pg_sig.py +0 -0
  25. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pg_split.py +0 -0
  26. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pg_util.py +0 -0
  27. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pgpassword.py +0 -0
  28. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common/pgpassword.usg +0 -0
  29. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common.egg-info/SOURCES.txt +0 -0
  30. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common.egg-info/dependency_links.txt +0 -0
  31. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common.egg-info/entry_points.txt +0 -0
  32. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common.egg-info/requires.txt +0 -0
  33. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/src/rda_python_common.egg-info/top_level.txt +0 -0
  34. {rda_python_common-3.0.14 → rda_python_common-3.0.16}/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.14
3
+ Version: 3.0.16
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.14`). If the import
192
+ You should see the installed version (currently `3.0.16`). 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.14`). If the import
168
+ You should see the installed version (currently `3.0.16`). 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.14"
7
+ version = "3.0.16"
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.14"
25
+ __version__ = "3.0.16"
26
26
 
27
27
  __all__ = [
28
28
  "PgLOG",
@@ -76,7 +76,7 @@ class PgDBI(PgLOG):
76
76
  using psycopg (v3) when available, falling back to psycopg2. Supports single
77
77
  and batch INSERT, SELECT, UPDATE, and DELETE operations, transaction
78
78
  management, schema introspection, user lookups, usage tracking, and
79
- credential retrieval from .pgpass or OpenBao.
79
+ credential retrieval from the environment, .pgpass or OpenBao.
80
80
 
81
81
  Inherits all logging and utility helpers from PgLOG.
82
82
 
@@ -96,7 +96,10 @@ class PgDBI(PgLOG):
96
96
  set from psycopg/psycopg2 depending on which driver is in use.
97
97
  DBPORTS (dict): Mapping of database names to non-default TCP port numbers.
98
98
  DBPASS (dict): Credentials loaded from .pgpass, keyed by (host, port, db, user).
99
- DBBAOS (dict): Credentials loaded from OpenBao, keyed by database name.
99
+ DBBAOS (dict): Credentials loaded from OpenBao, keyed by db server node.
100
+ DBNODES (dict): Mapping of database names to their db server nodes, used when the
101
+ node cannot be taken from the host being connected to.
102
+ DBSKEYS (dict): Mapping of login names to secret key names ('<lnname>pass' by default).
100
103
  DBNAMES (dict): Mapping of schema names to their parent database names.
101
104
  DBSOCKS (dict): Mapping of database names to Unix socket paths.
102
105
  VIEWHOMES (dict): Mapping of hostnames to home directories for the view host.
@@ -139,6 +142,19 @@ class PgDBI(PgLOG):
139
142
  self.DBPORTS = {'default': 0}
140
143
  self.DBPASS = {}
141
144
  self.DBBAOS = {}
145
+ # hard coded db server nodes for dbnames, for hosts not named for their nodes; a node
146
+ # names the OpenBao path (kv/gdex/<node>) and the password environment variable
147
+ # (<NODE>_<KEY>) the same secrets are passed in through
148
+ self.DBNODES = {
149
+ 'ivaddb': 'pgdb03',
150
+ 'ispddb': 'pgdb03',
151
+ 'default': 'pgdb01',
152
+ }
153
+ # hard coded secret key names for db login names, defaulting to <lnname>pass
154
+ self.DBSKEYS = {
155
+ 'postgres': 'password',
156
+ 'metadata': 'metapass',
157
+ }
142
158
  # hard coded db names for given schema names
143
159
  self.DBNAMES = {
144
160
  'ivaddb': 'ivaddb',
@@ -2617,19 +2633,91 @@ class PgDBI(PgLOG):
2617
2633
  def get_pgpass_password(self):
2618
2634
  """Return the database password for the current connection settings.
2619
2635
 
2620
- Checks PGDBI['PWNAME'] first, then tries the .pgpass file (get_pgpassword()),
2621
- and finally falls back to OpenBao (get_baopassword()).
2636
+ Checks PGDBI['PWNAME'] first, then the environment (get_envpassword(), as set
2637
+ for containers on CIRRUS), then the .pgpass file (get_pgpassword()), and
2638
+ finally falls back to OpenBao (get_baopassword()). A password found on local
2639
+ or batch hosts is cached in the environment (set_envpassword()) so that the
2640
+ child processes inherit it instead of looking it up again.
2622
2641
 
2623
2642
  Returns:
2624
2643
  str | None: Password string, or None when no credential is found.
2625
2644
  """
2626
2645
  if self.PGDBI['PWNAME']: return self.PGDBI['PWNAME']
2646
+ pwname = self.get_envpassword()
2647
+ if pwname: return pwname
2627
2648
  pwname = self.get_pgpassword()
2628
2649
  if not pwname: pwname = self.get_baopassword()
2629
- if not pwname:
2630
- self.pglog("Unable to find password for {} in .pgpass or OpenBao".format(self.PGDBI['DBNAME']), self.PGDBI['ERRLOG'])
2650
+ if pwname:
2651
+ self.set_envpassword(pwname)
2652
+ else:
2653
+ self.pglog("Unable to find password for {} in environment, .pgpass or OpenBao".format(self.PGDBI['DBNAME']), self.PGDBI['ERRLOG'])
2631
2654
  return pwname
2632
2655
 
2656
+ def get_dbnodes(self):
2657
+ """Return the db server nodes holding the secrets of the current connection.
2658
+
2659
+ The node named by DBSHOST comes first, such as pgdb02 for the view-only host,
2660
+ so that a node with its own passwords is served its own secrets. The DBNODES
2661
+ node of the current DBNAME follows it as a fallback, such as pgdb01 for pgdb02,
2662
+ for a node that shares the passwords of the default one without holding secrets
2663
+ of its own.
2664
+
2665
+ Returns:
2666
+ list: Node names, such as ['pgdb02', 'pgdb01'], the first one preferred.
2667
+ """
2668
+ dbnodes = []
2669
+ ms = re.match(r'^(pgdb\d+)$', self.PGDBI['DBSHOST']) if self.PGDBI['DBSHOST'] else None
2670
+ if ms: dbnodes.append(ms.group(1))
2671
+ dbnode = self.DBNODES.get(self.PGDBI['DBNAME'], self.DBNODES['default'])
2672
+ if dbnode not in dbnodes: dbnodes.append(dbnode)
2673
+
2674
+ return dbnodes
2675
+
2676
+ def get_secret_name(self, dbnode):
2677
+ """Build the name of the secret holding the password of the current connection.
2678
+
2679
+ The key comes from DBSKEYS for the current LNNAME, defaulting to '<lnname>pass'.
2680
+ The pair names the secret in OpenBao (kv/gdex/<node> key <key>) and the
2681
+ environment variable (<NODE>_<KEY>) the same secret is passed in through
2682
+ on CIRRUS.
2683
+
2684
+ Args:
2685
+ dbnode (str): Db server node holding the secret, such as 'pgdb01'.
2686
+
2687
+ Returns:
2688
+ tuple: (node, key) naming the db server node and the secret key.
2689
+ """
2690
+ lnname = self.PGDBI['LNNAME']
2691
+
2692
+ return (dbnode, self.DBSKEYS.get(lnname, lnname + 'pass'))
2693
+
2694
+ def get_envpassword(self):
2695
+ """Look up the password in the environment, as set for containers on CIRRUS.
2696
+
2697
+ The variable name is the uppercased '<node>_<key>' secret name, for example
2698
+ PGDB01_DSSDBPASS for login name dssdb and PGDB03_IVADDBPASS for ivaddb. The
2699
+ fallback nodes of get_dbnodes() are tried in turn.
2700
+
2701
+ Returns:
2702
+ str | None: Password string, or None when no variable is set.
2703
+ """
2704
+ for dbnode in self.get_dbnodes():
2705
+ pwname = os.environ.get("{}_{}".format(*self.get_secret_name(dbnode)).upper())
2706
+ if pwname: return pwname
2707
+
2708
+ return None
2709
+
2710
+ def set_envpassword(self, pwname):
2711
+ """Cache a password in the environment under the name get_envpassword() reads.
2712
+
2713
+ Called for a password found in .pgpass or OpenBao, on local and PBS batch hosts,
2714
+ so that the child processes of the current one inherit it and skip the lookup.
2715
+
2716
+ Args:
2717
+ pwname (str): Password to pass on to the child processes.
2718
+ """
2719
+ os.environ["{}_{}".format(*self.get_secret_name(self.get_dbnodes()[0])).upper()] = pwname
2720
+
2633
2721
  def get_pgpassword(self):
2634
2722
  """Look up the password in the cached .pgpass credentials.
2635
2723
 
@@ -2646,16 +2734,20 @@ class PgDBI(PgLOG):
2646
2734
  return pwname
2647
2735
 
2648
2736
  def get_baopassword(self):
2649
- """Look up the password from OpenBao for the current database and login name.
2737
+ """Look up the password from OpenBao for the current db server node and login name.
2650
2738
 
2651
- Loads OpenBao secrets for PGDBI['DBNAME'] on first call (or when not cached).
2739
+ Loads the OpenBao secrets of a node on first call (or when not cached), trying
2740
+ the fallback nodes of get_dbnodes() in turn.
2652
2741
 
2653
2742
  Returns:
2654
2743
  str | None: Password string, or None when not found in OpenBao.
2655
2744
  """
2656
- dbname = self.PGDBI['DBNAME']
2657
- if dbname not in self.DBBAOS: self.read_openbao()
2658
- return self.DBBAOS[dbname].get(self.PGDBI['LNNAME'])
2745
+ for dbnode in self.get_dbnodes():
2746
+ if dbnode not in self.DBBAOS: self.read_openbao(dbnode)
2747
+ pwname = self.DBBAOS[dbnode].get(self.PGDBI['LNNAME'])
2748
+ if pwname: return pwname
2749
+
2750
+ return None
2659
2751
 
2660
2752
  def read_pgpass(self):
2661
2753
  """Read the .pgpass file and populate DBPASS with credentials.
@@ -2675,21 +2767,17 @@ class PgDBI(PgLOG):
2675
2767
  except Exception:
2676
2768
  pass
2677
2769
 
2678
- def read_openbao(self):
2679
- """Read OpenBao secrets and populate DBBAOS with credentials for DBNAME.
2770
+ def read_openbao(self, dbnode):
2771
+ """Read OpenBao secrets and populate DBBAOS with the credentials of a db server node.
2680
2772
 
2681
2773
  Uses the hvac client to fetch key-value secrets from the configured BAOURL.
2682
2774
  Parses keys matching 'pass' patterns to extract database usernames and passwords.
2775
+
2776
+ Args:
2777
+ dbnode (str): Db server node naming the secret path, such as 'pgdb01'.
2683
2778
  """
2684
- dbname = self.PGDBI['DBNAME']
2685
- self.DBBAOS[dbname] = {}
2686
- url = 'https://bao.k8s.ucar.edu/'
2687
- baopath = {
2688
- 'ivaddb': 'gdex/pgdb03',
2689
- 'ispddb': 'gdex/pgdb03',
2690
- 'default': 'gdex/pgdb01'
2691
- }
2692
- dbpath = baopath[dbname] if dbname in baopath else baopath['default']
2779
+ self.DBBAOS[dbnode] = {}
2780
+ dbpath = 'gdex/' + dbnode
2693
2781
  client = hvac.Client(url=self.PGDBI.get('BAOURL'))
2694
2782
  client.token = self.PGLOG.get('BAOTOKEN')
2695
2783
  try:
@@ -2711,4 +2799,4 @@ class PgDBI(PgLOG):
2711
2799
  baoname = 'metadata' if pre == 'meta' else pre
2712
2800
  elif suf == 'word':
2713
2801
  baoname = 'postgres'
2714
- if baoname: self.DBBAOS[dbname][baoname] = baos[key]
2802
+ if baoname: self.DBBAOS[dbnode][baoname] = baos[key]
@@ -766,7 +766,9 @@ class PgFile(PgUtil, PgSIG):
766
766
  """Download a file from the object store to the local filesystem.
767
767
 
768
768
  Changes to the target directory, downloads using isd_s3_cli, verifies size,
769
- sets permissions, and renames if needed. Retries once on failure.
769
+ sets permissions, and renames if needed. Retries once on failure. A key that
770
+ is a prefix of multiple objects, such as a zarr store, is downloaded as a
771
+ directory by object_copy_local_directory().
770
772
 
771
773
  Args:
772
774
  tofile (str): Destination local file path.
@@ -783,6 +785,28 @@ class PgFile(PgUtil, PgSIG):
783
785
  if not finfo:
784
786
  if finfo != None: return ret
785
787
  return self.lmsg(fromfile, "{}-{} to copy to {}".format(self.OHOST, self.PGLOG['MISSFILE'], tofile), logact)
788
+ if not finfo['isfile']: return self.object_copy_local_directory(tofile, fromfile, bucket, logact)
789
+ return self.object_get_local(tofile, fromfile, finfo['data_size'], bucket, logact)
790
+
791
+ # Download a single object of a known size to a local file
792
+ # tofile - target file name
793
+ # fromfile - source object key name
794
+ # fsize - size of the object, to verify the download against
795
+ # bucket - bucket name on Object store
796
+ def object_get_local(self, tofile, fromfile, fsize, bucket, logact = 0):
797
+ """Download one object key to a local file and verify its size.
798
+
799
+ Args:
800
+ tofile (str): Destination local file path.
801
+ fromfile (str): Object key (source path in the bucket).
802
+ fsize (int): Expected size of the object.
803
+ bucket (str): Source bucket.
804
+ logact (int): Logging action flags; default 0.
805
+
806
+ Returns:
807
+ int: self.SUCCESS on success, self.FAILURE on error.
808
+ """
809
+ ret = self.FAILURE
786
810
  ocmd = self.OBJCTCMD
787
811
  cmd = "{} go -k {} -b {}".format(ocmd, fromfile, bucket)
788
812
  fromname = op.basename(fromfile)
@@ -797,7 +821,7 @@ class PgFile(PgUtil, PgSIG):
797
821
  buf = self.pgsystem(cmd, logact, self.CMDBTH)
798
822
  info = self.check_local_file(fromname, 143, logact|self.PFSIZE) # 1+2+4+8+128
799
823
  if info:
800
- if info['data_size'] == finfo['data_size']:
824
+ if info['data_size'] == fsize:
801
825
  self.set_local_mode(fromfile, info['isfile'], 0, info['mode'], info['logname'], logact)
802
826
  if toname == fromname or self.move_local_file(toname, fromname, logact):
803
827
  ret = self.SUCCESS
@@ -811,6 +835,41 @@ class PgFile(PgUtil, PgSIG):
811
835
  self.change_local_directory(odir, logact)
812
836
  return ret
813
837
 
838
+ # Copy an object directory, a key prefix holding multiple objects, to local
839
+ # todir - target local directory name
840
+ # fromdir - source object key prefix
841
+ # bucket - bucket name on Object store
842
+ def object_copy_local_directory(self, todir, fromdir, bucket = None, logact = 0):
843
+ """Download every object under a key prefix into a local directory.
844
+
845
+ The object store has no directories; a zarr store and the like is a set of
846
+ objects sharing a key prefix. Each object is downloaded individually and
847
+ keeps its position relative to the prefix under todir.
848
+
849
+ Args:
850
+ todir (str): Destination local directory path.
851
+ fromdir (str): Object key prefix (source path in the bucket).
852
+ bucket (str | None): Source bucket; defaults to PGLOG['OBJCTBKT'].
853
+ logact (int): Logging action flags; default 0.
854
+
855
+ Returns:
856
+ int: self.SUCCESS on success, self.FAILURE on error.
857
+ """
858
+ if not bucket: bucket = self.PGLOG['OBJCTBKT']
859
+ ms = re.match(r'^(.+)/$', fromdir)
860
+ if ms: fromdir = ms.group(1)
861
+ flist = self.object_glob(fromdir, bucket, 0, logact)
862
+ if flist == self.FAILURE: return self.FAILURE
863
+ prefix = fromdir + '/'
864
+ keys = [key for key in flist if key.startswith(prefix)]
865
+ if not keys:
866
+ return self.lmsg(fromdir, "{}-{} to copy to {}".format(self.OHOST, self.PGLOG['MISSFILE'], todir), logact)
867
+ plen = len(prefix)
868
+ for key in keys:
869
+ tofile = "{}/{}".format(todir, key[plen:])
870
+ if not self.object_get_local(tofile, key, flist[key]['data_size'], bucket, logact): return self.FAILURE
871
+ return self.SUCCESS
872
+
814
873
  # Copy a remote file to object
815
874
  # tofile - target object file name
816
875
  # fromfile - source remote file name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_common
3
- Version: 3.0.14
3
+ Version: 3.0.16
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.14`). If the import
192
+ You should see the installed version (currently `3.0.16`). 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