rda-python-common 3.0.9__tar.gz → 3.0.11__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.9/src/rda_python_common.egg-info → rda_python_common-3.0.11}/PKG-INFO +1 -1
  2. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/pyproject.toml +1 -1
  3. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pg_dbi.py +40 -127
  4. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pg_file.py +28 -4
  5. {rda_python_common-3.0.9 → rda_python_common-3.0.11/src/rda_python_common.egg-info}/PKG-INFO +1 -1
  6. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/LICENSE +0 -0
  7. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/README.md +0 -0
  8. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/setup.cfg +0 -0
  9. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/PgCMD.py +0 -0
  10. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/PgDBI.py +0 -0
  11. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/PgFile.py +0 -0
  12. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/PgLOG.py +0 -0
  13. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/PgLock.py +0 -0
  14. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/PgOPT.py +0 -0
  15. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/PgSIG.py +0 -0
  16. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/PgSplit.py +0 -0
  17. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/PgUtil.py +0 -0
  18. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/__init__.py +0 -0
  19. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pg_cmd.py +0 -0
  20. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pg_lock.py +0 -0
  21. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pg_log.py +0 -0
  22. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pg_opt.py +0 -0
  23. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pg_password.py +0 -0
  24. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pg_sig.py +0 -0
  25. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pg_split.py +0 -0
  26. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pg_util.py +0 -0
  27. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pgpassword.py +0 -0
  28. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common/pgpassword.usg +0 -0
  29. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common.egg-info/SOURCES.txt +0 -0
  30. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common.egg-info/dependency_links.txt +0 -0
  31. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common.egg-info/entry_points.txt +0 -0
  32. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common.egg-info/requires.txt +0 -0
  33. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/src/rda_python_common.egg-info/top_level.txt +0 -0
  34. {rda_python_common-3.0.9 → rda_python_common-3.0.11}/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.9
3
+ Version: 3.0.11
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rda_python_common"
7
- version = "3.0.9"
7
+ version = "3.0.11"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -85,7 +85,7 @@ class PgDBI(PgLOG):
85
85
  curtran (int): Transaction counter: 0 = idle, >0 = inside a transaction.
86
86
  NMISSES (list): Cached list of scientist IDs (userno) not found in the DB.
87
87
  LMISSES (list): Cached list of login names not found in the DB.
88
- USRWARN (int): Set to 1 once the 'run filluser' reminder has been logged.
88
+ USRWARN (int): Set to 1 once the incomplete-user reminder has been logged.
89
89
  TABLES (dict): Cache of table-field default info keyed by table name.
90
90
  SEQUENCES (dict): Cache of sequence field names keyed by table name.
91
91
  SPECIALIST (dict): Cache of specialist records keyed by dataset ID.
@@ -1702,19 +1702,17 @@ class PgDBI(PgLOG):
1702
1702
  return (self.SUCCESS if ret else self.FAILURE)
1703
1703
 
1704
1704
  def dssgrp_user_info(self, userno, logname = None):
1705
- """Build a partial dssdb.user record from dssdb.dssgrp.
1705
+ """Build a dssdb.user record from dssdb.dssgrp.
1706
1706
 
1707
- Fallback for when the UCAR People API is unreachable, so a usable user record
1708
- can still be added for a DECS group member. Fields absent from dssgrp are set
1709
- to the values implied by DECS membership; the record is marked stat_flag 'M'
1710
- so that 'filluser -i' completes it once the People API is available again.
1707
+ dssdb.dssgrp is the only source of specialist information; fields absent from
1708
+ it are set to the values implied by DECS group membership.
1711
1709
 
1712
1710
  Args:
1713
1711
  userno (int): Scientist number; pass 0 to look up by logname instead.
1714
1712
  logname (str | None): UCAR login name; used when userno is 0.
1715
1713
 
1716
1714
  Returns:
1717
- dict | None: Partial mapping of column_name → value suitable for
1715
+ dict | None: Mapping of column_name → value suitable for
1718
1716
  pgadd('dssdb.user'), or None when not in dssgrp.
1719
1717
  """
1720
1718
  cond = "userno = {}".format(userno) if userno else "logname = '{}'".format(logname)
@@ -1726,15 +1724,16 @@ class PgDBI(PgLOG):
1726
1724
  pgrec['org_name'] = 'NCAR'
1727
1725
  pgrec['country'] = 'UNITED.STATES'
1728
1726
  pgrec['email'] = pgrec['ucaremail'] = email
1729
- pgrec['stat_flag'] = 'M'
1727
+ pgrec['stat_flag'] = 'A'
1730
1728
  return pgrec
1731
1729
 
1732
1730
  def add_missed_user(self, userno, logname = None):
1733
1731
  """Add a dssdb.user record for a user missing from the table.
1734
1732
 
1735
- Tries the UCAR People API first, then dssgrp, and finally falls back to a bare
1736
- stub. A reminder to run filluser is logged once per process whenever the added
1737
- record is incomplete (stat_flag 'M').
1733
+ Builds the record from dssdb.dssgrp for a DECS group member. For anyone not
1734
+ in dssgrp, adds a record filled with whatever can be derived from the login
1735
+ name, using 'UNKNOWN' for the fields that cannot be filled in; such a record
1736
+ is marked stat_flag 'M' and logs a reminder once per process.
1738
1737
 
1739
1738
  Args:
1740
1739
  userno (int): Scientist number; pass 0 to add by logname instead.
@@ -1743,46 +1742,41 @@ class PgDBI(PgLOG):
1743
1742
  Returns:
1744
1743
  int: The new user.uid, or 0 if the insert fails.
1745
1744
  """
1746
- pgrec = self.ucar_user_info(userno, logname)
1747
- if not pgrec: pgrec = self.dssgrp_user_info(userno, logname)
1745
+ pgrec = self.dssgrp_user_info(userno, logname)
1748
1746
  if not pgrec:
1749
- pgrec = {'userno': userno} if userno else {'logname': logname}
1750
- pgrec['stat_flag'] = 'M'
1751
- if pgrec['stat_flag'] == 'M': self.incomplete_user_warning()
1747
+ pgrec = {'stat_flag': 'M', 'lstname': "UNKNOWN", 'fstname': "UNKNOWN"}
1748
+ if userno: pgrec['userno'] = userno
1749
+ if logname:
1750
+ pgrec['logname'] = logname
1751
+ pgrec['email'] = pgrec['ucaremail'] = logname + '@ucar.edu'
1752
+ pgrec['org_type'] = 'NCAR' # a UCAR login name outside of the DECS group
1753
+ pgrec['org_name'] = 'NCAR'
1754
+ pgrec['country'] = 'UNITED.STATES'
1755
+ else:
1756
+ pgrec['org_type'] = "OTHER"
1757
+ pgrec['org_name'] = pgrec['country'] = "UNKNOWN"
1758
+ self.incomplete_user_warning(userno, logname)
1752
1759
  return self.pgadd("dssdb.user", pgrec, (self.PGDBI['EXITLG']|self.AUTOID))
1753
1760
 
1754
- def incomplete_user_warning(self):
1755
- """Log a one-time reminder to run filluser for incomplete dssdb.user records."""
1756
- if self.USRWARN: return
1757
- self.USRWARN = 1
1758
- self.pglog("Incomplete record in dssdb.user; run 'filluser -i' later to complete it", self.LGWNEM)
1759
-
1760
- def reopen_user_record(self, pgrec):
1761
- """Reactivate a closed dssdb.user record and return its uid.
1762
-
1763
- Clears until_date and marks the record incomplete (stat_flag 'M') so that
1764
- 'filluser -i' refreshes it. Records that are not closed are returned as is.
1765
- Only call this for a present-day lookup; reopening on a historical lookup
1766
- would wrongly reactivate a user who has since left.
1761
+ def incomplete_user_warning(self, userno, logname = None):
1762
+ """Log a one-time reminder that an incomplete dssdb.user record was added.
1767
1763
 
1768
1764
  Args:
1769
- pgrec (dict): Record holding at least 'uid' and 'until_date'.
1770
-
1771
- Returns:
1772
- int: The uid of the record.
1765
+ userno (int): Scientist number of the added user; 0 if unknown.
1766
+ logname (str | None): UCAR login name of the added user; None if unknown.
1773
1767
  """
1774
- if pgrec['until_date']:
1775
- record = {'until_date': None, 'stat_flag': 'M'}
1776
- self.pgupdt("dssdb.user", record, "uid = {}".format(pgrec['uid']), self.PGDBI['ERRLOG'])
1777
- self.pglog("Closed user.uid = {} is reopened".format(pgrec['uid']), self.LGWNEM)
1778
- return pgrec['uid']
1768
+ if self.USRWARN: return
1769
+ self.USRWARN = 1
1770
+ who = logname if logname else userno
1771
+ self.pglog("{}: Not in dssdb.dssgrp; incomplete record added to dssdb.user".format(who), self.LGWNEM)
1779
1772
 
1780
1773
  def check_user_uid(self, userno, date = None):
1781
1774
  """Return the user.uid for a scientist ID, adding a record if missing.
1782
1775
 
1783
1776
  Looks up the active user record for userno on the given date. If not found,
1784
- logs a warning, attempts a date-range-independent lookup, and finally fetches
1785
- UCAR person info to insert a new user record.
1777
+ logs a warning and retries without the date range, returning the uid of a
1778
+ closed record as is; a closed record is never reactivated. Only when no
1779
+ record exists at all is a new one added from dssdb.dssgrp.
1786
1780
 
1787
1781
  Args:
1788
1782
  userno (int | str): UCAR scientist number.
@@ -1794,7 +1788,6 @@ class PgDBI(PgLOG):
1794
1788
  """
1795
1789
  if not userno: return 0
1796
1790
  if type(userno) is str: userno = int(userno)
1797
- istoday = (date is None)
1798
1791
  if date is None:
1799
1792
  datecond = "until_date IS NULL"
1800
1793
  date = 'today'
@@ -1806,8 +1799,8 @@ class PgDBI(PgLOG):
1806
1799
  self.pglog("{}: Scientist ID NOT on file for {}".format(userno, date), self.LGWNEM)
1807
1800
  self.NMISSES.append(userno)
1808
1801
  # check again if a user is on file with different date range
1809
- pgrec = self.pgget("dssdb.user", "uid, until_date", "userno = {}".format(userno), self.PGDBI['ERRLOG'])
1810
- if pgrec: return self.reopen_user_record(pgrec) if istoday else pgrec['uid']
1802
+ pgrec = self.pgget("dssdb.user", "uid", "userno = {}".format(userno), self.PGDBI['ERRLOG'])
1803
+ if pgrec: return pgrec['uid']
1811
1804
  uid = self.add_missed_user(userno)
1812
1805
  if uid: self.pglog("{}: Scientist ID Added as user.uid = {}".format(userno, uid), self.LGWNEM)
1813
1806
  return uid
@@ -1815,7 +1808,8 @@ class PgDBI(PgLOG):
1815
1808
  def get_user_uid(self, logname, date = None):
1816
1809
  """Return the user.uid for a UCAR login name, adding a record if missing.
1817
1810
 
1818
- Similar to check_user_uid() but looks up by logname instead of userno.
1811
+ Similar to check_user_uid() but looks up by logname instead of userno; a
1812
+ closed record is likewise returned as is and never reactivated.
1819
1813
 
1820
1814
  Args:
1821
1815
  logname (str): UCAR login name.
@@ -1825,7 +1819,6 @@ class PgDBI(PgLOG):
1825
1819
  int: user.uid on success, 0 if logname is falsy or insert fails.
1826
1820
  """
1827
1821
  if not logname: return 0
1828
- istoday = (not date)
1829
1822
  if not date:
1830
1823
  date = 'today'
1831
1824
  datecond = "until_date IS NULL"
@@ -1837,92 +1830,12 @@ class PgDBI(PgLOG):
1837
1830
  self.pglog("{}: UCAR Login Name NOT on file for {}".format(logname, date), self.LGWNEM)
1838
1831
  self.LMISSES.append(logname)
1839
1832
  # check again if a user is on file with different date range
1840
- pgrec = self.pgget("dssdb.user", "uid, until_date", "logname = '{}'".format(logname), self.PGDBI['ERRLOG'])
1841
- if pgrec: return self.reopen_user_record(pgrec) if istoday else pgrec['uid']
1833
+ pgrec = self.pgget("dssdb.user", "uid", "logname = '{}'".format(logname), self.PGDBI['ERRLOG'])
1834
+ if pgrec: return pgrec['uid']
1842
1835
  uid = self.add_missed_user(0, logname)
1843
1836
  if uid: self.pglog("{}: UCAR Login Name Added as user.uid = {}".format(logname, uid), self.LGWNEM)
1844
1837
  return uid
1845
1838
 
1846
- def ucar_user_info(self, userno, logname = None):
1847
- """Fetch UCAR person info for a scientist ID or login name via pgperson/pgusername.
1848
-
1849
- Runs the pgperson command-line tool and parses its key<=>value output.
1850
- Maps UCAR API fields to database column names, normalises country code,
1851
- organisation type, and employment dates.
1852
-
1853
- Args:
1854
- userno (int): Scientist number; pass 0 to look up by logname instead.
1855
- logname (str | None): UCAR login name; used when userno is 0.
1856
-
1857
- Returns:
1858
- dict | None: Mapping of column_name → value suitable for pgadd('dssdb.user'),
1859
- or None when pgperson returns no output.
1860
- """
1861
- matches = {
1862
- 'upid': "upid",
1863
- 'uid': "userno",
1864
- 'username': "logname",
1865
- 'lastName': "lstname",
1866
- 'firstName': "fstname",
1867
- 'active': "stat_flag",
1868
- 'internalOrg': "division",
1869
- 'externalOrg': "org_name",
1870
- 'country': "country",
1871
- 'forwardEmail': "email",
1872
- 'email': "ucaremail",
1873
- 'phone': "phoneno"
1874
- }
1875
- # pgperson/pgusername are installed by rda_python_metrics; skip quietly when the
1876
- # commands are not on this host so that callers can fall back to dssgrp
1877
- if not self.valid_command("pgperson"): return None
1878
- buf = self.pgsystem("pgperson " + ("-uid {}".format(userno) if userno else "-username {}".format(logname)), self.LOGWRN, 20)
1879
- if not buf: return None
1880
- pgrec = {}
1881
- for line in buf.split('\n'):
1882
- ms = re.match(r'^(.+)<=>(.*)$', line)
1883
- if ms:
1884
- (key, val) = ms.groups()
1885
- if key in matches:
1886
- if key == 'upid' and 'upid' in pgrec: break # get one record only
1887
- pgrec[matches[key]] = val
1888
- if not pgrec: return None
1889
- # the People API omits keys it has no value for, so read them defensively
1890
- if userno:
1891
- pgrec['userno'] = userno
1892
- elif pgrec.get('userno'):
1893
- pgrec['userno'] = userno = int(pgrec['userno'])
1894
- if pgrec.get('upid'): pgrec['upid'] = int(pgrec['upid'])
1895
- if pgrec.get('stat_flag'): pgrec['stat_flag'] = 'A' if pgrec['stat_flag'] == "True" else 'C'
1896
- email = pgrec.get('email')
1897
- if email and re.search(r'(@|\.)ucar\.edu$', email, re.I):
1898
- email = pgrec['email'] = pgrec.get('ucaremail')
1899
- pgrec['org_name'] = 'NCAR'
1900
- country = pgrec.get('country')
1901
- if email or country: pgrec['country'] = self.set_country_code(email, country)
1902
- val = "NCAR" if pgrec.get('division') else None
1903
- pgrec['org_type'] = self.get_org_type(val, email)
1904
- logname = pgrec.get('logname')
1905
- if not logname or not self.valid_command("pgusername"): return pgrec
1906
- buf = self.pgsystem("pgusername {}".format(logname), self.LOGWRN, 20)
1907
- if not buf: return pgrec
1908
- for line in buf.split('\n'):
1909
- ms = re.match(r'^(.+)<=>(.*)$', line)
1910
- if ms:
1911
- (key, val) = ms.groups()
1912
- if key == 'startDate':
1913
- m = re.match(r'^(\d+-\d+-\d+)\s', val)
1914
- if m:
1915
- pgrec['start_date'] = m.group(1)
1916
- else:
1917
- pgrec['start_date'] = val
1918
- if key == 'endDate':
1919
- m = re.match(r'^(\d+-\d+-\d+)\s', val)
1920
- if m:
1921
- pgrec['until_date'] = m.group(1)
1922
- else:
1923
- pgrec['until_date'] = val
1924
- return pgrec
1925
-
1926
1839
  def set_country_code(self, email, country = None):
1927
1840
  """Normalise a country name or derive it from an email domain.
1928
1841
 
@@ -233,9 +233,9 @@ class PgFile(PgUtil, PgSIG):
233
233
  if self.pgcmp(thost, self.OHOST, 1) == 0:
234
234
  return self.local_copy_object(tofile, fromfile, None, None, logact)
235
235
  elif self.pgcmp(thost, self.BHOST, 1) == 0:
236
- return self.local_copy_backup(tofile, fromfile, self.QPOINTS['B'], logact)
236
+ return self.wait_copy_backup(tofile, fromfile, self.QPOINTS['B'], logact)
237
237
  elif self.pgcmp(thost, self.DHOST, 1) == 0:
238
- return self.local_copy_backup(tofile, fromfile, self.QPOINTS['D'], logact)
238
+ return self.wait_copy_backup(tofile, fromfile, self.QPOINTS['D'], logact)
239
239
  else:
240
240
  return self.local_copy_remote(tofile, fromfile, tohost, logact)
241
241
  elif self.pgcmp(thost, self.LHOST, 1) == 0:
@@ -664,6 +664,25 @@ class PgFile(PgUtil, PgSIG):
664
664
  if not endpoint: endpoint = self.PGLOG['BACKUPEP']
665
665
  return self.endpoint_copy_endpoint(tofile, fromfile, endpoint, 'gdex-glade', logact)
666
666
 
667
+ # Copy a local file to Quasar backup tape system and wait until the Globus task is done
668
+ # for callers that cannot handle a self.FINISH return
669
+ def wait_copy_backup(self, tofile, fromfile, endpoint = None, logact = 0):
670
+ """Copy a local GLADE file to the Quasar backup endpoint and wait for completion.
671
+
672
+ Args:
673
+ tofile (str): Destination path on the backup endpoint (leading '/dsNNN.N/').
674
+ fromfile (str): Source local path (leading '/data/' or '/decsdata/').
675
+ endpoint (str | None): Globus endpoint name; defaults to PGLOG['BACKUPEP'].
676
+ logact (int): Logging action flags; default 0.
677
+
678
+ Returns:
679
+ int: self.SUCCESS or self.FAILURE.
680
+ """
681
+ if not endpoint: endpoint = self.PGLOG['BACKUPEP']
682
+ ret = self.local_copy_backup(tofile, fromfile, endpoint, logact)
683
+ if ret == self.FINISH: ret = self.check_globus_finished(tofile, endpoint, logact)
684
+ return ret
685
+
667
686
  # Copy a Quasar backup file to local Globus endpoint
668
687
  # tofile - target file name, leading with /data/ or /decsdata/
669
688
  # fromfile - source file name, leading with /dsnnn.n/
@@ -671,6 +690,9 @@ class PgFile(PgUtil, PgSIG):
671
690
  def backup_copy_local(self, tofile, fromfile, endpoint = None, logact = 0):
672
691
  """Copy a file from the Quasar backup endpoint to a local GLADE path via Globus.
673
692
 
693
+ Waits for the Globus task to finish before returning, so the local file is
694
+ complete for the caller.
695
+
674
696
  Args:
675
697
  tofile (str): Destination local path (leading '/data/' or '/decsdata/').
676
698
  fromfile (str): Source backup path (leading '/dsNNN.N/').
@@ -678,10 +700,12 @@ class PgFile(PgUtil, PgSIG):
678
700
  logact (int): Logging action flags; default 0.
679
701
 
680
702
  Returns:
681
- int: self.SUCCESS, self.FAILURE, or self.FINISH.
703
+ int: self.SUCCESS or self.FAILURE.
682
704
  """
683
705
  if not endpoint: endpoint = self.PGLOG['BACKUPEP']
684
- return self.endpoint_copy_endpoint(tofile, fromfile, 'gdex-glade', endpoint, logact)
706
+ ret = self.endpoint_copy_endpoint(tofile, fromfile, 'gdex-glade', endpoint, logact)
707
+ if ret == self.FINISH: ret = self.check_globus_finished(tofile, 'gdex-glade', logact)
708
+ return ret
685
709
 
686
710
  # Copy a remote file to local
687
711
  # tofile - target file name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_common
3
- Version: 3.0.9
3
+ Version: 3.0.11
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