Skip to content

Commit 6329927

Browse files
Alter Time Delta Codes to Match Usage in ISO 8601
- Update TimeDelta code from y to Y (year), m to M (month) etc. to be more intuitive - Patch bug with bad data showing up in diff output - Cleanup of print statements
1 parent 5bb37bb commit 6329927

File tree

4 files changed

+97
-72
lines changed

4 files changed

+97
-72
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "zfslib"
3-
version = "0.9.1"
3+
version = "0.9.3"
44
description = "ZFS Utilities For Python3"
55
license = "MIT"
66
authors = ["Timothy C. Quinn"]

src/zfslib/zfslib.py

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,9 @@ def get_dataset(self, name):
361361
if isinstance(ds, Dataset): return ds
362362

363363
if isinstance(self, Pool):
364-
raise KeyError("Dataset '{}' not found in Pool '{}'.".format(name, self.name))
364+
raise KeyError(f"Dataset '{name}' not found in Pool '{self.name}'.")
365365
else:
366-
raise KeyError("Dataset '{}' not found in Dataset '{}'.".format(name, self.path))
366+
raise KeyError(f"Dataset '{name}' not found in Dataset '{self.path}'.")
367367

368368

369369
# returns list(of str) or if with_depth == True then list(of tuple(of depth, Dataset))
@@ -380,8 +380,8 @@ def get_all_datasets(self, with_depth=False, depth=0):
380380
# WARNING - Using this function to filter if snapshot contains a folder
381381
def get_snapshots(self, flt=True, index=False):
382382
if flt is True: flt = lambda _:True
383-
assert inspect.isfunction(flt), "flt must either be True or a Function. Got: {}".format(type(flt))
384-
assert isinstance(index, bool), "index must be a boolean. Got: {}".format(type(index))
383+
assert inspect.isfunction(flt), f"flt must either be True or a Function. Got: {type(flt)}"
384+
assert isinstance(index, bool), f"index must be a boolean. Got: {type(index)}"
385385
_ds_path = self.path
386386
res = []
387387
for idx, c in enumerate(self.children):
@@ -423,7 +423,7 @@ def __assert(k, types, default=None, to_datetime=False):
423423
else:
424424
if not k in find_opts: return default
425425
v = find_opts[k]
426-
assert isinstance(v, types), 'Invalid type for param {}. Expecting {} but got: {}'.format(k, types, type(v))
426+
assert isinstance(v, types), f'Invalid type for param {k}. Expecting {types} but got: {type(v)}'
427427

428428
if to_datetime and not isinstance(v, datetime):
429429
return datetime(v.year, v.month, v.day)
@@ -476,7 +476,7 @@ def __fil_dt(snap):
476476
if not tdelta is None:
477477
raise AssertionError("tdelta cannot be specified when both dt_from and dt_to are specified")
478478
if dt_from >= dt_to:
479-
raise AssertionError("dt_from ({}) must be < dt_to ({})".format(dt_from, dt_to))
479+
raise AssertionError(f"dt_from ({dt_from}) must be < dt_to ({dt_to})")
480480
(dt_f, dt_t) = (dt_from, dt_to)
481481
f=__fil_dt
482482

@@ -558,7 +558,7 @@ def __tv(k, v):
558558
if v is None: return None
559559
if isinstance(v, str): return [v]
560560
if isinstance(v, list): return v
561-
raise AssertionError("{} can only be a str or list. Got: {}".format(k, type(v)))
561+
raise AssertionError(f"{k} can only be a str or list. Got: {type(v)}")
562562

563563

564564
file_type = __tv('file_type', file_type)
@@ -591,8 +591,14 @@ def __row(s):
591591

592592
rows = list(map(lambda s: __row(s), stdout.splitlines()))
593593
diffs = []
594-
for row in rows:
594+
for i, row in enumerate(rows):
595+
# if i == 429:
596+
# print("HERE")
595597
d = Diff(row, snap_left, snap_right)
598+
if d.path_full.find('(on_delete_queue)') > 0:
599+
# It looks to be an artefact of ZFS that does not actually exist in FS
600+
# https://github.com/openzfs/zfs/blob/master/lib/libzfs/libzfs_diff.c
601+
continue
596602
if not file_type is None and not d.file_type in file_type: continue
597603
if not chg_type is None and not d.chg_type in chg_type: continue
598604

@@ -614,6 +620,7 @@ def __row(s):
614620
bIgn = True
615621
break
616622
if bIgn: continue
623+
617624
diffs.append(d)
618625

619626
return diffs
@@ -641,12 +648,12 @@ def _get_mounted(self):
641648
# path must be an actual path on the system being analyzed
642649
def get_rel_path(self, path):
643650
self.assertHaveMounts()
644-
assert isinstance(path, str), "argument passed is not a string. Got: {}".format(type(path))
651+
assert isinstance(path, str), f"argument passed is not a string. Got: {type(path)}"
645652
p_real = os.path.abspath( expand_user(path) )
646653
p_real = os.path.realpath(p_real)
647654
mp = self.mountpoint
648655
if not p_real.find(mp) == 0:
649-
raise KeyError('path given is not in current dataset mountpoint {}. Path: {}'.format(mp, path))
656+
raise KeyError(f'path given is not in current dataset mountpoint {mp}. Path: {path}')
650657
return p_real.replace(mp, '')
651658

652659

@@ -690,7 +697,7 @@ def _get_snap_path(self):
690697
assert isinstance(self.parent, Dataset), \
691698
"This function is only available for Snapshots of Datasets not Pools"
692699
self.parent.assertHaveMounts()
693-
return "{}/.zfs/snapshot/{}".format(self.parent.mountpoint, self.name)
700+
return f"{self.parent.mountpoint}/.zfs/snapshot/{self.name}"
694701
snap_path = property(_get_snap_path)
695702

696703

@@ -706,7 +713,7 @@ def resolve_snap_path(self, path):
706713
"This function is only available for Snapshots of Datasets not Pools"
707714
self.parent.assertHaveMounts()
708715
assert self.parent.mounted, \
709-
"Parent Dataset {} is not mounted. Please verify datsset.mounted before calling this function".format(self.parent)
716+
f"Parent Dataset {self.parent} is not mounted. Please verify datsset.mounted before calling this function"
710717

711718
if path is None or not isinstance(path, str) or path.strip() == '':
712719
assert 0, "path must be a non-blank string"
@@ -715,7 +722,7 @@ def resolve_snap_path(self, path):
715722
snap_path_base = self.snap_path
716723
ds_mp = self.dataset.mountpoint
717724
if path_neweal.find(ds_mp) == -1:
718-
raise KeyError("Path given is not within the dataset's mountpoint of {}. Path passed: {}".format(ds_mp, path))
725+
raise KeyError(f"Path given is not within the dataset's mountpoint of {ds_mp}. Path passed: {path}")
719726
snap_path = "{}{}".format(snap_path_base, path_neweal.replace(ds_mp, ''))
720727
if os.path.exists(snap_path):
721728
return (True, snap_path)
@@ -762,17 +769,17 @@ def __init__(self, row, snap_left, snap_right):
762769
self.no_from_snap=True
763770
snap_left = None
764771
elif not isinstance(snap_left, Snapshot):
765-
raise AssertionError("snap_left must be either a Snapshot or str('na-first'). Got: {}".format(type(snap_left)))
772+
raise AssertionError(f"snap_left must be either a Snapshot or str('na-first'). Got: {type(snap_left)}")
766773

767774
if isinstance(snap_right, str) and snap_right == '(present)':
768775
self.to_present=True
769776
snap_right = None
770777

771778
elif not isinstance(snap_right, Snapshot):
772-
raise AssertionError("snap_left must be either a Snapshot. Got: {}".format(type(snap_right)))
779+
raise AssertionError(f"snap_left must be either a Snapshot. Got: {type(snap_right)}")
773780

774-
if not self.no_from_snap and not self.to_present and snap_left.creation >= snap_right.creation:
775-
raise AssertionError("diff from creation ({}) is > or = to diff_to creation ({})".format(snap_left.creation, snap_right.creation))
781+
if not self.no_from_snap and not self.to_present and snap_left.creation > snap_right.creation:
782+
raise AssertionError(f"diff from creation ({snap_left.creation}) is > to diff_to creation ({snap_right.creation})")
776783

777784
self.snap_left = snap_left
778785
self.snap_right = snap_right
@@ -783,7 +790,7 @@ def __init__(self, row, snap_left, snap_right):
783790
elif len(row) == 5:
784791
(inode_ts, chg_type, file_type, path, path_new) = row
785792
else:
786-
raise Exception("Unexpected len: {}. Row = {}".format(len(row), row))
793+
raise Exception(f"Unexpected len: {len(row)}. Row = {row}")
787794

788795
chg_time = datetime.fromtimestamp(int(inode_ts[:inode_ts.find('.')]))
789796
self.chg_ts = inode_ts
@@ -834,13 +841,13 @@ def _get_snap_path_right(self):
834841
@staticmethod
835842
def get_file_type(s):
836843
assert (isinstance(s, str) and not s == ''), "argument must be a non-empty string"
837-
assert s in Diff.FILE_TYPES, "ZFS Diff File type is invalid: '{}'".format(s)
844+
assert s in Diff.FILE_TYPES, f"ZFS Diff File type is invalid: '{s}'"
838845
return Diff.FILE_TYPES[s]
839846

840847
@staticmethod
841848
def get_change_type(s):
842849
assert (isinstance(s, str) and not s == ''), "argument must be a non-empty string"
843-
assert s in Diff.CHANGE_TYPES, "ZFS Diff Change type is invalid: '{}'".format(s)
850+
assert s in Diff.CHANGE_TYPES, f"ZFS Diff Change type is invalid: '{s}'"
844851
return Diff.CHANGE_TYPES[s]
845852

846853
def __str__(self):
@@ -859,57 +866,57 @@ def __str__(self):
859866
# buildTimedelta()
860867
# Builds timedelta from string:
861868
# . tdelta is a timedelta -or- str(nC) where: n is an integer > 0 and C is one of:
862-
# . y=year, m=month, w=week, d=day, H=hour, M=minute, s=second
869+
# . Y=year, M=month, W=week, D=day, h=hour, m=minute, s=second
863870
# Note: month and year are imprecise and assume 30.4 and 365 days
864-
def buildTimedelta(tdelta):
871+
def buildTimedelta(tdelta) -> timedelta:
865872
if isinstance(tdelta, timedelta): return tdelta
866873

867874
if not isinstance(tdelta, str):
868-
raise AssertionError('tdelta must be a string')
875+
raise KeyError('tdelta must be a string')
869876
elif len(tdelta) < 2:
870-
raise AssertionError('len(tdelta) must be >= 2')
877+
raise KeyError('len(tdelta) must be >= 2')
871878
n = tdelta[:-1]
872879
try:
873880
n = int(n)
874-
if n < 1: raise AssertionError('tdelta must be > 0')
881+
if n < 1: raise KeyError('tdelta must be > 0')
875882
except ValueError as ex:
876-
raise AssertionError('Value passed for tdelta does not contain a number: {}'.format(tdelta))
883+
raise KeyError(f'Value passed for tdelta does not contain a number: {tdelta}')
877884

878885
c = tdelta[-1:]
879-
if c == 'H':
886+
if c == 'h':
880887
return timedelta(hours=n)
881-
elif c == 'M':
888+
elif c == 'm':
882889
return timedelta(minutes=n)
883-
elif c == 'S':
890+
elif c == 's':
884891
return timedelta(seconds=n)
885-
elif c == 'd':
892+
elif c == 'D':
886893
return timedelta(days=n)
887-
elif c == 'm':
888-
return timedelta(days=n*(365/12))
889-
elif c == 'w':
894+
elif c == 'W':
890895
return timedelta(weeks=n)
891-
elif c == 'y':
896+
elif c == 'M':
897+
return timedelta(days=n*(365/12))
898+
elif c == 'Y':
892899
return timedelta(days=n*365)
893900
else:
894-
raise AssertionError('Unexpected datetime identifier, expecting one of y,m,w,d,H,M,S.')
901+
raise KeyError('Unexpected datetime identifier, expecting one of Y,M,W,D,h,m,s')
895902

896903

897904
# calcDateRange()
898905
# Calculates a date range based on tdelta string passed
899906
# tdelta is a timedelta -or- str(nC) where: n is an integer > 0 and C is one of:
900-
# . y=year, m=month, w=week, d=day, H=hour, M=minute, s=second
907+
# . Y=year, M=month, W=week, D=day, h=hour, m=minute, s=second
901908
# If dt_from is defined, return tuple: (dt_from, dt_from+tdelta)
902909
# If dt_to is defined, return tuple: (dt_from-tdelta, dt_to)
903-
def calcDateRange(tdelta, dt_from=None, dt_to=None):
904-
if tdelta is None: raise AssertionError('tdelta is required')
910+
def calcDateRange(tdelta, dt_from:datetime=None, dt_to:datetime=None) -> tuple:
911+
if tdelta is None: raise KeyError('tdelta is required')
905912
if dt_from and dt_to:
906-
raise AssertionError('Only one of dt_from or dt_to must be defined')
913+
raise KeyError('Only one of dt_from or dt_to must be defined')
907914
elif (not dt_from and not dt_to):
908-
raise AssertionError('Please specify one of dt_from or dt_to')
915+
raise KeyError('Please specify one of dt_from or dt_to')
909916
elif dt_from and not isinstance(dt_from, datetime):
910-
raise AssertionError('dt_from must be a datetime')
917+
raise KeyError('dt_from must be a datetime')
911918
elif dt_to and not isinstance(dt_to, datetime):
912-
raise AssertionError('dt_to must be a datetime')
919+
raise KeyError('dt_to must be a datetime')
913920

914921
td = buildTimedelta(tdelta)
915922

@@ -920,7 +927,7 @@ def calcDateRange(tdelta, dt_from=None, dt_to=None):
920927

921928

922929
def splitPath(s):
923-
assert isinstance(s, str), "String not passed. Got: {}".format(type(s))
930+
assert isinstance(s, str), f"String not passed. Got: {type(s)}"
924931
s = s.strip()
925932
assert not s == '', "Empty string passed"
926933
f = os.path.basename(s)
@@ -976,7 +983,25 @@ def expand_user(path):
976983
return pathlib.Path(path).expanduser()
977984

978985

986+
# Ignore snapshots with exact same timestamp
987+
# . Edge cases that can happen and muck up stuff
988+
# . Handles list(of Snapshot) and list(of tuple(of idx, Snapshot))
989+
def removeDuplicateSnapshotsByDate(snapshots):
990+
_ret = []
991+
for i, snap_rec in enumerate(snapshots):
992+
if isinstance(snap_rec, Snapshot):
993+
snap = snap_rec
994+
elif isinstance(snap_rec, tuple):
995+
(idx, snap) = snap_rec
996+
else:
997+
raise Exception(f"Invalid snapshot list passed. Got {type(snap_rec)} at record {i}")
998+
999+
if i > 0 and snap.creation == snap_last.creation: continue
1000+
1001+
_ret.append(snap_rec)
9791002

1003+
snap_last = snap
1004+
return _ret
9801005

9811006

9821007
''' END Utilities '''

tests/test_zfslib.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ def test_snapable_find_snapshots(self):
337337
snaps = ds.find_snapshots({'name': '*zsys_*e*', 'tdelta': timedelta(hours=36)
338338
,'dt_to': dt_from_creation('1609362247')})
339339
self.assertEqual(len(snaps), 5)
340-
snaps2 = ds.find_snapshots({'name': '*zsys_*e*', 'tdelta': '36H'
340+
snaps2 = ds.find_snapshots({'name': '*zsys_*e*', 'tdelta': '36h'
341341
,'dt_to': dt_from_creation('1609362247')})
342342
self.assertEqual(len(snaps2), len(snaps))
343343
for i, snap in enumerate(snaps):
@@ -348,7 +348,7 @@ def test_snapable_find_snapshots(self):
348348
,'dt_from': dt_from_creation('1608233673'), 'tdelta': timedelta(hours=48)})
349349
self.assertEqual(len(snaps), 5)
350350
snaps2 = ds.find_snapshots({'name': '*zsys_*w*'
351-
,'dt_from': dt_from_creation('1608233673'), 'tdelta': '48H'})
351+
,'dt_from': dt_from_creation('1608233673'), 'tdelta': '48h'})
352352
self.assertEqual(len(snaps2), len(snaps))
353353
for i, snap in enumerate(snaps):
354354
self.assertIs(snap, snaps2[i])
@@ -384,15 +384,15 @@ def test_snapable_find_snapshots(self):
384384
with self.assertRaises(AssertionError): snaps = ds.find_snapshots({'dt_from': 'asdf'})
385385
with self.assertRaises(AssertionError): snaps = ds.find_snapshots({'dt_to': 'asdf'})
386386
with self.assertRaises(AssertionError): snaps = ds.find_snapshots({'tdelta': 10})
387-
with self.assertRaises(AssertionError): snaps = ds.find_snapshots({'tdelta': '-10H'})
387+
with self.assertRaises(KeyError): snaps = ds.find_snapshots({'tdelta': '-10h'})
388388
with self.assertRaises(AssertionError): snaps = ds.find_snapshots({'index': 1})
389389
with self.assertRaises(AssertionError):
390390
snaps = ds.find_snapshots({'dt_to': dt_date(2020, 12, 20)
391391
, 'dt_from': dt_date(2020, 12, 21)})
392392
with self.assertRaises(AssertionError):
393393
snaps = ds.find_snapshots({'dt_to': dt_date(2020, 12, 21)
394394
,'dt_from': dt_date(2020, 12, 20)
395-
,'tdelta': "1H"})
395+
,'tdelta': "1h"})
396396

397397

398398
# tested against data_nomounts.tsv
@@ -422,45 +422,45 @@ def test_no_mounts(self):
422422
# Test General Utilities
423423
def test_buildTimedelta(self):
424424
self.assertEqual(zfs.buildTimedelta(timedelta(seconds=10)), timedelta(seconds=10))
425-
self.assertEqual(zfs.buildTimedelta('1y'), timedelta(days=365))
426-
self.assertEqual(zfs.buildTimedelta('1m'), timedelta(days=(365/12)))
427-
self.assertEqual(zfs.buildTimedelta('1w'), timedelta(weeks=1))
428-
self.assertEqual(zfs.buildTimedelta('10d'), timedelta(days=10))
429-
self.assertEqual(zfs.buildTimedelta('10H'), timedelta(hours=10))
430-
self.assertEqual(zfs.buildTimedelta('10M'), timedelta(minutes=10))
431-
self.assertEqual(zfs.buildTimedelta('10S'), timedelta(seconds=10))
425+
self.assertEqual(zfs.buildTimedelta('1Y'), timedelta(days=365))
426+
self.assertEqual(zfs.buildTimedelta('1M'), timedelta(days=(365/12)))
427+
self.assertEqual(zfs.buildTimedelta('1W'), timedelta(weeks=1))
428+
self.assertEqual(zfs.buildTimedelta('10D'), timedelta(days=10))
429+
self.assertEqual(zfs.buildTimedelta('10h'), timedelta(hours=10))
430+
self.assertEqual(zfs.buildTimedelta('10m'), timedelta(minutes=10))
431+
self.assertEqual(zfs.buildTimedelta('10s'), timedelta(seconds=10))
432432

433433
# Negative tests
434434
with self.assertRaises(TypeError): zfs.buildTimedelta()
435-
with self.assertRaises(TypeError): zfs.buildTimedelta('1H', True)
436-
with self.assertRaises(AssertionError): zfs.buildTimedelta(None)
437-
with self.assertRaises(AssertionError): zfs.buildTimedelta(datetime.now())
438-
with self.assertRaises(AssertionError): zfs.buildTimedelta('1')
439-
with self.assertRaises(AssertionError): zfs.buildTimedelta('aH')
440-
with self.assertRaises(AssertionError): zfs.buildTimedelta('-1H')
441-
with self.assertRaises(AssertionError): zfs.buildTimedelta('1X')
435+
with self.assertRaises(TypeError): zfs.buildTimedelta('1h', True)
436+
with self.assertRaises(KeyError): zfs.buildTimedelta(None)
437+
with self.assertRaises(KeyError): zfs.buildTimedelta(datetime.now())
438+
with self.assertRaises(KeyError): zfs.buildTimedelta('1')
439+
with self.assertRaises(KeyError): zfs.buildTimedelta('ah')
440+
with self.assertRaises(KeyError): zfs.buildTimedelta('-1h')
441+
with self.assertRaises(KeyError): zfs.buildTimedelta('1X')
442442

443443

444444

445445
def test_calcDateRange(self):
446446
_now = datetime.now()
447447
_weekago = datetime.now() - timedelta(weeks=1)
448-
self.assertEqual(zfs.calcDateRange('1d', dt_to=_now), \
448+
self.assertEqual(zfs.calcDateRange('1D', dt_to=_now), \
449449
(_now - timedelta(days=1), _now) )
450450

451-
self.assertEqual(zfs.calcDateRange('2d', dt_from=_weekago), \
451+
self.assertEqual(zfs.calcDateRange('2D', dt_from=_weekago), \
452452
(_weekago, _weekago + timedelta(days=2)) )
453453

454-
self.assertEqual(zfs.calcDateRange('3m', dt_to=_now), \
454+
self.assertEqual(zfs.calcDateRange('3M', dt_to=_now), \
455455
(_now - timedelta(days=(3 * (365/12))), _now) )
456456

457457
# Negative tests
458458
with self.assertRaises(TypeError): zfs.calcDateRange()
459459
with self.assertRaises(TypeError): zfs.calcDateRange(1,2,3,4)
460-
with self.assertRaises(AssertionError): zfs.calcDateRange(None)
461-
with self.assertRaises(AssertionError): zfs.calcDateRange('1H', _now, _now)
462-
with self.assertRaises(AssertionError): zfs.calcDateRange('1H', dt_from=None)
463-
with self.assertRaises(AssertionError): zfs.calcDateRange('1H', dt_to=None)
460+
with self.assertRaises(KeyError): zfs.calcDateRange(None)
461+
with self.assertRaises(KeyError): zfs.calcDateRange('1h', _now, _now)
462+
with self.assertRaises(KeyError): zfs.calcDateRange('1h', dt_from=None)
463+
with self.assertRaises(KeyError): zfs.calcDateRange('1h', dt_to=None)
464464

465465

466466
def test_splitPath(self):

0 commit comments

Comments
 (0)