@@ -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
922929def 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 '''
0 commit comments