rda-python-dsarch 3.0.12__tar.gz → 3.0.14__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 (21) hide show
  1. {rda_python_dsarch-3.0.12/src/rda_python_dsarch.egg-info → rda_python_dsarch-3.0.14}/PKG-INFO +1 -1
  2. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/pyproject.toml +1 -1
  3. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch/dsarch.py +3 -3
  4. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch/dsarch.usg +42 -6
  5. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch/pg_arch.py +240 -8
  6. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14/src/rda_python_dsarch.egg-info}/PKG-INFO +1 -1
  7. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/LICENSE +0 -0
  8. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/MANIFEST.in +0 -0
  9. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/README.md +0 -0
  10. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/setup.cfg +0 -0
  11. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch/PgArch.py +0 -0
  12. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch/PgMeta.py +0 -0
  13. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch/__init__.py +0 -0
  14. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch/ds_arch.py +0 -0
  15. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch/pg_meta.py +0 -0
  16. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch.egg-info/SOURCES.txt +0 -0
  17. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch.egg-info/dependency_links.txt +0 -0
  18. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch.egg-info/entry_points.txt +0 -0
  19. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch.egg-info/requires.txt +0 -0
  20. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/src/rda_python_dsarch.egg-info/top_level.txt +0 -0
  21. {rda_python_dsarch-3.0.12 → rda_python_dsarch-3.0.14}/tests/test_dsarch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsarch
3
- Version: 3.0.12
3
+ Version: 3.0.14
4
4
  Summary: RDA Python package for utility program dsarch archive RDA data
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-dsarch
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rda_python_dsarch"
7
- version = "3.0.12"
7
+ version = "3.0.14"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -2049,7 +2049,7 @@ class DsArch(PgArch, PgMeta):
2049
2049
  self.print_row_format(pgrec, fnames, hash)
2050
2050
  self.view_keyvalues(dsid, kvalues, getkeys)
2051
2051
  if fnames and 'PE' in self.params: self.get_period_info()
2052
- if 'WN' in self.params: self.view_filenumber(dsid, 0)
2052
+ if 'WN' in self.params or 'WS' in self.params: self.view_filenumber(dsid, 0)
2053
2053
  return 1 # get one dataset record
2054
2054
 
2055
2055
  def get_period_info(self):
@@ -2206,7 +2206,7 @@ class DsArch(PgArch, PgMeta):
2206
2206
  onames = self.params['ON'] if 'ON' in self.params else "I"
2207
2207
  qnames = fnames
2208
2208
  if fnames.find('Y') > -1 and fnames.find('X') < 0: qnames += 'X'
2209
- if 'WN' in self.params and fnames.find('I') < 0: qnames += 'I'
2209
+ if ('WN' in self.params or 'WS' in self.params) and fnames.find('I') < 0: qnames += 'I'
2210
2210
  qnames += self.append_order_fields(onames, qnames, tname, "Y")
2211
2211
  condition = self.get_condition(tname)
2212
2212
  if 'ON' in self.params and ('OB' in self.params or re.search(r'Y', onames, re.I)):
@@ -2227,7 +2227,7 @@ class DsArch(PgArch, PgMeta):
2227
2227
  cnt = self.print_column_format(pgrecs, fnames, hash, lens)
2228
2228
  s = 's' if cnt > 1 else ''
2229
2229
  self.pglog("{} group record{} retrieved".format(cnt, s), self.LOGWRN)
2230
- if 'WN' in self.params: self.view_filenumber(dsid, pgrecs['gindex'], cnt)
2230
+ if 'WN' in self.params or 'WS' in self.params: self.view_filenumber(dsid, pgrecs['gindex'], cnt)
2231
2231
  else:
2232
2232
  self.pglog("No group found for " + condition, self.LOGWRN)
2233
2233
 
@@ -609,10 +609,11 @@ QSCAT observations.<:>
609
609
  Results can be filtered by group index (-GI), name (-GN), or parent
610
610
  index (-PI). The -GN option accepts the '%' wildcard.
611
611
 
612
- For example, to retrieve the default group fields and file counts for
613
- groups 1 and 2 of d260001:
612
+ For example, to retrieve the default group fields, the file numbers
613
+ and sizes, and the storage and Quasar backup checks for groups 1 and
614
+ 2 of d260001:
614
615
 
615
- dsarch d260001 GG -WN -GI 1 2
616
+ dsarch d260001 GG -WN -WS -GI 1 2
616
617
 
617
618
  <<Content of the output>>
618
619
  Dataset<=>d260001
@@ -621,7 +622,13 @@ GroupIndex<:>GroupName<:>ParentIndex<:>Title<:>GroupPattern<:>BackupFlag<:>Saved
621
622
  2<:>MONTHLY<:>0<:>MONTHLY OAFlux Products<:><:>P<:><:>monthly<:>
622
623
  2 group records retrieved
623
624
  d260001-G1: DS-11094192261 DC-327 WC-327 SS-8909173908 SC-51
625
+ d260001-G1 Web Files: Total 327(11.09GB), Quasar Backup&DR 327(11.09GB)
626
+ d260001-G1 Saved Files: Total 51(8.91GB), Quasar Backup&DR 51(8.91GB)
624
627
  d260001-G2: DS-552780725 DC-522 WC-522 SS-465789806 SC-51
628
+ d260001-G2 Web Files: Total 522(552.78MB), Missing on Glade 2(2.12MB) [Quasar Backup&DR 1(1.06MB), Not Backed Up 1(1.06MB)], Quasar Backup&DR 521(551.72MB), Not Backed Up 1(1.06MB)
629
+ d260001-G2-W-D: monthly/2025/oaflux_202511.nc: Missing on Glade; on Quasar Backup&DR (Backup ID 3456789)
630
+ d260001-G2-W-D: monthly/2025/oaflux_202512.nc: Missing on Glade; Not Backed Up
631
+ d260001-G2 Saved Files: Total 51(465.79MB), Quasar Backup&DR 51(465.79MB)
625
632
 
626
633
 
627
634
  3.3.3 Delete Group Information
@@ -1909,12 +1916,41 @@ behavior.
1909
1916
 
1910
1917
  -WN or -WithFileNumber, recomputes and updates Saved/Web file
1911
1918
  counts for the dataset and any specified groups. With -GD
1912
- (-GetDataset), the counts are calculated on the fly and included
1913
- in the output.
1919
+ (-GetDataset) or -GG (-GetGroup), shows the Web/Saved file numbers
1920
+ of the dataset or each group, and checks their files as described
1921
+ below.
1914
1922
 
1915
1923
  -WS or -WithFileSize, rechecks Web file sizes and timestamps for
1916
1924
  provided web files with -WF (-WebFile), and updates them into GDEXDB
1917
- This Mode option works with action -SW (SetWebFile) only.
1925
+ with action -SW (SetWebFile). With -GD (-GetDataset) or -GG
1926
+ (-GetGroup), shows the Web/Saved file sizes of the dataset or each
1927
+ group, and checks their files as described below.
1928
+
1929
+ With -GD/-GG, either -WN or -WS also checks every Web and Saved file
1930
+ (including the files of sub groups for a group) that is not deleted:
1931
+ - a file with Location Flag G or B must exist on Glade, and one
1932
+ with Location Flag O or B on Boreas (object store);
1933
+ - a Saved file on Glade (Location Flag G or B) is looked for in
1934
+ COLD_STORAGE, <decsdata>/COLD_STORAGE/<dsid> or
1935
+ <decsdata>/cold_storage_<YYYYMMDD>/COLD_STORAGE/<dsid>, and is
1936
+ counted as In COLD_STORAGE whether or not it is on Glade too;
1937
+ - its Quasar backup, by the backup file (bfile) of its Backup ID:
1938
+ type B for Quasar Backup, type D for Quasar Backup & Disaster
1939
+ Recovery, counted once the backup file status is A; Pending
1940
+ when it is not, and Not Backed Up when it has no Backup ID.
1941
+ Files with Location Flag C (CGD) or R (remote) are not stored by
1942
+ GDEX and are not checked. One summary line per file category shows
1943
+ the file counts with -WN and the total file sizes with -WS (both as
1944
+ Count(Size) when both options are given, sizes in KB/MB/GB/TB): the
1945
+ Total, then the result of each check. Each Missing on Glade/Boreas
1946
+ count is followed by the Quasar status of its files in [], e.g.
1947
+ [Quasar Backup&DR 1(1.06MB), Not Backed Up 1(1.06MB)], so it tells
1948
+ how many of the missing files can be restored with action -RQ and
1949
+ how many are lost; for Saved files it also tells how many are In
1950
+ COLD_STORAGE. The Quasar counts after them are of all the files,
1951
+ missing or not. It is followed by one line for every file missing or
1952
+ not backed up yet; the line of a missing file also tells its Quasar
1953
+ backup, or that it is not backed up.
1918
1954
 
1919
1955
  -XC or -CrossCopy, used with -AW or -AS to fill gaps in storage:
1920
1956
  copies files from Glade disk to bOreas (or vice versa) wherever
@@ -21,6 +21,7 @@ and caching dataset/group metadata.
21
21
  import time
22
22
  import re
23
23
  import os
24
+ import glob
24
25
  from os import path as op
25
26
  from rda_python_common.pg_split import PgSplit
26
27
  from rda_python_common.pg_cmd import PgCMD
@@ -61,6 +62,8 @@ class PgArch(PgOPT, PgCMD, PgSplit):
61
62
  'SP' : '', # dataset savedpath
62
63
  }
63
64
  self.webpaths = {}
65
+ self.OBJKEYS = {} # object keys listed per bucket:dsid, by object_file_exists()
66
+ self.COLDDIRS = {} # cold storage directories found per dsid, by cold_storage_file()
64
67
  self.savedpaths = {}
65
68
  self.grouptypes = {}
66
69
  self.CORDERS = {}
@@ -1417,27 +1420,256 @@ class PgArch(PgOPT, PgCMD, PgSplit):
1417
1420
  else:
1418
1421
  cnt = 1
1419
1422
  grecs = [gindex]
1423
+ # -WN shows the file numbers and -WS the file sizes; either one also checks the
1424
+ # files on storage and on Quasar, see check_file_storage()
1425
+ wn = 'WN' in self.params
1426
+ ws = 'WS' in self.params
1420
1427
  fields = ("dwebcnt, webcnt, cdwcnt, nwebcnt, dweb_size, nweb_size, savedcnt, saved_size")
1428
+ dcnd = "dsid = '{}'".format(dsid)
1421
1429
  for i in range(cnt):
1422
1430
  if grecs[i] == 0:
1423
1431
  table = "dataset"
1424
- cnd = "dsid = '{}'".format(dsid)
1432
+ cnd = dcnd
1425
1433
  head = dsid
1426
1434
  else:
1427
1435
  table = "dsgroup"
1428
- cnd = "dsid = '{}' AND gindex = {}".format(dsid, grecs[i])
1436
+ cnd = "{} AND gindex = {}".format(dcnd, grecs[i])
1429
1437
  head = "{}-G{}".format(dsid, grecs[i])
1430
1438
  pgrec = self.pgget(table, fields, cnd, self.PGOPT['extlog'])
1431
1439
  if pgrec:
1432
1440
  msg = ''
1433
1441
  if pgrec['webcnt']:
1434
- if pgrec['dwebcnt']: msg += " DS-{} DC-{}".format(pgrec['dweb_size'], pgrec['dwebcnt'])
1435
- if pgrec['cdwcnt']: msg += " CDC-{}".format(pgrec['cdwcnt'])
1436
- if pgrec['nwebcnt']: msg += " NS-{} NC-{}".format(pgrec['nweb_size'], pgrec['nwebcnt'])
1437
- msg += " WC-{}".format(pgrec['webcnt'])
1442
+ if pgrec['dwebcnt']:
1443
+ if ws: msg += " DS-{}".format(pgrec['dweb_size'])
1444
+ if wn: msg += " DC-{}".format(pgrec['dwebcnt'])
1445
+ if pgrec['cdwcnt'] and wn: msg += " CDC-{}".format(pgrec['cdwcnt'])
1446
+ if pgrec['nwebcnt']:
1447
+ if ws: msg += " NS-{}".format(pgrec['nweb_size'])
1448
+ if wn: msg += " NC-{}".format(pgrec['nwebcnt'])
1449
+ if wn: msg += " WC-{}".format(pgrec['webcnt'])
1438
1450
  if pgrec['savedcnt']:
1439
- msg += " SS-{} SC-{}".format(pgrec['saved_size'], pgrec['savedcnt'])
1440
- if msg: print("{}:{}".format(head, msg))
1451
+ if ws: msg += " SS-{}".format(pgrec['saved_size'])
1452
+ if wn: msg += " SC-{}".format(pgrec['savedcnt'])
1453
+ if msg: self.OUTPUT.write("{}:{}\n".format(head, msg))
1454
+ # group counts include the sub groups, so check the files of all of them
1455
+ gidxs = self.get_all_subgroups(dcnd, grecs[i]) if grecs[i] else None
1456
+ self.check_file_storage(dsid, head, gidxs, wn, ws)
1457
+
1458
+ # Quasar backup status of a file, by bfile.type and bfile.status of its bid
1459
+ QBSTATS = (('B', 'Quasar Backup'), ('D', 'Quasar Backup&DR'),
1460
+ ('P', 'Quasar Backup Pending'), ('N', 'Not Backed Up'))
1461
+ # where a missing file was looked for, by locflag; 'C' (CGD) and 'R' (remote) files
1462
+ # are not stored by GDEX and are checked neither on storage nor on Quasar
1463
+ # 'M' is the total of files missing, each counted once even when missing in both
1464
+ # places; a saved file found in COLD_STORAGE is not missing
1465
+ # each 'Missing on' count is broken down by the Quasar status of its files, so
1466
+ # it tells how many of them can still be restored and how many are lost; the
1467
+ # Quasar counts that follow are of all the files, missing or not
1468
+ QMSTATS = (('G', 'Missing on Glade'), ('O', 'Missing on Boreas'),
1469
+ ('C', 'In COLD_STORAGE'), ('X', 'Not Checked (CGD/Remote)'))
1470
+
1471
+ def check_file_storage(self, dsid, head, gidxs, wn, ws):
1472
+ """
1473
+ Check the web and saved files of a dataset or group on storage and Quasar.
1474
+
1475
+ For every web/saved file that is not deleted: a Glade copy (locflag G or B)
1476
+ must exist under the dataset home, a Boreas copy (locflag O or B) must exist
1477
+ in the object store, and a Glade saved file is looked for under COLD_STORAGE,
1478
+ counted there whether or not it is still on Glade too. The Quasar backup status comes from bfile.type ('B' backup,
1479
+ 'D' backup and disaster recovery) of the file's bid, counted only once the
1480
+ backup file status is 'A'. Writes the totals and every missing or not
1481
+ backed up file to self.OUTPUT.
1482
+
1483
+ Args:
1484
+ dsid -- dataset ID string
1485
+ head -- line prefix, dsid or dsid-G<gindex>
1486
+ gidxs -- group indices to check, None for the whole dataset
1487
+ wn -- show file numbers (-WN)
1488
+ ws -- show file sizes (-WS)
1489
+ """
1490
+ if gidxs is None:
1491
+ gcnd = ''
1492
+ elif len(gidxs) == 1:
1493
+ gcnd = " AND {{0}}.gindex = {}".format(gidxs[0])
1494
+ else:
1495
+ gcnd = " AND {{0}}.gindex IN ({})".format(', '.join(str(g) for g in gidxs))
1496
+ bflds = "bfile.type btype, bfile.status bstat"
1497
+ bjoin = "LEFT JOIN bfile ON {0}.bid = bfile.bid"
1498
+ wrecs = self.pgmget_wfile_join(dsid, bjoin.format('wfile'),
1499
+ "wfile.wid fid, wfile.wfile fname, wfile.type, wfile.data_size, " +
1500
+ "wfile.locflag, wfile.bid, " + bflds,
1501
+ "wfile.status <> 'D'" + gcnd.format('wfile'), self.PGOPT['extlog'])
1502
+ srecs = self.pgmget("sfile " + bjoin.format('sfile'),
1503
+ "sfile.sid fid, sfile.sfile fname, sfile.type, sfile.data_size, " +
1504
+ "sfile.locflag, sfile.bid, " + bflds,
1505
+ "sfile.dsid = '{}' AND sfile.status <> 'D'".format(dsid) + gcnd.format('sfile'),
1506
+ self.PGOPT['extlog'])
1507
+ for (cate, frecs) in (('W', wrecs), ('S', srecs)):
1508
+ fcnt = len(frecs['fid']) if frecs else 0
1509
+ if fcnt: self.view_file_storage(dsid, head, cate, frecs, fcnt, wn, ws)
1510
+
1511
+ def view_file_storage(self, dsid, head, cate, frecs, fcnt, wn, ws):
1512
+ """
1513
+ Check one category (W-web or S-saved) of file records and write the report.
1514
+
1515
+ Args:
1516
+ dsid -- dataset ID string
1517
+ head -- line prefix, dsid or dsid-G<gindex>
1518
+ cate -- 'W' for web files, 'S' for saved files
1519
+ frecs -- multi-record dict from check_file_storage()
1520
+ fcnt -- number of records in frecs
1521
+ wn -- show file numbers (-WN)
1522
+ ws -- show file sizes (-WS)
1523
+ """
1524
+ self.cache_rtpath(dsid)
1525
+ bucket = 'gdex-decsdata' if cate == 'S' else self.PGLOG['OBJCTBKT']
1526
+ cname = 'Saved' if cate == 'S' else 'Web'
1527
+ total = [0, 0]
1528
+ bcnts = {k: [0, 0] for (k, m) in self.QBSTATS}
1529
+ mcnts = {k: [0, 0] for (k, m) in self.QMSTATS}
1530
+ # Quasar status (and COLD_STORAGE for saved files) of the files missing per location
1531
+ mbcnts = {'G': {}, 'O': {}}
1532
+ lines = []
1533
+ for i in range(fcnt):
1534
+ fname = frecs['fname'][i]
1535
+ ftype = frecs['type'][i]
1536
+ fsize = frecs['data_size'][i] or 0
1537
+ locflag = frecs['locflag'][i]
1538
+ finfo = "{}-{}-{}: {}".format(head, cate, ftype, fname)
1539
+ total[0] += 1
1540
+ total[1] += fsize
1541
+ if locflag in ('C', 'R'):
1542
+ # not stored by GDEX, so neither on storage nor on Quasar
1543
+ mcnts['X'][0] += 1
1544
+ mcnts['X'][1] += fsize
1545
+ continue
1546
+ else:
1547
+ # quasar backup, by the backup file of the bid
1548
+ if not frecs['bid'][i]:
1549
+ bkey = 'N'
1550
+ elif frecs['bstat'][i] == 'A' and frecs['btype'][i] in ('B', 'D'):
1551
+ bkey = frecs['btype'][i]
1552
+ else:
1553
+ bkey = 'P'
1554
+ bcnts[bkey][0] += 1
1555
+ bcnts[bkey][1] += fsize
1556
+ bmsg = dict(self.QBSTATS)[bkey]
1557
+ if bkey != 'N': bmsg += " (Backup ID {})".format(frecs['bid'][i])
1558
+ # physical copies
1559
+ msgs = []
1560
+ if locflag != 'O':
1561
+ if cate == 'S':
1562
+ lfile = "{}/{}/{}".format(self.RTPATH['SP'], ftype, fname)
1563
+ else:
1564
+ lfile = self.join_paths(self.RTPATH.get(ftype, self.RTPATH['D']), fname)
1565
+ # every Glade saved file is looked for in cold storage, so the count tells
1566
+ # how much of the saved data is on tape, whether or not it is on Glade too
1567
+ cfile = self.cold_storage_file(dsid, ftype, fname) if cate == 'S' else None
1568
+ if cfile:
1569
+ mcnts['C'][0] += 1
1570
+ mcnts['C'][1] += fsize
1571
+ if not op.exists(lfile):
1572
+ mcnts['G'][0] += 1
1573
+ mcnts['G'][1] += fsize
1574
+ self.add_file_count(mbcnts['G'], bkey, fsize)
1575
+ msg = self.QMSTATS[0][1]
1576
+ if cfile:
1577
+ self.add_file_count(mbcnts['G'], 'C', fsize)
1578
+ msg += ", Found in COLD_STORAGE " + cfile
1579
+ msgs.append(msg)
1580
+ if locflag in ('O', 'B'):
1581
+ if not self.object_file_exists(dsid, fname, bucket):
1582
+ mcnts['O'][0] += 1
1583
+ mcnts['O'][1] += fsize
1584
+ self.add_file_count(mbcnts['O'], bkey, fsize)
1585
+ msgs.append(self.QMSTATS[1][1])
1586
+ # one line per file: where it is missing and where it is backed up, so a
1587
+ # missing file shows whether it can be restored; a present file is listed
1588
+ # only when it is not backed up yet
1589
+ if msgs:
1590
+ msgs.append("on " + bmsg if bkey in ('B', 'D') else bmsg)
1591
+ elif bkey in ('N', 'P'):
1592
+ msgs.append(bmsg)
1593
+ if msgs: lines.append("{}: {}".format(finfo, '; '.join(msgs)))
1594
+ msg = "{} {} Files: Total {}".format(head, cname, self.file_number_size(total, wn, ws))
1595
+ for (k, m) in self.QMSTATS:
1596
+ if not mcnts[k][0]: continue
1597
+ msg += ", {} {}".format(m, self.file_number_size(mcnts[k], wn, ws))
1598
+ if k in mbcnts:
1599
+ # e.g. Missing on Glade 6309(65.73GB) [Quasar Backup 10(1.20GB), Not Backed Up 6299(64.53GB)]
1600
+ subs = [(bk, bm) for (bk, bm) in self.QBSTATS] + [('C', self.QMSTATS[2][1])]
1601
+ parts = ["{} {}".format(bm, self.file_number_size(mbcnts[k][bk], wn, ws))
1602
+ for (bk, bm) in subs if bk in mbcnts[k]]
1603
+ msg += " [{}]".format(', '.join(parts))
1604
+ for (k, m) in self.QBSTATS:
1605
+ if bcnts[k][0]: msg += ", {} {}".format(m, self.file_number_size(bcnts[k], wn, ws))
1606
+ self.OUTPUT.write(msg + "\n")
1607
+ for line in lines:
1608
+ self.OUTPUT.write(line + "\n")
1609
+
1610
+ @staticmethod
1611
+ def add_file_count(cnts, key, fsize):
1612
+ """Add one file of fsize bytes to the [count, size] of key in dict cnts."""
1613
+ if key not in cnts: cnts[key] = [0, 0]
1614
+ cnts[key][0] += 1
1615
+ cnts[key][1] += fsize
1616
+
1617
+ def file_number_size(self, cnts, wn, ws):
1618
+ """Format [count, size] as 'count', 'size' or 'count(size)' per -WN/-WS, size readable."""
1619
+ size = self.format_float_value(cnts[1])
1620
+ if wn and ws: return "{}({})".format(cnts[0], size)
1621
+ return size if ws else str(cnts[0])
1622
+
1623
+ def object_file_exists(self, dsid, fname, bucket):
1624
+ """
1625
+ Check whether an object key, or a directory key such as a zarr store, exists.
1626
+
1627
+ Lists the whole dataset prefix once per dataset and bucket, so checking a
1628
+ dataset of many object files costs one object store call instead of one per
1629
+ file. A listing error is reported once and the files are then taken as present.
1630
+ """
1631
+ ckey = "{}:{}".format(bucket, dsid)
1632
+ if ckey not in self.OBJKEYS:
1633
+ keys = set()
1634
+ flist = self.object_glob(dsid + '/', bucket, 0, self.PGOPT['wrnlog'])
1635
+ if flist == self.FAILURE:
1636
+ keys = None
1637
+ elif flist:
1638
+ for key in flist:
1639
+ # a key under a directory key also stands for each of its parent directories
1640
+ while key and key not in keys:
1641
+ keys.add(key)
1642
+ key = op.dirname(key)
1643
+ self.OBJKEYS[ckey] = keys
1644
+ keys = self.OBJKEYS[ckey]
1645
+ if keys is None: return True
1646
+ return self.join_paths(dsid, fname) in keys
1647
+
1648
+ def cold_storage_file(self, dsid, stype, sfile):
1649
+ """
1650
+ Find a saved file moved into GLADE cold storage.
1651
+
1652
+ A dataset is moved either to <DECSHOME>/COLD_STORAGE/<dsid> or to
1653
+ <DECSHOME>/cold_storage_<YYYYMMDD>/COLD_STORAGE/<dsid>; both are searched,
1654
+ the root one first and then the dated ones newest first. Checking a migrated
1655
+ file only reads its inode, it does not recall it from tape.
1656
+
1657
+ Returns:
1658
+ Path of the file in cold storage, or None if not found.
1659
+ """
1660
+ if dsid not in self.COLDDIRS:
1661
+ dhome = self.PGLOG['DECSHOME']
1662
+ cdirs = []
1663
+ for cname in ('COLD_STORAGE', 'COLD_STORAGE_SINGLE_COPY'):
1664
+ cdirs.append("{}/{}/{}".format(dhome, cname, dsid))
1665
+ for cdir in sorted(glob.glob(dhome + "/cold_storage_*"), reverse = True):
1666
+ for cname in ('COLD_STORAGE', 'COLD_STORAGE_SINGLE_COPY'):
1667
+ cdirs.append("{}/{}/{}".format(cdir, cname, dsid))
1668
+ self.COLDDIRS[dsid] = [cdir for cdir in cdirs if op.isdir(cdir)]
1669
+ for cdir in self.COLDDIRS[dsid]:
1670
+ cfile = "{}/{}/{}".format(cdir, stype, sfile)
1671
+ if op.exists(cfile): return cfile
1672
+ return None
1441
1673
 
1442
1674
  def get_bid_numbers(self, bfiles):
1443
1675
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsarch
3
- Version: 3.0.12
3
+ Version: 3.0.14
4
4
  Summary: RDA Python package for utility program dsarch archive RDA data
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-dsarch