Skip to content

Commit 7664f28

Browse files
committed
Refactor filename matching logic to separate classes
Signed-off-by: Olivier Mehani <shtrom@ssji.net>
1 parent 32e966c commit 7664f28

File tree

1 file changed

+114
-66
lines changed

1 file changed

+114
-66
lines changed

rotate_backups/__init__.py

Lines changed: 114 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,98 @@ def rotate_backups(directory, rotation_scheme, **options):
260260
program.rotate_backups(directory)
261261

262262

263+
class Match:
264+
def match_to_datetime(self) -> datetime.datetime:
265+
pass
266+
267+
268+
class Matcher:
269+
def search(self, location: str, entry: str) -> Match:
270+
pass
271+
272+
273+
class FilenameMatch(Match):
274+
match: re.Match = None
275+
276+
def __init__(self, match: re.Match):
277+
self.match = match
278+
279+
def match_to_datetime(self) -> datetime.datetime:
280+
"""
281+
Convert the regular expression match to a :class:`~datetime.datetime` value.
282+
283+
:returns: A :class:`~datetime.datetime` value.
284+
:raises: :exc:`exceptions.ValueError` when a required date component is
285+
not captured by the pattern, the captured value is an empty
286+
string or the captured value cannot be interpreted as a
287+
base-10 integer.
288+
289+
.. seealso:: :data:`SUPPORTED_DATE_COMPONENTS`
290+
"""
291+
kw = {}
292+
captures = self.match.groupdict()
293+
for component, required in SUPPORTED_DATE_COMPONENTS:
294+
value = captures.get(component)
295+
if value:
296+
kw[component] = int(value, 10)
297+
elif required:
298+
raise ValueError("Missing required date component! (%s)" % component)
299+
else:
300+
kw[component] = 0
301+
return datetime.datetime(**kw)
302+
303+
304+
class FilenameMatcher(Matcher):
305+
timestamp_pattern: re.Pattern = None
306+
307+
def __init__(self, timestamp_pattern: re.Pattern):
308+
self.timestamp_pattern = timestamp_pattern
309+
310+
def search(self, location: str, entry: str) -> FilenameMatch:
311+
if match := self.timestamp_pattern.search(entry):
312+
return FilenameMatch(match)
313+
314+
@mutable_property
315+
def timestamp_pattern(self):
316+
"""
317+
The pattern used to extract timestamps from filenames (defaults to :data:`TIMESTAMP_PATTERN`).
318+
319+
The value of this property is a compiled regular expression object.
320+
Callers can provide their own compiled regular expression which
321+
makes it possible to customize the compilation flags (see the
322+
:func:`re.compile()` documentation for details).
323+
324+
The regular expression pattern is expected to be a Python compatible
325+
regular expression that defines the named capture groups 'year',
326+
'month' and 'day' and optionally 'hour', 'minute' and 'second'.
327+
328+
String values are automatically coerced to compiled regular expressions
329+
by calling :func:`~humanfriendly.coerce_pattern()`, in this case only
330+
the :data:`re.VERBOSE` flag is used.
331+
332+
If the caller provides a custom pattern it will be validated
333+
to confirm that the pattern contains named capture groups
334+
corresponding to each of the required date components
335+
defined by :data:`SUPPORTED_DATE_COMPONENTS`.
336+
"""
337+
return TIMESTAMP_PATTERN
338+
339+
@timestamp_pattern.setter
340+
def timestamp_pattern(self, value):
341+
"""Coerce the value of :attr:`timestamp_pattern` to a compiled regular expression."""
342+
pattern = coerce_pattern(value, re.VERBOSE)
343+
for component, required in SUPPORTED_DATE_COMPONENTS:
344+
if component not in pattern.groupindex and required:
345+
raise ValueError("Pattern is missing required capture group! (%s)" % component)
346+
set_property(self, 'timestamp_pattern', pattern)
347+
348+
263349
class RotateBackups(PropertyManager):
264350

265351
"""Python API for the ``rotate-backups`` program."""
266352

353+
_matcher: Matcher = FilenameMatcher(TIMESTAMP_PATTERN)
354+
267355
def __init__(self, rotation_scheme, **options):
268356
"""
269357
Initialize a :class:`RotateBackups` object.
@@ -449,37 +537,21 @@ def strict(self):
449537

450538
@mutable_property
451539
def timestamp_pattern(self):
452-
"""
453-
The pattern used to extract timestamps from filenames (defaults to :data:`TIMESTAMP_PATTERN`).
454-
455-
The value of this property is a compiled regular expression object.
456-
Callers can provide their own compiled regular expression which
457-
makes it possible to customize the compilation flags (see the
458-
:func:`re.compile()` documentation for details).
459-
460-
The regular expression pattern is expected to be a Python compatible
461-
regular expression that defines the named capture groups 'year',
462-
'month' and 'day' and optionally 'hour', 'minute' and 'second'.
463-
464-
String values are automatically coerced to compiled regular expressions
465-
by calling :func:`~humanfriendly.coerce_pattern()`, in this case only
466-
the :data:`re.VERBOSE` flag is used.
467-
468-
If the caller provides a custom pattern it will be validated
469-
to confirm that the pattern contains named capture groups
470-
corresponding to each of the required date components
471-
defined by :data:`SUPPORTED_DATE_COMPONENTS`.
472-
"""
473-
return TIMESTAMP_PATTERN
540+
return self.matcher.timestamp_pattern
474541

475542
@timestamp_pattern.setter
476543
def timestamp_pattern(self, value):
477-
"""Coerce the value of :attr:`timestamp_pattern` to a compiled regular expression."""
478-
pattern = coerce_pattern(value, re.VERBOSE)
479-
for component, required in SUPPORTED_DATE_COMPONENTS:
480-
if component not in pattern.groupindex and required:
481-
raise ValueError("Pattern is missing required capture group! (%s)" % component)
482-
set_property(self, 'timestamp_pattern', pattern)
544+
self.matcher.timestamp_pattern = value
545+
546+
@mutable_property
547+
def matcher(self):
548+
return self._matcher
549+
550+
@matcher.setter
551+
def matcher(self, matcher):
552+
if not isinstance(matcher, Matcher):
553+
raise ValueError(f'{matcher} is not a Matcher')
554+
set_property(self, '_matcher', matcher)
483555

484556
def rotate_concurrent(self, *locations, **kw):
485557
"""
@@ -642,8 +714,9 @@ def collect_backups(self, location):
642714
location = coerce_location(location)
643715
logger.info("Scanning %s for backups ..", location)
644716
location.ensure_readable(self.force)
717+
645718
for entry in natsort(location.context.list_entries(location.directory)):
646-
match = self.timestamp_pattern.search(entry)
719+
match = self.matcher.search(location, entry)
647720
if match:
648721
if self.exclude_list and any(fnmatch.fnmatch(entry, p) for p in self.exclude_list):
649722
logger.verbose("Excluded %s (it matched the exclude list).", entry)
@@ -653,7 +726,7 @@ def collect_backups(self, location):
653726
try:
654727
backups.append(Backup(
655728
pathname=os.path.join(location.directory, entry),
656-
timestamp=self.match_to_datetime(match),
729+
timestamp=match.match_to_datetime(),
657730
))
658731
except ValueError as e:
659732
logger.notice("Ignoring %s due to invalid date (%s).", entry, e)
@@ -663,31 +736,6 @@ def collect_backups(self, location):
663736
logger.info("Found %i timestamped backups in %s.", len(backups), location)
664737
return sorted(backups)
665738

666-
def match_to_datetime(self, match):
667-
"""
668-
Convert a regular expression match to a :class:`~datetime.datetime` value.
669-
670-
:param match: A regular expression match object.
671-
:returns: A :class:`~datetime.datetime` value.
672-
:raises: :exc:`exceptions.ValueError` when a required date component is
673-
not captured by the pattern, the captured value is an empty
674-
string or the captured value cannot be interpreted as a
675-
base-10 integer.
676-
677-
.. seealso:: :data:`SUPPORTED_DATE_COMPONENTS`
678-
"""
679-
kw = {}
680-
captures = match.groupdict()
681-
for component, required in SUPPORTED_DATE_COMPONENTS:
682-
value = captures.get(component)
683-
if value:
684-
kw[component] = int(value, 10)
685-
elif required:
686-
raise ValueError("Missing required date component! (%s)" % component)
687-
else:
688-
kw[component] = 0
689-
return datetime.datetime(**kw)
690-
691739
def group_backups(self, backups):
692740
"""
693741
Group backups collected by :func:`collect_backups()` by rotation frequencies.
@@ -781,25 +829,25 @@ class Location(PropertyManager):
781829

782830
""":class:`Location` objects represent a root directory containing backups."""
783831

784-
@required_property
832+
@ required_property
785833
def context(self):
786834
"""An execution context created using :mod:`executor.contexts`."""
787835

788-
@required_property
836+
@ required_property
789837
def directory(self):
790838
"""The pathname of a directory containing backups (a string)."""
791839

792-
@lazy_property
840+
@ lazy_property
793841
def have_ionice(self):
794842
""":data:`True` when ionice_ is available, :data:`False` otherwise."""
795843
return self.context.have_ionice
796844

797-
@lazy_property
845+
@ lazy_property
798846
def have_wildcards(self):
799847
""":data:`True` if :attr:`directory` is a filename pattern, :data:`False` otherwise."""
800848
return '*' in self.directory
801849

802-
@lazy_property
850+
@ lazy_property
803851
def mount_point(self):
804852
"""
805853
The pathname of the mount point of :attr:`directory` (a string or :data:`None`).
@@ -814,17 +862,17 @@ def mount_point(self):
814862
except ExternalCommandFailed:
815863
return None
816864

817-
@lazy_property
865+
@ lazy_property
818866
def is_remote(self):
819867
""":data:`True` if the location is remote, :data:`False` otherwise."""
820868
return isinstance(self.context, RemoteContext)
821869

822-
@lazy_property
870+
@ lazy_property
823871
def ssh_alias(self):
824872
"""The SSH alias of a remote location (a string or :data:`None`)."""
825873
return self.context.ssh_alias if self.is_remote else None
826874

827-
@property
875+
@ property
828876
def key_properties(self):
829877
"""
830878
A list of strings with the names of the :attr:`~custom_property.key` properties.
@@ -975,15 +1023,15 @@ class Backup(PropertyManager):
9751023
:attr:`~property_manager.PropertyManager.key_properties`.
9761024
"""
9771025

978-
@key_property
1026+
@ key_property
9791027
def pathname(self):
9801028
"""The pathname of the backup (a string)."""
9811029

982-
@key_property
1030+
@ key_property
9831031
def timestamp(self):
9841032
"""The date and time when the backup was created (a :class:`~datetime.datetime` object)."""
9851033

986-
@property
1034+
@ property
9871035
def week(self):
9881036
"""The ISO week number of :attr:`timestamp` (a number)."""
9891037
return self.timestamp.isocalendar()[1]

0 commit comments

Comments
 (0)