Browse Source

Reorganize data source and monitoring hooks to make developing new hooks easier.

Dan Helfman 6 months ago
parent
commit
6b2f2b2ac4
58 changed files with 508 additions and 349 deletions
  1. 1 0
      NEWS
  2. 1 1
      borgmatic/actions/check.py
  3. 3 4
      borgmatic/actions/create.py
  4. 17 10
      borgmatic/actions/restore.py
  5. 9 8
      borgmatic/commands/borgmatic.py
  6. 0 0
      borgmatic/hooks/data_source/__init__.py
  7. 0 0
      borgmatic/hooks/data_source/bootstrap.py
  8. 1 9
      borgmatic/hooks/data_source/dump.py
  9. 1 1
      borgmatic/hooks/data_source/mariadb.py
  10. 1 1
      borgmatic/hooks/data_source/mongodb.py
  11. 1 1
      borgmatic/hooks/data_source/mysql.py
  12. 1 1
      borgmatic/hooks/data_source/postgresql.py
  13. 1 1
      borgmatic/hooks/data_source/sqlite.py
  14. 0 0
      borgmatic/hooks/data_source/zfs.py
  15. 53 54
      borgmatic/hooks/dispatch.py
  16. 0 21
      borgmatic/hooks/monitor.py
  17. 0 0
      borgmatic/hooks/monitoring/__init__.py
  18. 12 10
      borgmatic/hooks/monitoring/apprise.py
  19. 1 1
      borgmatic/hooks/monitoring/cronhub.py
  20. 1 1
      borgmatic/hooks/monitoring/cronitor.py
  21. 9 7
      borgmatic/hooks/monitoring/healthchecks.py
  22. 1 0
      borgmatic/hooks/monitoring/logs.py
  23. 1 1
      borgmatic/hooks/monitoring/loki.py
  24. 10 0
      borgmatic/hooks/monitoring/monitor.py
  25. 0 0
      borgmatic/hooks/monitoring/ntfy.py
  26. 1 1
      borgmatic/hooks/monitoring/pagerduty.py
  27. 0 0
      borgmatic/hooks/monitoring/pushover.py
  28. 0 0
      borgmatic/hooks/monitoring/uptime_kuma.py
  29. 0 0
      borgmatic/hooks/monitoring/zabbix.py
  30. 4 2
      docs/reference/source-code.md
  31. 0 0
      tests/integration/hooks/__init__.py
  32. 0 0
      tests/integration/hooks/monitoring/__init__.py
  33. 3 3
      tests/integration/hooks/monitoring/test_apprise.py
  34. 3 3
      tests/integration/hooks/monitoring/test_healthchecks.py
  35. 1 1
      tests/integration/hooks/monitoring/test_loki.py
  36. 24 24
      tests/unit/actions/test_restore.py
  37. 0 0
      tests/unit/hooks/data_source/__init__.py
  38. 1 1
      tests/unit/hooks/data_source/test_bootstrap.py
  39. 1 1
      tests/unit/hooks/data_source/test_dump.py
  40. 1 1
      tests/unit/hooks/data_source/test_mariadb.py
  41. 1 1
      tests/unit/hooks/data_source/test_mongodb.py
  42. 1 1
      tests/unit/hooks/data_source/test_mysql.py
  43. 1 1
      tests/unit/hooks/data_source/test_postgresql.py
  44. 1 1
      tests/unit/hooks/data_source/test_sqlite.py
  45. 1 1
      tests/unit/hooks/data_source/test_zfs.py
  46. 0 0
      tests/unit/hooks/monitoring/__init__.py
  47. 60 50
      tests/unit/hooks/monitoring/test_apprise.py
  48. 1 1
      tests/unit/hooks/monitoring/test_cronhub.py
  49. 1 1
      tests/unit/hooks/monitoring/test_cronitor.py
  50. 54 30
      tests/unit/hooks/monitoring/test_healthchecks.py
  51. 1 1
      tests/unit/hooks/monitoring/test_logs.py
  52. 1 1
      tests/unit/hooks/monitoring/test_loki.py
  53. 26 26
      tests/unit/hooks/monitoring/test_ntfy.py
  54. 1 1
      tests/unit/hooks/monitoring/test_pagerduty.py
  55. 16 16
      tests/unit/hooks/monitoring/test_pushover.py
  56. 12 12
      tests/unit/hooks/monitoring/test_uptimekuma.py
  57. 21 21
      tests/unit/hooks/monitoring/test_zabbix.py
  58. 145 15
      tests/unit/hooks/test_dispatch.py

+ 1 - 0
NEWS

@@ -1,5 +1,6 @@
 1.9.4.dev0
 1.9.4.dev0
  * #926: Fix library error when running within a PyInstaller bundle.
  * #926: Fix library error when running within a PyInstaller bundle.
+ * Reorganize data source and monitoring hooks to make developing new hooks easier.
 
 
 1.9.3
 1.9.3
  * #261 (beta): Add a ZFS hook for snapshotting and backing up ZFS datasets. See the documentation
  * #261 (beta): Add a ZFS hook for snapshotting and backing up ZFS datasets. See the documentation

+ 1 - 1
borgmatic/actions/check.py

@@ -364,7 +364,7 @@ def collect_spot_check_source_paths(
             'use_streaming',
             'use_streaming',
             config,
             config,
             repository['path'],
             repository['path'],
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
         ).values()
         ).values()
     )
     )
 
 

+ 3 - 4
borgmatic/actions/create.py

@@ -10,7 +10,6 @@ import borgmatic.config.paths
 import borgmatic.config.validate
 import borgmatic.config.validate
 import borgmatic.hooks.command
 import borgmatic.hooks.command
 import borgmatic.hooks.dispatch
 import borgmatic.hooks.dispatch
-import borgmatic.hooks.dump
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -186,7 +185,7 @@ def run_create(
             'remove_data_source_dumps',
             'remove_data_source_dumps',
             config,
             config,
             repository['path'],
             repository['path'],
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
             borgmatic_runtime_directory,
             borgmatic_runtime_directory,
             global_arguments.dry_run,
             global_arguments.dry_run,
         )
         )
@@ -195,7 +194,7 @@ def run_create(
             'dump_data_sources',
             'dump_data_sources',
             config,
             config,
             repository['path'],
             repository['path'],
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
             config_paths,
             config_paths,
             borgmatic_runtime_directory,
             borgmatic_runtime_directory,
             source_directories,
             source_directories,
@@ -232,7 +231,7 @@ def run_create(
             'remove_data_source_dumps',
             'remove_data_source_dumps',
             config,
             config,
             config_filename,
             config_filename,
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
             borgmatic_runtime_directory,
             borgmatic_runtime_directory,
             global_arguments.dry_run,
             global_arguments.dry_run,
         )
         )

+ 17 - 10
borgmatic/actions/restore.py

@@ -11,8 +11,8 @@ import borgmatic.borg.mount
 import borgmatic.borg.repo_list
 import borgmatic.borg.repo_list
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.config.validate
 import borgmatic.config.validate
+import borgmatic.hooks.data_source.dump
 import borgmatic.hooks.dispatch
 import borgmatic.hooks.dispatch
-import borgmatic.hooks.dump
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -44,7 +44,8 @@ def get_configured_data_source(
         hooks_to_search = {
         hooks_to_search = {
             hook_name: value
             hook_name: value
             for (hook_name, value) in config.items()
             for (hook_name, value) in config.items()
-            if hook_name in borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES
+            if hook_name.split('_databases')[0]
+            in borgmatic.hooks.dispatch.get_submodule_names(borgmatic.hooks.data_source)
         }
         }
     else:
     else:
         try:
         try:
@@ -123,10 +124,10 @@ def restore_single_data_source(
         'make_data_source_dump_patterns',
         'make_data_source_dump_patterns',
         config,
         config,
         repository['path'],
         repository['path'],
-        borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+        borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
         borgmatic_runtime_directory,
         borgmatic_runtime_directory,
         data_source['name'],
         data_source['name'],
-    )[hook_name]
+    )[hook_name.split('_databases')[0]]
 
 
     destination_path = (
     destination_path = (
         tempfile.mkdtemp(dir=borgmatic_runtime_directory)
         tempfile.mkdtemp(dir=borgmatic_runtime_directory)
@@ -141,7 +142,11 @@ def restore_single_data_source(
             dry_run=global_arguments.dry_run,
             dry_run=global_arguments.dry_run,
             repository=repository['path'],
             repository=repository['path'],
             archive=archive_name,
             archive=archive_name,
-            paths=[borgmatic.hooks.dump.convert_glob_patterns_to_borg_pattern(dump_patterns)],
+            paths=[
+                borgmatic.hooks.data_source.dump.convert_glob_patterns_to_borg_pattern(
+                    dump_patterns
+                )
+            ],
             config=config,
             config=config,
             local_borg_version=local_borg_version,
             local_borg_version=local_borg_version,
             global_arguments=global_arguments,
             global_arguments=global_arguments,
@@ -162,11 +167,11 @@ def restore_single_data_source(
             shutil.rmtree(destination_path, ignore_errors=True)
             shutil.rmtree(destination_path, ignore_errors=True)
 
 
     # Run a single data source restore, consuming the extract stdout (if any).
     # Run a single data source restore, consuming the extract stdout (if any).
-    borgmatic.hooks.dispatch.call_hooks(
+    borgmatic.hooks.dispatch.call_hook(
         function_name='restore_data_source_dump',
         function_name='restore_data_source_dump',
         config=config,
         config=config,
         log_prefix=repository['path'],
         log_prefix=repository['path'],
-        hook_names=[hook_name],
+        hook_name=hook_name,
         data_source=data_source,
         data_source=data_source,
         dry_run=global_arguments.dry_run,
         dry_run=global_arguments.dry_run,
         extract_process=extract_process,
         extract_process=extract_process,
@@ -206,7 +211,9 @@ def collect_archive_data_source_names(
         global_arguments,
         global_arguments,
         list_paths=[
         list_paths=[
             'sh:'
             'sh:'
-            + borgmatic.hooks.dump.make_data_source_dump_path(base_directory, '*_databases/*/*')
+            + borgmatic.hooks.data_source.dump.make_data_source_dump_path(
+                base_directory, '*_databases/*/*'
+            )
             for base_directory in (
             for base_directory in (
                 'borgmatic',
                 'borgmatic',
                 borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
                 borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
@@ -354,7 +361,7 @@ def run_restore(
             'remove_data_source_dumps',
             'remove_data_source_dumps',
             config,
             config,
             repository['path'],
             repository['path'],
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
             borgmatic_runtime_directory,
             borgmatic_runtime_directory,
             global_arguments.dry_run,
             global_arguments.dry_run,
         )
         )
@@ -451,7 +458,7 @@ def run_restore(
             'remove_data_source_dumps',
             'remove_data_source_dumps',
             config,
             config,
             repository['path'],
             repository['path'],
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
             borgmatic_runtime_directory,
             borgmatic_runtime_directory,
             global_arguments.dry_run,
             global_arguments.dry_run,
         )
         )

+ 9 - 8
borgmatic/commands/borgmatic.py

@@ -39,7 +39,8 @@ from borgmatic.borg import umount as borg_umount
 from borgmatic.borg import version as borg_version
 from borgmatic.borg import version as borg_version
 from borgmatic.commands.arguments import parse_arguments
 from borgmatic.commands.arguments import parse_arguments
 from borgmatic.config import checks, collect, validate
 from borgmatic.config import checks, collect, validate
-from borgmatic.hooks import command, dispatch, monitor
+from borgmatic.hooks import command, dispatch
+from borgmatic.hooks.monitoring import monitor
 from borgmatic.logger import DISABLED, add_custom_log_levels, configure_logging, should_do_markup
 from borgmatic.logger import DISABLED, add_custom_log_levels, configure_logging, should_do_markup
 from borgmatic.signals import configure_signals
 from borgmatic.signals import configure_signals
 from borgmatic.verbosity import verbosity_to_log_level
 from borgmatic.verbosity import verbosity_to_log_level
@@ -103,7 +104,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                 'initialize_monitor',
                 'initialize_monitor',
                 config,
                 config,
                 config_filename,
                 config_filename,
-                monitor.MONITOR_HOOK_NAMES,
+                dispatch.Hook_type.MONITORING,
                 monitoring_log_level,
                 monitoring_log_level,
                 global_arguments.dry_run,
                 global_arguments.dry_run,
             )
             )
@@ -112,7 +113,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                 'ping_monitor',
                 'ping_monitor',
                 config,
                 config,
                 config_filename,
                 config_filename,
-                monitor.MONITOR_HOOK_NAMES,
+                dispatch.Hook_type.MONITORING,
                 monitor.State.START,
                 monitor.State.START,
                 monitoring_log_level,
                 monitoring_log_level,
                 global_arguments.dry_run,
                 global_arguments.dry_run,
@@ -188,7 +189,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                 'ping_monitor',
                 'ping_monitor',
                 config,
                 config,
                 config_filename,
                 config_filename,
-                monitor.MONITOR_HOOK_NAMES,
+                dispatch.Hook_type.MONITORING,
                 monitor.State.LOG,
                 monitor.State.LOG,
                 monitoring_log_level,
                 monitoring_log_level,
                 global_arguments.dry_run,
                 global_arguments.dry_run,
@@ -205,7 +206,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                     'ping_monitor',
                     'ping_monitor',
                     config,
                     config,
                     config_filename,
                     config_filename,
-                    monitor.MONITOR_HOOK_NAMES,
+                    dispatch.Hook_type.MONITORING,
                     monitor.State.FINISH,
                     monitor.State.FINISH,
                     monitoring_log_level,
                     monitoring_log_level,
                     global_arguments.dry_run,
                     global_arguments.dry_run,
@@ -214,7 +215,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                     'destroy_monitor',
                     'destroy_monitor',
                     config,
                     config,
                     config_filename,
                     config_filename,
-                    monitor.MONITOR_HOOK_NAMES,
+                    dispatch.Hook_type.MONITORING,
                     monitoring_log_level,
                     monitoring_log_level,
                     global_arguments.dry_run,
                     global_arguments.dry_run,
                 )
                 )
@@ -241,7 +242,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                 'ping_monitor',
                 'ping_monitor',
                 config,
                 config,
                 config_filename,
                 config_filename,
-                monitor.MONITOR_HOOK_NAMES,
+                dispatch.Hook_type.MONITORING,
                 monitor.State.FAIL,
                 monitor.State.FAIL,
                 monitoring_log_level,
                 monitoring_log_level,
                 global_arguments.dry_run,
                 global_arguments.dry_run,
@@ -250,7 +251,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                 'destroy_monitor',
                 'destroy_monitor',
                 config,
                 config,
                 config_filename,
                 config_filename,
-                monitor.MONITOR_HOOK_NAMES,
+                dispatch.Hook_type.MONITORING,
                 monitoring_log_level,
                 monitoring_log_level,
                 global_arguments.dry_run,
                 global_arguments.dry_run,
             )
             )

+ 0 - 0
borgmatic/hooks/data_source/__init__.py


+ 0 - 0
borgmatic/hooks/bootstrap.py → borgmatic/hooks/data_source/bootstrap.py


+ 1 - 9
borgmatic/hooks/dump.py → borgmatic/hooks/data_source/dump.py

@@ -5,15 +5,7 @@ import shutil
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
-DATA_SOURCE_HOOK_NAMES = (
-    'bootstrap',
-    'mariadb_databases',
-    'mysql_databases',
-    'mongodb_databases',
-    'postgresql_databases',
-    'sqlite_databases',
-    'zfs',
-)
+IS_A_HOOK = False
 
 
 
 
 def make_data_source_dump_path(borgmatic_runtime_directory, data_source_hook_name):
 def make_data_source_dump_path(borgmatic_runtime_directory, data_source_hook_name):

+ 1 - 1
borgmatic/hooks/mariadb.py → borgmatic/hooks/data_source/mariadb.py

@@ -9,7 +9,7 @@ from borgmatic.execute import (
     execute_command_and_capture_output,
     execute_command_and_capture_output,
     execute_command_with_processes,
     execute_command_with_processes,
 )
 )
-from borgmatic.hooks import dump
+from borgmatic.hooks.data_source import dump
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 1 - 1
borgmatic/hooks/mongodb.py → borgmatic/hooks/data_source/mongodb.py

@@ -4,7 +4,7 @@ import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 from borgmatic.execute import execute_command, execute_command_with_processes
 from borgmatic.execute import execute_command, execute_command_with_processes
-from borgmatic.hooks import dump
+from borgmatic.hooks.data_source import dump
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 1 - 1
borgmatic/hooks/mysql.py → borgmatic/hooks/data_source/mysql.py

@@ -9,7 +9,7 @@ from borgmatic.execute import (
     execute_command_and_capture_output,
     execute_command_and_capture_output,
     execute_command_with_processes,
     execute_command_with_processes,
 )
 )
-from borgmatic.hooks import dump
+from borgmatic.hooks.data_source import dump
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 1 - 1
borgmatic/hooks/postgresql.py → borgmatic/hooks/data_source/postgresql.py

@@ -11,7 +11,7 @@ from borgmatic.execute import (
     execute_command_and_capture_output,
     execute_command_and_capture_output,
     execute_command_with_processes,
     execute_command_with_processes,
 )
 )
-from borgmatic.hooks import dump
+from borgmatic.hooks.data_source import dump
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 1 - 1
borgmatic/hooks/sqlite.py → borgmatic/hooks/data_source/sqlite.py

@@ -4,7 +4,7 @@ import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 from borgmatic.execute import execute_command, execute_command_with_processes
 from borgmatic.execute import execute_command, execute_command_with_processes
-from borgmatic.hooks import dump
+from borgmatic.hooks.data_source import dump
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 0 - 0
borgmatic/hooks/zfs.py → borgmatic/hooks/data_source/zfs.py


+ 53 - 54
borgmatic/hooks/dispatch.py

@@ -1,75 +1,70 @@
+import enum
+import importlib
 import logging
 import logging
+import pkgutil
 
 
-from borgmatic.hooks import (
-    apprise,
-    bootstrap,
-    cronhub,
-    cronitor,
-    healthchecks,
-    loki,
-    mariadb,
-    mongodb,
-    mysql,
-    ntfy,
-    pagerduty,
-    postgresql,
-    pushover,
-    sqlite,
-    uptimekuma,
-    zabbix,
-    zfs,
-)
+import borgmatic.hooks.data_source
+import borgmatic.hooks.monitoring
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
-HOOK_NAME_TO_MODULE = {
-    'apprise': apprise,
-    'bootstrap': bootstrap,
-    'cronhub': cronhub,
-    'cronitor': cronitor,
-    'healthchecks': healthchecks,
-    'loki': loki,
-    'mariadb_databases': mariadb,
-    'mongodb_databases': mongodb,
-    'mysql_databases': mysql,
-    'ntfy': ntfy,
-    'pagerduty': pagerduty,
-    'postgresql_databases': postgresql,
-    'pushover': pushover,
-    'sqlite_databases': sqlite,
-    'uptime_kuma': uptimekuma,
-    'zabbix': zabbix,
-    'zfs': zfs,
-}
+
+class Hook_type(enum.Enum):
+    DATA_SOURCE = 'data_source'
+    MONITORING = 'monitoring'
+
+
+def get_submodule_names(parent_module):  # pragma: no cover
+    '''
+    Given a parent module, return the names of its direct submodules as a tuple of strings.
+    '''
+    return tuple(module_info.name for module_info in pkgutil.iter_modules(parent_module.__path__))
 
 
 
 
 def call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs):
 def call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs):
     '''
     '''
     Given a configuration dict and a prefix to use in log entries, call the requested function of
     Given a configuration dict and a prefix to use in log entries, call the requested function of
     the Python module corresponding to the given hook name. Supply that call with the configuration
     the Python module corresponding to the given hook name. Supply that call with the configuration
-    for this hook (if any), the log prefix, and any given args and kwargs. Return any return value.
+    for this hook (if any), the log prefix, and any given args and kwargs. Return the return value
+    of that call or None if the module in question is not a hook.
 
 
     Raise ValueError if the hook name is unknown.
     Raise ValueError if the hook name is unknown.
     Raise AttributeError if the function name is not found in the module.
     Raise AttributeError if the function name is not found in the module.
     Raise anything else that the called function raises.
     Raise anything else that the called function raises.
     '''
     '''
-    hook_config = config.get(hook_name) or {}
+    hook_config = config.get(hook_name) or config.get(f'{hook_name}_databases') or {}
+    module_name = hook_name.split('_databases')[0]
 
 
-    try:
-        module = HOOK_NAME_TO_MODULE[hook_name]
-    except KeyError:
+    # Probe for a data source or monitoring hook module corresponding to the hook name.
+    for parent_module in (borgmatic.hooks.data_source, borgmatic.hooks.monitoring):
+        if module_name not in get_submodule_names(parent_module):
+            continue
+
+        module = importlib.import_module(f'{parent_module.__name__}.{module_name}')
+
+        # If this module is explicitly flagged as not a hook, bail.
+        if not getattr(module, 'IS_A_HOOK', True):
+            return None
+
+        break
+    else:
         raise ValueError(f'Unknown hook name: {hook_name}')
         raise ValueError(f'Unknown hook name: {hook_name}')
 
 
     logger.debug(f'{log_prefix}: Calling {hook_name} hook function {function_name}')
     logger.debug(f'{log_prefix}: Calling {hook_name} hook function {function_name}')
+
     return getattr(module, function_name)(hook_config, config, log_prefix, *args, **kwargs)
     return getattr(module, function_name)(hook_config, config, log_prefix, *args, **kwargs)
 
 
 
 
-def call_hooks(function_name, config, log_prefix, hook_names, *args, **kwargs):
+def call_hooks(function_name, config, log_prefix, hook_type, *args, **kwargs):
     '''
     '''
     Given a configuration dict and a prefix to use in log entries, call the requested function of
     Given a configuration dict and a prefix to use in log entries, call the requested function of
-    the Python module corresponding to each given hook name. Supply each call with the configuration
-    for that hook, the log prefix, and any given args and kwargs. Collect any return values into a
-    dict from hook name to return value.
+    the Python module corresponding to each hook of the given hook type (either "data_source" or
+    "monitoring"). Supply each call with the configuration for that hook, the log prefix, and any
+    given args and kwargs.
+
+    Collect any return values into a dict from module name to return value. Note that the module
+    name is the name of the hook module itself, which might be different from the hook configuration
+    option (e.g. "postgresql" for the former vs. "postgresql_databases" for the latter).
 
 
     If the hook name is not present in the hooks configuration, then don't call the function for it
     If the hook name is not present in the hooks configuration, then don't call the function for it
     and omit it from the return values.
     and omit it from the return values.
@@ -80,22 +75,26 @@ def call_hooks(function_name, config, log_prefix, hook_names, *args, **kwargs):
     '''
     '''
     return {
     return {
         hook_name: call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs)
         hook_name: call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs)
-        for hook_name in hook_names
-        if hook_name in config
+        for hook_name in get_submodule_names(
+            importlib.import_module(f'borgmatic.hooks.{hook_type.value}')
+        )
+        if hook_name in config or f'{hook_name}_databases' in config
     }
     }
 
 
 
 
-def call_hooks_even_if_unconfigured(function_name, config, log_prefix, hook_names, *args, **kwargs):
+def call_hooks_even_if_unconfigured(function_name, config, log_prefix, hook_type, *args, **kwargs):
     '''
     '''
     Given a configuration dict and a prefix to use in log entries, call the requested function of
     Given a configuration dict and a prefix to use in log entries, call the requested function of
-    the Python module corresponding to each given hook name. Supply each call with the configuration
-    for that hook, the log prefix, and any given args and kwargs. Collect any return values into a
-    dict from hook name to return value.
+    the Python module corresponding to each hook of the given hook type (either "data_source" or
+    "monitoring"). Supply each call with the configuration for that hook, the log prefix, and any
+    given args and kwargs. Collect any return values into a dict from hook name to return value.
 
 
     Raise AttributeError if the function name is not found in the module.
     Raise AttributeError if the function name is not found in the module.
     Raise anything else that a called function raises. An error stops calls to subsequent functions.
     Raise anything else that a called function raises. An error stops calls to subsequent functions.
     '''
     '''
     return {
     return {
         hook_name: call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs)
         hook_name: call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs)
-        for hook_name in hook_names
+        for hook_name in get_submodule_names(
+            importlib.import_module(f'borgmatic.hooks.{hook_type.value}')
+        )
     }
     }

+ 0 - 21
borgmatic/hooks/monitor.py

@@ -1,21 +0,0 @@
-from enum import Enum
-
-MONITOR_HOOK_NAMES = (
-    'apprise',
-    'cronhub',
-    'cronitor',
-    'healthchecks',
-    'loki',
-    'ntfy',
-    'pagerduty',
-    'pushover',
-    'uptime_kuma',
-    'zabbix',
-)
-
-
-class State(Enum):
-    START = 1
-    FINISH = 2
-    FAIL = 3
-    LOG = 4

+ 0 - 0
borgmatic/hooks/monitoring/__init__.py


+ 12 - 10
borgmatic/hooks/apprise.py → borgmatic/hooks/monitoring/apprise.py

@@ -1,8 +1,8 @@
 import logging
 import logging
 import operator
 import operator
 
 
-import borgmatic.hooks.logs
-import borgmatic.hooks.monitor
+import borgmatic.hooks.monitoring.logs
+import borgmatic.hooks.monitoring.monitor
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -22,12 +22,12 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
 
 
     logs_size_limit = max(
     logs_size_limit = max(
         hook_config.get('logs_size_limit', DEFAULT_LOGS_SIZE_LIMIT_BYTES)
         hook_config.get('logs_size_limit', DEFAULT_LOGS_SIZE_LIMIT_BYTES)
-        - len(borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
+        - len(borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
         0,
         0,
     )
     )
 
 
-    borgmatic.hooks.logs.add_handler(
-        borgmatic.hooks.logs.Forgetful_buffering_handler(
+    borgmatic.hooks.monitoring.logs.add_handler(
+        borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
             HANDLER_IDENTIFIER, logs_size_limit, monitoring_log_level
             HANDLER_IDENTIFIER, logs_size_limit, monitoring_log_level
         )
         )
     )
     )
@@ -82,11 +82,13 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
     body = state_config.get('body')
     body = state_config.get('body')
 
 
     if state in (
     if state in (
-        borgmatic.hooks.monitor.State.FINISH,
-        borgmatic.hooks.monitor.State.FAIL,
-        borgmatic.hooks.monitor.State.LOG,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.LOG,
     ):
     ):
-        formatted_logs = borgmatic.hooks.logs.format_buffered_logs_for_payload(HANDLER_IDENTIFIER)
+        formatted_logs = borgmatic.hooks.monitoring.logs.format_buffered_logs_for_payload(
+            HANDLER_IDENTIFIER
+        )
         if formatted_logs:
         if formatted_logs:
             body += f'\n\n{formatted_logs}'
             body += f'\n\n{formatted_logs}'
 
 
@@ -106,4 +108,4 @@ def destroy_monitor(hook_config, config, config_filename, monitoring_log_level,
     Remove the monitor handler that was added to the root logger. This prevents the handler from
     Remove the monitor handler that was added to the root logger. This prevents the handler from
     getting reused by other instances of this monitor.
     getting reused by other instances of this monitor.
     '''
     '''
-    borgmatic.hooks.logs.remove_handler(HANDLER_IDENTIFIER)
+    borgmatic.hooks.monitoring.logs.remove_handler(HANDLER_IDENTIFIER)

+ 1 - 1
borgmatic/hooks/cronhub.py → borgmatic/hooks/monitoring/cronhub.py

@@ -2,7 +2,7 @@ import logging
 
 
 import requests
 import requests
 
 
-from borgmatic.hooks import monitor
+from borgmatic.hooks.monitoring import monitor
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 1 - 1
borgmatic/hooks/cronitor.py → borgmatic/hooks/monitoring/cronitor.py

@@ -2,7 +2,7 @@ import logging
 
 
 import requests
 import requests
 
 
-from borgmatic.hooks import monitor
+from borgmatic.hooks.monitoring import monitor
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 9 - 7
borgmatic/hooks/healthchecks.py → borgmatic/hooks/monitoring/healthchecks.py

@@ -3,8 +3,8 @@ import re
 
 
 import requests
 import requests
 
 
-import borgmatic.hooks.logs
-from borgmatic.hooks import monitor
+import borgmatic.hooks.monitoring.logs
+from borgmatic.hooks.monitoring import monitor
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -30,12 +30,12 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
 
 
     ping_body_limit = max(
     ping_body_limit = max(
         hook_config.get('ping_body_limit', DEFAULT_PING_BODY_LIMIT_BYTES)
         hook_config.get('ping_body_limit', DEFAULT_PING_BODY_LIMIT_BYTES)
-        - len(borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
+        - len(borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
         0,
         0,
     )
     )
 
 
-    borgmatic.hooks.logs.add_handler(
-        borgmatic.hooks.logs.Forgetful_buffering_handler(
+    borgmatic.hooks.monitoring.logs.add_handler(
+        borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
             HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level
             HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level
         )
         )
     )
     )
@@ -78,7 +78,9 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
     logger.debug(f'{config_filename}: Using Healthchecks ping URL {ping_url}')
     logger.debug(f'{config_filename}: Using Healthchecks ping URL {ping_url}')
 
 
     if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG):
     if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG):
-        payload = borgmatic.hooks.logs.format_buffered_logs_for_payload(HANDLER_IDENTIFIER)
+        payload = borgmatic.hooks.monitoring.logs.format_buffered_logs_for_payload(
+            HANDLER_IDENTIFIER
+        )
     else:
     else:
         payload = ''
         payload = ''
 
 
@@ -99,4 +101,4 @@ def destroy_monitor(hook_config, config, config_filename, monitoring_log_level,
     Remove the monitor handler that was added to the root logger. This prevents the handler from
     Remove the monitor handler that was added to the root logger. This prevents the handler from
     getting reused by other instances of this monitor.
     getting reused by other instances of this monitor.
     '''
     '''
-    borgmatic.hooks.logs.remove_handler(HANDLER_IDENTIFIER)
+    borgmatic.hooks.monitoring.logs.remove_handler(HANDLER_IDENTIFIER)

+ 1 - 0
borgmatic/hooks/logs.py → borgmatic/hooks/monitoring/logs.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+IS_A_HOOK = False
 PAYLOAD_TRUNCATION_INDICATOR = '...\n'
 PAYLOAD_TRUNCATION_INDICATOR = '...\n'
 
 
 
 

+ 1 - 1
borgmatic/hooks/loki.py → borgmatic/hooks/monitoring/loki.py

@@ -6,7 +6,7 @@ import time
 
 
 import requests
 import requests
 
 
-from borgmatic.hooks import monitor
+from borgmatic.hooks.monitoring import monitor
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 10 - 0
borgmatic/hooks/monitoring/monitor.py

@@ -0,0 +1,10 @@
+import enum
+
+IS_A_HOOK = False
+
+
+class State(enum.Enum):
+    START = 1
+    FINISH = 2
+    FAIL = 3
+    LOG = 4

+ 0 - 0
borgmatic/hooks/ntfy.py → borgmatic/hooks/monitoring/ntfy.py


+ 1 - 1
borgmatic/hooks/pagerduty.py → borgmatic/hooks/monitoring/pagerduty.py

@@ -5,7 +5,7 @@ import platform
 
 
 import requests
 import requests
 
 
-from borgmatic.hooks import monitor
+from borgmatic.hooks.monitoring import monitor
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 

+ 0 - 0
borgmatic/hooks/pushover.py → borgmatic/hooks/monitoring/pushover.py


+ 0 - 0
borgmatic/hooks/uptimekuma.py → borgmatic/hooks/monitoring/uptime_kuma.py


+ 0 - 0
borgmatic/hooks/zabbix.py → borgmatic/hooks/monitoring/zabbix.py


+ 4 - 2
docs/reference/source-code.md

@@ -16,8 +16,10 @@ you get started. Starting at the top level, we have:
    * [actions](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/actions): borgmatic-specific logic for running each action (create, list, check, etc.).
    * [actions](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/actions): borgmatic-specific logic for running each action (create, list, check, etc.).
    * [borg](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/borg): Lower-level code that's responsible for interacting with Borg to run each action.
    * [borg](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/borg): Lower-level code that's responsible for interacting with Borg to run each action.
    * [commands](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/commands): Looking to add a new flag or action? Start here. This contains borgmatic's entry point, argument parsing, and shell completion. 
    * [commands](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/commands): Looking to add a new flag or action? Start here. This contains borgmatic's entry point, argument parsing, and shell completion. 
-   * [config](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/config): Code responsible for loading, normalizing, and validating borgmatic's configuration.
-   * [hooks](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Looking to add a new database or monitoring integration? Start here.
+   * [config](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/config): Code responsible for loading, normalizing, and validating borgmatic's configuration. Interested in adding a new configuration option? Check out `schema.yaml` here.
+   * [hooks](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Looking to add a new database, filesystem, or monitoring integration? Start here.
+     * [data_source](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Database and filesystem hooks—anything that produces data or files to go into a backup archive.
+     * [monitoring](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Monitoring hooks—integrations with third-party or self-hosted monitoring services.
  * [docs](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/docs): How-to and reference documentation, including the document you're reading now.
  * [docs](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/docs): How-to and reference documentation, including the document you're reading now.
  * [sample](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/sample): Example configurations for cron and systemd.
  * [sample](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/sample): Example configurations for cron and systemd.
  * [scripts](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/scripts): Dev-facing scripts for things like building documentation and running end-to-end tests.
  * [scripts](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/scripts): Dev-facing scripts for things like building documentation and running end-to-end tests.

+ 0 - 0
tests/integration/hooks/__init__.py


+ 0 - 0
tests/integration/hooks/monitoring/__init__.py


+ 3 - 3
tests/integration/hooks/test_apprise.py → tests/integration/hooks/monitoring/test_apprise.py

@@ -2,14 +2,14 @@ import logging
 
 
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import apprise as module
+from borgmatic.hooks.monitoring import apprise as module
 
 
 
 
 def test_destroy_monitor_removes_apprise_handler():
 def test_destroy_monitor_removes_apprise_handler():
     logger = logging.getLogger()
     logger = logging.getLogger()
     original_handlers = list(logger.handlers)
     original_handlers = list(logger.handlers)
-    module.borgmatic.hooks.logs.add_handler(
-        module.borgmatic.hooks.logs.Forgetful_buffering_handler(
+    module.borgmatic.hooks.monitoring.logs.add_handler(
+        module.borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
             identifier=module.HANDLER_IDENTIFIER, byte_capacity=100, log_level=1
             identifier=module.HANDLER_IDENTIFIER, byte_capacity=100, log_level=1
         )
         )
     )
     )

+ 3 - 3
tests/integration/hooks/test_healthchecks.py → tests/integration/hooks/monitoring/test_healthchecks.py

@@ -2,14 +2,14 @@ import logging
 
 
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import healthchecks as module
+from borgmatic.hooks.monitoring import healthchecks as module
 
 
 
 
 def test_destroy_monitor_removes_healthchecks_handler():
 def test_destroy_monitor_removes_healthchecks_handler():
     logger = logging.getLogger()
     logger = logging.getLogger()
     original_handlers = list(logger.handlers)
     original_handlers = list(logger.handlers)
-    module.borgmatic.hooks.logs.add_handler(
-        module.borgmatic.hooks.logs.Forgetful_buffering_handler(
+    module.borgmatic.hooks.monitoring.logs.add_handler(
+        module.borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
             identifier=module.HANDLER_IDENTIFIER, byte_capacity=100, log_level=1
             identifier=module.HANDLER_IDENTIFIER, byte_capacity=100, log_level=1
         )
         )
     )
     )

+ 1 - 1
tests/integration/hooks/test_loki.py → tests/integration/hooks/monitoring/test_loki.py

@@ -3,7 +3,7 @@ import platform
 
 
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import loki as module
+from borgmatic.hooks.monitoring import loki as module
 
 
 
 
 def test_initialize_monitor_replaces_labels():
 def test_initialize_monitor_replaces_labels():

+ 24 - 24
tests/unit/actions/test_restore.py

@@ -90,7 +90,7 @@ def test_restore_single_data_source_extracts_and_restores_single_file_dump():
         'make_data_source_dump_patterns', object, object, object, object, object
         'make_data_source_dump_patterns', object, object, object, object, object
     ).and_return({'postgresql': flexmock()})
     ).and_return({'postgresql': flexmock()})
     flexmock(module.tempfile).should_receive('mkdtemp').never()
     flexmock(module.tempfile).should_receive('mkdtemp').never()
-    flexmock(module.borgmatic.hooks.dump).should_receive(
+    flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
         'convert_glob_patterns_to_borg_pattern'
         'convert_glob_patterns_to_borg_pattern'
     ).and_return(flexmock())
     ).and_return(flexmock())
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
@@ -98,11 +98,11 @@ def test_restore_single_data_source_extracts_and_restores_single_file_dump():
     ).once()
     ).once()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').never()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').never()
     flexmock(module.shutil).should_receive('rmtree').never()
     flexmock(module.shutil).should_receive('rmtree').never()
-    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
         function_name='restore_data_source_dump',
         function_name='restore_data_source_dump',
         config=object,
         config=object,
         log_prefix=object,
         log_prefix=object,
-        hook_names=object,
+        hook_name=object,
         data_source=object,
         data_source=object,
         dry_run=object,
         dry_run=object,
         extract_process=object,
         extract_process=object,
@@ -132,7 +132,7 @@ def test_restore_single_data_source_extracts_and_restores_directory_dump():
     flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
     flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
         '/run/user/0/borgmatic/tmp1234'
         '/run/user/0/borgmatic/tmp1234'
     )
     )
-    flexmock(module.borgmatic.hooks.dump).should_receive(
+    flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
         'convert_glob_patterns_to_borg_pattern'
         'convert_glob_patterns_to_borg_pattern'
     ).and_return(flexmock())
     ).and_return(flexmock())
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
@@ -140,11 +140,11 @@ def test_restore_single_data_source_extracts_and_restores_directory_dump():
     ).once()
     ).once()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').once()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').once()
     flexmock(module.shutil).should_receive('rmtree').once()
     flexmock(module.shutil).should_receive('rmtree').once()
-    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
         function_name='restore_data_source_dump',
         function_name='restore_data_source_dump',
         config=object,
         config=object,
         log_prefix=object,
         log_prefix=object,
-        hook_names=object,
+        hook_name=object,
         data_source=object,
         data_source=object,
         dry_run=object,
         dry_run=object,
         extract_process=object,
         extract_process=object,
@@ -174,7 +174,7 @@ def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporar
     flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
     flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
         '/run/user/0/borgmatic/tmp1234'
         '/run/user/0/borgmatic/tmp1234'
     )
     )
-    flexmock(module.borgmatic.hooks.dump).should_receive(
+    flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
         'convert_glob_patterns_to_borg_pattern'
         'convert_glob_patterns_to_borg_pattern'
     ).and_return(flexmock())
     ).and_return(flexmock())
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_raise(
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_raise(
@@ -182,11 +182,11 @@ def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporar
     ).once()
     ).once()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').never()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').never()
     flexmock(module.shutil).should_receive('rmtree').once()
     flexmock(module.shutil).should_receive('rmtree').once()
-    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
         function_name='restore_data_source_dump',
         function_name='restore_data_source_dump',
         config=object,
         config=object,
         log_prefix=object,
         log_prefix=object,
-        hook_names=object,
+        hook_name=object,
         data_source=object,
         data_source=object,
         dry_run=object,
         dry_run=object,
         extract_process=object,
         extract_process=object,
@@ -215,7 +215,7 @@ def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_direct
         'make_data_source_dump_patterns', object, object, object, object, object
         'make_data_source_dump_patterns', object, object, object, object, object
     ).and_return({'postgresql': flexmock()})
     ).and_return({'postgresql': flexmock()})
     flexmock(module.tempfile).should_receive('mkdtemp').once().and_return('/run/borgmatic/tmp1234')
     flexmock(module.tempfile).should_receive('mkdtemp').once().and_return('/run/borgmatic/tmp1234')
-    flexmock(module.borgmatic.hooks.dump).should_receive(
+    flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
         'convert_glob_patterns_to_borg_pattern'
         'convert_glob_patterns_to_borg_pattern'
     ).and_return(flexmock())
     ).and_return(flexmock())
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
@@ -223,11 +223,11 @@ def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_direct
     ).once()
     ).once()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').never()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').never()
     flexmock(module.shutil).should_receive('rmtree').never()
     flexmock(module.shutil).should_receive('rmtree').never()
-    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
         function_name='restore_data_source_dump',
         function_name='restore_data_source_dump',
         config=object,
         config=object,
         log_prefix=object,
         log_prefix=object,
-        hook_names=object,
+        hook_name=object,
         data_source=object,
         data_source=object,
         dry_run=object,
         dry_run=object,
         extract_process=object,
         extract_process=object,
@@ -254,9 +254,9 @@ def test_collect_archive_data_source_names_parses_archive_paths():
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
         'get_borgmatic_source_directory'
     ).and_return('/root/.borgmatic')
     ).and_return('/root/.borgmatic')
-    flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
-        ''
-    )
+    flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
+        'make_data_source_dump_path'
+    ).and_return('')
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         [
         [
             'borgmatic/postgresql_databases/localhost/foo',
             'borgmatic/postgresql_databases/localhost/foo',
@@ -286,9 +286,9 @@ def test_collect_archive_data_source_names_parses_archive_paths_with_different_b
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
         'get_borgmatic_source_directory'
     ).and_return('/root/.borgmatic')
     ).and_return('/root/.borgmatic')
-    flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
-        ''
-    )
+    flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
+        'make_data_source_dump_path'
+    ).and_return('')
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         [
         [
             'borgmatic/postgresql_databases/localhost/foo',
             'borgmatic/postgresql_databases/localhost/foo',
@@ -319,9 +319,9 @@ def test_collect_archive_data_source_names_parses_directory_format_archive_paths
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
         'get_borgmatic_source_directory'
     ).and_return('/root/.borgmatic')
     ).and_return('/root/.borgmatic')
-    flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
-        ''
-    )
+    flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
+        'make_data_source_dump_path'
+    ).and_return('')
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         [
         [
             'borgmatic/postgresql_databases/localhost/foo/table1',
             'borgmatic/postgresql_databases/localhost/foo/table1',
@@ -349,9 +349,9 @@ def test_collect_archive_data_source_names_skips_bad_archive_paths():
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
         'get_borgmatic_source_directory'
     ).and_return('/root/.borgmatic')
     ).and_return('/root/.borgmatic')
-    flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
-        ''
-    )
+    flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
+        'make_data_source_dump_path'
+    ).and_return('')
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         [
         [
             'borgmatic/postgresql_databases/localhost/foo',
             'borgmatic/postgresql_databases/localhost/foo',

+ 0 - 0
tests/unit/hooks/data_source/__init__.py


+ 1 - 1
tests/unit/hooks/test_bootstrap.py → tests/unit/hooks/data_source/test_bootstrap.py

@@ -2,7 +2,7 @@ import sys
 
 
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import bootstrap as module
+from borgmatic.hooks.data_source import bootstrap as module
 
 
 
 
 def test_dump_data_sources_creates_manifest_file():
 def test_dump_data_sources_creates_manifest_file():

+ 1 - 1
tests/unit/hooks/test_dump.py → tests/unit/hooks/data_source/test_dump.py

@@ -1,7 +1,7 @@
 import pytest
 import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import dump as module
+from borgmatic.hooks.data_source import dump as module
 
 
 
 
 def test_make_data_source_dump_path_joins_arguments():
 def test_make_data_source_dump_path_joins_arguments():

+ 1 - 1
tests/unit/hooks/test_mariadb.py → tests/unit/hooks/data_source/test_mariadb.py

@@ -3,7 +3,7 @@ import logging
 import pytest
 import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import mariadb as module
+from borgmatic.hooks.data_source import mariadb as module
 
 
 
 
 def test_database_names_to_dump_passes_through_name():
 def test_database_names_to_dump_passes_through_name():

+ 1 - 1
tests/unit/hooks/test_mongodb.py → tests/unit/hooks/data_source/test_mongodb.py

@@ -2,7 +2,7 @@ import logging
 
 
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import mongodb as module
+from borgmatic.hooks.data_source import mongodb as module
 
 
 
 
 def test_use_streaming_true_for_any_non_directory_format_databases():
 def test_use_streaming_true_for_any_non_directory_format_databases():

+ 1 - 1
tests/unit/hooks/test_mysql.py → tests/unit/hooks/data_source/test_mysql.py

@@ -3,7 +3,7 @@ import logging
 import pytest
 import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import mysql as module
+from borgmatic.hooks.data_source import mysql as module
 
 
 
 
 def test_database_names_to_dump_passes_through_name():
 def test_database_names_to_dump_passes_through_name():

+ 1 - 1
tests/unit/hooks/test_postgresql.py → tests/unit/hooks/data_source/test_postgresql.py

@@ -3,7 +3,7 @@ import logging
 import pytest
 import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import postgresql as module
+from borgmatic.hooks.data_source import postgresql as module
 
 
 
 
 def test_make_extra_environment_maps_options_to_environment():
 def test_make_extra_environment_maps_options_to_environment():

+ 1 - 1
tests/unit/hooks/test_sqlite.py → tests/unit/hooks/data_source/test_sqlite.py

@@ -2,7 +2,7 @@ import logging
 
 
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import sqlite as module
+from borgmatic.hooks.data_source import sqlite as module
 
 
 
 
 def test_use_streaming_true_for_any_databases():
 def test_use_streaming_true_for_any_databases():

+ 1 - 1
tests/unit/hooks/test_zfs.py → tests/unit/hooks/data_source/test_zfs.py

@@ -2,7 +2,7 @@ import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
 import borgmatic.execute
 import borgmatic.execute
-from borgmatic.hooks import zfs as module
+from borgmatic.hooks.data_source import zfs as module
 
 
 
 
 def test_get_datasets_to_backup_filters_datasets_by_source_directories():
 def test_get_datasets_to_backup_filters_datasets_by_source_directories():

+ 0 - 0
tests/unit/hooks/monitoring/__init__.py


+ 60 - 50
tests/unit/hooks/test_apprise.py → tests/unit/hooks/monitoring/test_apprise.py

@@ -2,8 +2,8 @@ import apprise
 from apprise import NotifyFormat, NotifyType
 from apprise import NotifyFormat, NotifyType
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-import borgmatic.hooks.monitor
-from borgmatic.hooks import apprise as module
+import borgmatic.hooks.monitoring.monitor
+from borgmatic.hooks.monitoring import apprise as module
 
 
 TOPIC = 'borgmatic-unit-testing'
 TOPIC = 'borgmatic-unit-testing'
 
 
@@ -18,7 +18,7 @@ def mock_apprise():
 
 
 
 
 def test_initialize_monitor_with_send_logs_false_does_not_add_handler():
 def test_initialize_monitor_with_send_logs_false_does_not_add_handler():
-    flexmock(module.borgmatic.hooks.logs).should_receive('add_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('add_handler').never()
 
 
     module.initialize_monitor(
     module.initialize_monitor(
         hook_config={'send_logs': False},
         hook_config={'send_logs': False},
@@ -31,12 +31,14 @@ def test_initialize_monitor_with_send_logs_false_does_not_add_handler():
 
 
 def test_initialize_monitor_with_send_logs_true_adds_handler_with_default_log_size_limit():
 def test_initialize_monitor_with_send_logs_true_adds_handler_with_default_log_size_limit():
     truncation_indicator_length = 4
     truncation_indicator_length = 4
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).with_args(
         module.HANDLER_IDENTIFIER,
         module.HANDLER_IDENTIFIER,
         module.DEFAULT_LOGS_SIZE_LIMIT_BYTES - truncation_indicator_length,
         module.DEFAULT_LOGS_SIZE_LIMIT_BYTES - truncation_indicator_length,
         1,
         1,
     ).once()
     ).once()
-    flexmock(module.borgmatic.hooks.logs).should_receive('add_handler').once()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('add_handler').once()
 
 
     module.initialize_monitor(
     module.initialize_monitor(
         hook_config={'send_logs': True},
         hook_config={'send_logs': True},
@@ -49,12 +51,14 @@ def test_initialize_monitor_with_send_logs_true_adds_handler_with_default_log_si
 
 
 def test_initialize_monitor_without_send_logs_adds_handler_with_default_log_size_limit():
 def test_initialize_monitor_without_send_logs_adds_handler_with_default_log_size_limit():
     truncation_indicator_length = 4
     truncation_indicator_length = 4
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).with_args(
         module.HANDLER_IDENTIFIER,
         module.HANDLER_IDENTIFIER,
         module.DEFAULT_LOGS_SIZE_LIMIT_BYTES - truncation_indicator_length,
         module.DEFAULT_LOGS_SIZE_LIMIT_BYTES - truncation_indicator_length,
         1,
         1,
     ).once()
     ).once()
-    flexmock(module.borgmatic.hooks.logs).should_receive('add_handler').once()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('add_handler').once()
 
 
     module.initialize_monitor(
     module.initialize_monitor(
         hook_config={},
         hook_config={},
@@ -66,8 +70,8 @@ def test_initialize_monitor_without_send_logs_adds_handler_with_default_log_size
 
 
 
 
 def test_ping_monitor_respects_dry_run():
 def test_ping_monitor_respects_dry_run():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('loggy log')
     ).and_return('loggy log')
     mock_apprise().should_receive('notify').never()
     mock_apprise().should_receive('notify').never()
@@ -76,30 +80,32 @@ def test_ping_monitor_respects_dry_run():
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=True,
         dry_run=True,
     )
     )
 
 
 
 
 def test_ping_monitor_with_no_states_does_not_notify():
 def test_ping_monitor_with_no_states_does_not_notify():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler').never()
-    flexmock(module.borgmatic.hooks.logs).should_receive('format_buffered_logs_for_payload').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'format_buffered_logs_for_payload'
+    ).never()
     mock_apprise().should_receive('notify').never()
     mock_apprise().should_receive('notify').never()
 
 
     module.ping_monitor(
     module.ping_monitor(
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': []},
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': []},
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=True,
         dry_run=True,
     )
     )
 
 
 
 
 def test_ping_monitor_notifies_fail_by_default():
 def test_ping_monitor_notifies_fail_by_default():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('')
     ).and_return('')
     mock_apprise().should_receive('notify').with_args(
     mock_apprise().should_receive('notify').with_args(
@@ -109,7 +115,7 @@ def test_ping_monitor_notifies_fail_by_default():
         notify_type=NotifyType.FAILURE,
         notify_type=NotifyType.FAILURE,
     ).once()
     ).once()
 
 
-    for state in borgmatic.hooks.monitor.State:
+    for state in borgmatic.hooks.monitoring.monitor.State:
         module.ping_monitor(
         module.ping_monitor(
             {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
             {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
             {},
             {},
@@ -121,8 +127,8 @@ def test_ping_monitor_notifies_fail_by_default():
 
 
 
 
 def test_ping_monitor_with_logs_appends_logs_to_body():
 def test_ping_monitor_with_logs_appends_logs_to_body():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('loggy log')
     ).and_return('loggy log')
     mock_apprise().should_receive('notify').with_args(
     mock_apprise().should_receive('notify').with_args(
@@ -132,7 +138,7 @@ def test_ping_monitor_with_logs_appends_logs_to_body():
         notify_type=NotifyType.FAILURE,
         notify_type=NotifyType.FAILURE,
     ).once()
     ).once()
 
 
-    for state in borgmatic.hooks.monitor.State:
+    for state in borgmatic.hooks.monitoring.monitor.State:
         module.ping_monitor(
         module.ping_monitor(
             {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
             {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
             {},
             {},
@@ -144,8 +150,8 @@ def test_ping_monitor_with_logs_appends_logs_to_body():
 
 
 
 
 def test_ping_monitor_with_finish_default_config_notifies():
 def test_ping_monitor_with_finish_default_config_notifies():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('')
     ).and_return('')
     mock_apprise().should_receive('notify').with_args(
     mock_apprise().should_receive('notify').with_args(
@@ -159,15 +165,17 @@ def test_ping_monitor_with_finish_default_config_notifies():
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['finish']},
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['finish']},
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
 
 
 
 
 def test_ping_monitor_with_start_default_config_notifies():
 def test_ping_monitor_with_start_default_config_notifies():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler').never()
-    flexmock(module.borgmatic.hooks.logs).should_receive('format_buffered_logs_for_payload').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'format_buffered_logs_for_payload'
+    ).never()
     mock_apprise().should_receive('notify').with_args(
     mock_apprise().should_receive('notify').with_args(
         title='A borgmatic START event happened',
         title='A borgmatic START event happened',
         body='A borgmatic START event happened',
         body='A borgmatic START event happened',
@@ -179,15 +187,15 @@ def test_ping_monitor_with_start_default_config_notifies():
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['start']},
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['start']},
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
 
 
 
 
 def test_ping_monitor_with_fail_default_config_notifies():
 def test_ping_monitor_with_fail_default_config_notifies():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('')
     ).and_return('')
     mock_apprise().should_receive('notify').with_args(
     mock_apprise().should_receive('notify').with_args(
@@ -201,15 +209,15 @@ def test_ping_monitor_with_fail_default_config_notifies():
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['fail']},
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['fail']},
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
 
 
 
 
 def test_ping_monitor_with_log_default_config_notifies():
 def test_ping_monitor_with_log_default_config_notifies():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('')
     ).and_return('')
     mock_apprise().should_receive('notify').with_args(
     mock_apprise().should_receive('notify').with_args(
@@ -223,15 +231,15 @@ def test_ping_monitor_with_log_default_config_notifies():
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['log']},
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['log']},
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.LOG,
+        borgmatic.hooks.monitoring.monitor.State.LOG,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
 
 
 
 
 def test_ping_monitor_passes_through_custom_message_title():
 def test_ping_monitor_passes_through_custom_message_title():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('')
     ).and_return('')
     mock_apprise().should_receive('notify').with_args(
     mock_apprise().should_receive('notify').with_args(
@@ -249,15 +257,15 @@ def test_ping_monitor_passes_through_custom_message_title():
         },
         },
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
 
 
 
 
 def test_ping_monitor_passes_through_custom_message_body():
 def test_ping_monitor_passes_through_custom_message_body():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('')
     ).and_return('')
     mock_apprise().should_receive('notify').with_args(
     mock_apprise().should_receive('notify').with_args(
@@ -275,15 +283,15 @@ def test_ping_monitor_passes_through_custom_message_body():
         },
         },
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
 
 
 
 
 def test_ping_monitor_passes_through_custom_message_body_and_appends_logs():
 def test_ping_monitor_passes_through_custom_message_body_and_appends_logs():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('loggy log')
     ).and_return('loggy log')
     mock_apprise().should_receive('notify').with_args(
     mock_apprise().should_receive('notify').with_args(
@@ -301,15 +309,15 @@ def test_ping_monitor_passes_through_custom_message_body_and_appends_logs():
         },
         },
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
 
 
 
 
 def test_ping_monitor_pings_multiple_services():
 def test_ping_monitor_pings_multiple_services():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('')
     ).and_return('')
     mock_apprise().should_receive('add').with_args([f'ntfys://{TOPIC}', f'ntfy://{TOPIC}']).once()
     mock_apprise().should_receive('add').with_args([f'ntfys://{TOPIC}', f'ntfy://{TOPIC}']).once()
@@ -323,36 +331,38 @@ def test_ping_monitor_pings_multiple_services():
         },
         },
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
 
 
 
 
 def test_ping_monitor_logs_info_for_no_services():
 def test_ping_monitor_logs_info_for_no_services():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler').never()
-    flexmock(module.borgmatic.hooks.logs).should_receive('format_buffered_logs_for_payload').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'format_buffered_logs_for_payload'
+    ).never()
     flexmock(module.logger).should_receive('info').once()
     flexmock(module.logger).should_receive('info').once()
 
 
     module.ping_monitor(
     module.ping_monitor(
         {'services': []},
         {'services': []},
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
 
 
 
 
 def test_ping_monitor_logs_warning_when_notify_fails():
 def test_ping_monitor_logs_warning_when_notify_fails():
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return('')
     ).and_return('')
     mock_apprise().should_receive('notify').and_return(False)
     mock_apprise().should_receive('notify').and_return(False)
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
 
 
-    for state in borgmatic.hooks.monitor.State:
+    for state in borgmatic.hooks.monitoring.monitor.State:
         module.ping_monitor(
         module.ping_monitor(
             {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
             {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
             {},
             {},
@@ -364,7 +374,7 @@ def test_ping_monitor_logs_warning_when_notify_fails():
 
 
 
 
 def test_destroy_monitor_does_not_raise():
 def test_destroy_monitor_does_not_raise():
-    flexmock(module.borgmatic.hooks.logs).should_receive('remove_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('remove_handler')
 
 
     module.destroy_monitor(
     module.destroy_monitor(
         hook_config={},
         hook_config={},

+ 1 - 1
tests/unit/hooks/test_cronhub.py → tests/unit/hooks/monitoring/test_cronhub.py

@@ -1,6 +1,6 @@
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import cronhub as module
+from borgmatic.hooks.monitoring import cronhub as module
 
 
 
 
 def test_ping_monitor_rewrites_ping_url_for_start_state():
 def test_ping_monitor_rewrites_ping_url_for_start_state():

+ 1 - 1
tests/unit/hooks/test_cronitor.py → tests/unit/hooks/monitoring/test_cronitor.py

@@ -1,6 +1,6 @@
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import cronitor as module
+from borgmatic.hooks.monitoring import cronitor as module
 
 
 
 
 def test_ping_monitor_hits_ping_url_for_start_state():
 def test_ping_monitor_hits_ping_url_for_start_state():

+ 54 - 30
tests/unit/hooks/test_healthchecks.py → tests/unit/hooks/monitoring/test_healthchecks.py

@@ -1,6 +1,6 @@
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import healthchecks as module
+from borgmatic.hooks.monitoring import healthchecks as module
 
 
 
 
 def mock_logger():
 def mock_logger():
@@ -15,9 +15,11 @@ def test_initialize_monitor_creates_log_handler_with_ping_body_limit():
     monitoring_log_level = 1
     monitoring_log_level = 1
 
 
     mock_logger()
     mock_logger()
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).with_args(
         module.HANDLER_IDENTIFIER,
         module.HANDLER_IDENTIFIER,
-        ping_body_limit - len(module.borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
+        ping_body_limit - len(module.borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
         monitoring_log_level,
         monitoring_log_level,
     ).once()
     ).once()
 
 
@@ -30,10 +32,12 @@ def test_initialize_monitor_creates_log_handler_with_default_ping_body_limit():
     monitoring_log_level = 1
     monitoring_log_level = 1
 
 
     mock_logger()
     mock_logger()
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).with_args(
         module.HANDLER_IDENTIFIER,
         module.HANDLER_IDENTIFIER,
         module.DEFAULT_PING_BODY_LIMIT_BYTES
         module.DEFAULT_PING_BODY_LIMIT_BYTES
-        - len(module.borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
+        - len(module.borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
         monitoring_log_level,
         monitoring_log_level,
     ).once()
     ).once()
 
 
@@ -45,9 +49,9 @@ def test_initialize_monitor_creates_log_handler_with_zero_ping_body_limit():
     monitoring_log_level = 1
     monitoring_log_level = 1
 
 
     mock_logger()
     mock_logger()
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
-        module.HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level
-    ).once()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).with_args(module.HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level).once()
 
 
     module.initialize_monitor(
     module.initialize_monitor(
         {'ping_body_limit': ping_body_limit}, {}, 'test.yaml', monitoring_log_level, dry_run=False
         {'ping_body_limit': ping_body_limit}, {}, 'test.yaml', monitoring_log_level, dry_run=False
@@ -56,7 +60,9 @@ def test_initialize_monitor_creates_log_handler_with_zero_ping_body_limit():
 
 
 def test_initialize_monitor_creates_log_handler_when_send_logs_true():
 def test_initialize_monitor_creates_log_handler_when_send_logs_true():
     mock_logger()
     mock_logger()
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').once()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).once()
 
 
     module.initialize_monitor(
     module.initialize_monitor(
         {'send_logs': True}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
         {'send_logs': True}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
@@ -65,7 +71,9 @@ def test_initialize_monitor_creates_log_handler_when_send_logs_true():
 
 
 def test_initialize_monitor_bails_when_send_logs_false():
 def test_initialize_monitor_bails_when_send_logs_false():
     mock_logger()
     mock_logger()
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).never()
 
 
     module.initialize_monitor(
     module.initialize_monitor(
         {'send_logs': False}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
         {'send_logs': False}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
@@ -73,7 +81,9 @@ def test_initialize_monitor_bails_when_send_logs_false():
 
 
 
 
 def test_ping_monitor_hits_ping_url_for_start_state():
 def test_ping_monitor_hits_ping_url_for_start_state():
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).never()
     hook_config = {'ping_url': 'https://example.com'}
     hook_config = {'ping_url': 'https://example.com'}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://example.com/start', data=''.encode('utf-8'), verify=True
         'https://example.com/start', data=''.encode('utf-8'), verify=True
@@ -92,8 +102,8 @@ def test_ping_monitor_hits_ping_url_for_start_state():
 def test_ping_monitor_hits_ping_url_for_finish_state():
 def test_ping_monitor_hits_ping_url_for_finish_state():
     hook_config = {'ping_url': 'https://example.com'}
     hook_config = {'ping_url': 'https://example.com'}
     payload = 'data'
     payload = 'data'
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return(payload)
     ).and_return(payload)
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
@@ -113,8 +123,8 @@ def test_ping_monitor_hits_ping_url_for_finish_state():
 def test_ping_monitor_hits_ping_url_for_fail_state():
 def test_ping_monitor_hits_ping_url_for_fail_state():
     hook_config = {'ping_url': 'https://example.com'}
     hook_config = {'ping_url': 'https://example.com'}
     payload = 'data'
     payload = 'data'
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return(payload)
     ).and_return(payload)
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
@@ -134,8 +144,8 @@ def test_ping_monitor_hits_ping_url_for_fail_state():
 def test_ping_monitor_hits_ping_url_for_log_state():
 def test_ping_monitor_hits_ping_url_for_log_state():
     hook_config = {'ping_url': 'https://example.com'}
     hook_config = {'ping_url': 'https://example.com'}
     payload = 'data'
     payload = 'data'
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return(payload)
     ).and_return(payload)
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
@@ -155,8 +165,8 @@ def test_ping_monitor_hits_ping_url_for_log_state():
 def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
 def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
     hook_config = {'ping_url': 'abcd-efgh-ijkl-mnop'}
     hook_config = {'ping_url': 'abcd-efgh-ijkl-mnop'}
     payload = 'data'
     payload = 'data'
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return(payload)
     ).and_return(payload)
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
@@ -178,8 +188,8 @@ def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
 def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
 def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
     hook_config = {'ping_url': 'https://example.com', 'verify_tls': False}
     hook_config = {'ping_url': 'https://example.com', 'verify_tls': False}
     payload = 'data'
     payload = 'data'
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return(payload)
     ).and_return(payload)
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
@@ -199,8 +209,8 @@ def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
 def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
 def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
     hook_config = {'ping_url': 'https://example.com', 'verify_tls': True}
     hook_config = {'ping_url': 'https://example.com', 'verify_tls': True}
     payload = 'data'
     payload = 'data'
-    flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
-    flexmock(module.borgmatic.hooks.logs).should_receive(
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
         'format_buffered_logs_for_payload'
         'format_buffered_logs_for_payload'
     ).and_return(payload)
     ).and_return(payload)
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
@@ -218,7 +228,9 @@ def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
 
 
 
 
 def test_ping_monitor_dry_run_does_not_hit_ping_url():
 def test_ping_monitor_dry_run_does_not_hit_ping_url():
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).never()
     hook_config = {'ping_url': 'https://example.com'}
     hook_config = {'ping_url': 'https://example.com'}
     flexmock(module.requests).should_receive('post').never()
     flexmock(module.requests).should_receive('post').never()
 
 
@@ -233,7 +245,9 @@ def test_ping_monitor_dry_run_does_not_hit_ping_url():
 
 
 
 
 def test_ping_monitor_does_not_hit_ping_url_when_states_not_matching():
 def test_ping_monitor_does_not_hit_ping_url_when_states_not_matching():
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).never()
     hook_config = {'ping_url': 'https://example.com', 'states': ['finish']}
     hook_config = {'ping_url': 'https://example.com', 'states': ['finish']}
     flexmock(module.requests).should_receive('post').never()
     flexmock(module.requests).should_receive('post').never()
 
 
@@ -248,7 +262,9 @@ def test_ping_monitor_does_not_hit_ping_url_when_states_not_matching():
 
 
 
 
 def test_ping_monitor_hits_ping_url_when_states_matching():
 def test_ping_monitor_hits_ping_url_when_states_matching():
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).never()
     hook_config = {'ping_url': 'https://example.com', 'states': ['start', 'finish']}
     hook_config = {'ping_url': 'https://example.com', 'states': ['start', 'finish']}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://example.com/start', data=''.encode('utf-8'), verify=True
         'https://example.com/start', data=''.encode('utf-8'), verify=True
@@ -265,7 +281,9 @@ def test_ping_monitor_hits_ping_url_when_states_matching():
 
 
 
 
 def test_ping_monitor_adds_create_query_parameter_when_create_slug_true():
 def test_ping_monitor_adds_create_query_parameter_when_create_slug_true():
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).never()
     hook_config = {'ping_url': 'https://example.com', 'create_slug': True}
     hook_config = {'ping_url': 'https://example.com', 'create_slug': True}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://example.com/start?create=1', data=''.encode('utf-8'), verify=True
         'https://example.com/start?create=1', data=''.encode('utf-8'), verify=True
@@ -282,7 +300,9 @@ def test_ping_monitor_adds_create_query_parameter_when_create_slug_true():
 
 
 
 
 def test_ping_monitor_does_not_add_create_query_parameter_when_create_slug_false():
 def test_ping_monitor_does_not_add_create_query_parameter_when_create_slug_false():
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).never()
     hook_config = {'ping_url': 'https://example.com', 'create_slug': False}
     hook_config = {'ping_url': 'https://example.com', 'create_slug': False}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://example.com/start', data=''.encode('utf-8'), verify=True
         'https://example.com/start', data=''.encode('utf-8'), verify=True
@@ -334,7 +354,9 @@ def test_ping_monitor_issues_warning_when_ping_url_is_uuid_and_create_slug_true(
 
 
 
 
 def test_ping_monitor_with_connection_error_logs_warning():
 def test_ping_monitor_with_connection_error_logs_warning():
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).never()
     hook_config = {'ping_url': 'https://example.com'}
     hook_config = {'ping_url': 'https://example.com'}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://example.com/start', data=''.encode('utf-8'), verify=True
         'https://example.com/start', data=''.encode('utf-8'), verify=True
@@ -352,7 +374,9 @@ def test_ping_monitor_with_connection_error_logs_warning():
 
 
 
 
 def test_ping_monitor_with_other_error_logs_warning():
 def test_ping_monitor_with_other_error_logs_warning():
-    flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
+        'Forgetful_buffering_handler'
+    ).never()
     hook_config = {'ping_url': 'https://example.com'}
     hook_config = {'ping_url': 'https://example.com'}
     response = flexmock(ok=False)
     response = flexmock(ok=False)
     response.should_receive('raise_for_status').and_raise(
     response.should_receive('raise_for_status').and_raise(

+ 1 - 1
tests/unit/hooks/test_logs.py → tests/unit/hooks/monitoring/test_logs.py

@@ -1,7 +1,7 @@
 import pytest
 import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import logs as module
+from borgmatic.hooks.monitoring import logs as module
 
 
 
 
 def test_forgetful_buffering_handler_emit_collects_log_records():
 def test_forgetful_buffering_handler_emit_collects_log_records():

+ 1 - 1
tests/unit/hooks/test_loki.py → tests/unit/hooks/monitoring/test_loki.py

@@ -3,7 +3,7 @@ import json
 import requests
 import requests
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import loki as module
+from borgmatic.hooks.monitoring import loki as module
 
 
 
 
 def test_loki_log_buffer_add_value_gets_raw():
 def test_loki_log_buffer_add_value_gets_raw():

+ 26 - 26
tests/unit/hooks/test_ntfy.py → tests/unit/hooks/monitoring/test_ntfy.py

@@ -2,8 +2,8 @@ from enum import Enum
 
 
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-import borgmatic.hooks.monitor
-from borgmatic.hooks import ntfy as module
+import borgmatic.hooks.monitoring.monitor
+from borgmatic.hooks.monitoring import ntfy as module
 
 
 default_base_url = 'https://ntfy.sh'
 default_base_url = 'https://ntfy.sh'
 custom_base_url = 'https://ntfy.example.com'
 custom_base_url = 'https://ntfy.example.com'
@@ -38,7 +38,7 @@ def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail():
     hook_config = {'topic': topic}
     hook_config = {'topic': topic}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'{default_base_url}/{topic}',
         f'{default_base_url}/{topic}',
-        headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+        headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
         auth=None,
         auth=None,
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
@@ -46,7 +46,7 @@ def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -59,7 +59,7 @@ def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail():
     }
     }
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'{default_base_url}/{topic}',
         f'{default_base_url}/{topic}',
-        headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+        headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
         auth=module.requests.auth.HTTPBasicAuth('', 'abc123'),
         auth=module.requests.auth.HTTPBasicAuth('', 'abc123'),
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
@@ -67,7 +67,7 @@ def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -82,7 +82,7 @@ def test_ping_monitor_with_username_password_and_access_token_ignores_username_p
     }
     }
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'{default_base_url}/{topic}',
         f'{default_base_url}/{topic}',
-        headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+        headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
         auth=module.requests.auth.HTTPBasicAuth('', 'abc123'),
         auth=module.requests.auth.HTTPBasicAuth('', 'abc123'),
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
@@ -91,7 +91,7 @@ def test_ping_monitor_with_username_password_and_access_token_ignores_username_p
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -105,7 +105,7 @@ def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail():
     }
     }
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'{default_base_url}/{topic}',
         f'{default_base_url}/{topic}',
-        headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+        headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
         auth=module.requests.auth.HTTPBasicAuth('testuser', 'fakepassword'),
         auth=module.requests.auth.HTTPBasicAuth('testuser', 'fakepassword'),
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
@@ -113,7 +113,7 @@ def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -123,7 +123,7 @@ def test_ping_monitor_with_password_but_no_username_warns():
     hook_config = {'topic': topic, 'password': 'fakepassword'}
     hook_config = {'topic': topic, 'password': 'fakepassword'}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'{default_base_url}/{topic}',
         f'{default_base_url}/{topic}',
-        headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+        headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
         auth=None,
         auth=None,
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
@@ -132,7 +132,7 @@ def test_ping_monitor_with_password_but_no_username_warns():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -142,7 +142,7 @@ def test_ping_monitor_with_username_but_no_password_warns():
     hook_config = {'topic': topic, 'username': 'testuser'}
     hook_config = {'topic': topic, 'username': 'testuser'}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'{default_base_url}/{topic}',
         f'{default_base_url}/{topic}',
-        headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+        headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
         auth=None,
         auth=None,
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
@@ -151,7 +151,7 @@ def test_ping_monitor_with_username_but_no_password_warns():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -165,7 +165,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -179,7 +179,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_finish():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -189,7 +189,7 @@ def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail():
     hook_config = {'topic': topic, 'server': custom_base_url}
     hook_config = {'topic': topic, 'server': custom_base_url}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'{custom_base_url}/{topic}',
         f'{custom_base_url}/{topic}',
-        headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+        headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
         auth=None,
         auth=None,
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
@@ -197,7 +197,7 @@ def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -211,7 +211,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_fail_dry_run():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=True,
         dry_run=True,
     )
     )
@@ -227,7 +227,7 @@ def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -237,7 +237,7 @@ def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start():
     hook_config = {'topic': topic, 'states': ['start', 'fail']}
     hook_config = {'topic': topic, 'states': ['start', 'fail']}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'{default_base_url}/{topic}',
         f'{default_base_url}/{topic}',
-        headers=return_default_message_headers(borgmatic.hooks.monitor.State.START),
+        headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.START),
         auth=None,
         auth=None,
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
@@ -245,7 +245,7 @@ def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -255,7 +255,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
     hook_config = {'topic': topic}
     hook_config = {'topic': topic}
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'{default_base_url}/{topic}',
         f'{default_base_url}/{topic}',
-        headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+        headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
         auth=None,
         auth=None,
     ).and_raise(module.requests.exceptions.ConnectionError)
     ).and_raise(module.requests.exceptions.ConnectionError)
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
@@ -264,7 +264,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -278,7 +278,7 @@ def test_ping_monitor_with_other_error_logs_warning():
     )
     )
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'{default_base_url}/{topic}',
         f'{default_base_url}/{topic}',
-        headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+        headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
         auth=None,
         auth=None,
     ).and_return(response)
     ).and_return(response)
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
@@ -287,7 +287,7 @@ def test_ping_monitor_with_other_error_logs_warning():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )

+ 1 - 1
tests/unit/hooks/test_pagerduty.py → tests/unit/hooks/monitoring/test_pagerduty.py

@@ -1,6 +1,6 @@
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-from borgmatic.hooks import pagerduty as module
+from borgmatic.hooks.monitoring import pagerduty as module
 
 
 
 
 def test_ping_monitor_ignores_start_state():
 def test_ping_monitor_ignores_start_state():

+ 16 - 16
tests/unit/hooks/test_pushover.py → tests/unit/hooks/monitoring/test_pushover.py

@@ -1,8 +1,8 @@
 import pytest
 import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-import borgmatic.hooks.monitor
-from borgmatic.hooks import pushover as module
+import borgmatic.hooks.monitoring.monitor
+from borgmatic.hooks.monitoring import pushover as module
 
 
 
 
 def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover():
 def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover():
@@ -26,7 +26,7 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -45,7 +45,7 @@ def test_ping_monitor_config_with_minimum_config_start_state_backup_not_send_to_
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -78,7 +78,7 @@ def test_ping_monitor_start_state_backup_default_message_successfully_send_to_pu
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -111,7 +111,7 @@ def test_ping_monitor_start_state_backup_custom_message_successfully_send_to_pus
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -146,7 +146,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -181,7 +181,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -216,7 +216,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -244,7 +244,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_high_decl
             hook_config,
             hook_config,
             {},
             {},
             'config.yaml',
             'config.yaml',
-            borgmatic.hooks.monitor.State.START,
+            borgmatic.hooks.monitoring.monitor.State.START,
             monitoring_log_level=1,
             monitoring_log_level=1,
             dry_run=False,
             dry_run=False,
         )
         )
@@ -307,7 +307,7 @@ def test_ping_monitor_start_state_backup_based_on_documentation_advanced_example
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -375,7 +375,7 @@ def test_ping_monitor_fail_state_backup_based_on_documentation_advanced_example_
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -440,7 +440,7 @@ def test_ping_monitor_finish_state_backup_based_on_documentation_advanced_exampl
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -459,7 +459,7 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=True,
         dry_run=True,
     )
     )
@@ -480,7 +480,7 @@ def test_ping_monitor_config_incorrect_state_exit_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=True,
         dry_run=True,
     )
     )
@@ -516,7 +516,7 @@ def test_ping_monitor_push_post_error_exits_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )

+ 12 - 12
tests/unit/hooks/test_uptimekuma.py → tests/unit/hooks/monitoring/test_uptimekuma.py

@@ -1,7 +1,7 @@
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-import borgmatic.hooks.monitor
-from borgmatic.hooks import uptimekuma as module
+import borgmatic.hooks.monitoring.monitor
+from borgmatic.hooks.monitoring import uptime_kuma as module
 
 
 DEFAULT_PUSH_URL = 'https://example.uptime.kuma/api/push/abcd1234'
 DEFAULT_PUSH_URL = 'https://example.uptime.kuma/api/push/abcd1234'
 CUSTOM_PUSH_URL = 'https://uptime.example.com/api/push/efgh5678'
 CUSTOM_PUSH_URL = 'https://uptime.example.com/api/push/efgh5678'
@@ -17,7 +17,7 @@ def test_ping_monitor_hits_default_uptimekuma_on_fail():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -33,7 +33,7 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -49,7 +49,7 @@ def test_ping_monitor_custom_uptimekuma_on_start():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -65,7 +65,7 @@ def test_ping_monitor_custom_uptimekuma_on_finish():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -79,7 +79,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=True,
         dry_run=True,
     )
     )
@@ -93,7 +93,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=True,
         dry_run=True,
     )
     )
@@ -107,7 +107,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=True,
         dry_run=True,
     )
     )
@@ -124,7 +124,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -145,7 +145,7 @@ def test_ping_monitor_with_other_error_logs_warning():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -159,7 +159,7 @@ def test_ping_monitor_with_invalid_run_state():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.LOG,
+        borgmatic.hooks.monitoring.monitor.State.LOG,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=True,
         dry_run=True,
     )
     )

+ 21 - 21
tests/unit/hooks/test_zabbix.py → tests/unit/hooks/monitoring/test_zabbix.py

@@ -1,7 +1,7 @@
 from flexmock import flexmock
 from flexmock import flexmock
 
 
-import borgmatic.hooks.monitor
-from borgmatic.hooks import zabbix as module
+import borgmatic.hooks.monitoring.monitor
+from borgmatic.hooks.monitoring import zabbix as module
 
 
 SERVER = 'https://zabbix.com/zabbix/api_jsonrpc.php'
 SERVER = 'https://zabbix.com/zabbix/api_jsonrpc.php'
 ITEMID = 55105
 ITEMID = 55105
@@ -65,7 +65,7 @@ def test_ping_monitor_with_non_matching_state_exits_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -82,7 +82,7 @@ def test_ping_monitor_config_with_api_key_only_exit_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -99,7 +99,7 @@ def test_ping_monitor_config_with_host_only_exit_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -116,7 +116,7 @@ def test_ping_monitor_config_with_key_only_exit_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -133,7 +133,7 @@ def test_ping_monitor_config_with_server_only_exit_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -149,7 +149,7 @@ def test_ping_monitor_config_user_password_no_zabbix_data_exit_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -165,7 +165,7 @@ def test_ping_monitor_config_api_key_no_zabbix_data_exit_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -182,7 +182,7 @@ def test_ping_monitor_config_itemid_no_auth_data_exit_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -199,7 +199,7 @@ def test_ping_monitor_config_host_and_key_no_auth_data_exit_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -220,7 +220,7 @@ def test_ping_monitor_config_host_and_key_with_api_key_auth_data_successful():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -235,7 +235,7 @@ def test_ping_monitor_config_host_and_missing_key_exits_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -250,7 +250,7 @@ def test_ping_monitor_config_key_and_missing_host_exits_early():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -290,7 +290,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_succe
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -329,7 +329,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_and_a
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -350,7 +350,7 @@ def test_ping_monitor_config_host_and_key_with_username_and_missing_password_exi
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -371,7 +371,7 @@ def test_ping_monitor_config_host_and_key_with_passing_and_missing_username_exit
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -392,7 +392,7 @@ def test_ping_monitor_config_itemid_with_api_key_auth_data_successful():
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -426,7 +426,7 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_successful(
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )
@@ -462,7 +462,7 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_and_push_po
         hook_config,
         hook_config,
         {},
         {},
         'config.yaml',
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         monitoring_log_level=1,
         dry_run=False,
         dry_run=False,
     )
     )

+ 145 - 15
tests/unit/hooks/test_dispatch.py

@@ -17,7 +17,15 @@ def test_call_hook_invokes_module_function_with_arguments_and_returns_value():
     config = {'super_hook': flexmock(), 'other_hook': flexmock()}
     config = {'super_hook': flexmock(), 'other_hook': flexmock()}
     expected_return_value = flexmock()
     expected_return_value = flexmock()
     test_module = sys.modules[__name__]
     test_module = sys.modules[__name__]
-    flexmock(module).HOOK_NAME_TO_MODULE = {'super_hook': test_module}
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.data_source
+    ).and_return(['other_hook'])
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['super_hook', 'third_hook'])
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring.super_hook'
+    ).and_return(test_module)
     flexmock(test_module).should_receive('hook_function').with_args(
     flexmock(test_module).should_receive('hook_function').with_args(
         config['super_hook'], config, 'prefix', 55, value=66
         config['super_hook'], config, 'prefix', 55, value=66
     ).and_return(expected_return_value).once()
     ).and_return(expected_return_value).once()
@@ -27,11 +35,65 @@ def test_call_hook_invokes_module_function_with_arguments_and_returns_value():
     assert return_value == expected_return_value
     assert return_value == expected_return_value
 
 
 
 
+def test_call_hook_probes_config_with_databases_suffix():
+    config = {'super_hook_databases': flexmock(), 'other_hook': flexmock()}
+    expected_return_value = flexmock()
+    test_module = sys.modules[__name__]
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.data_source
+    ).and_return(['other_hook'])
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['super_hook', 'third_hook'])
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring.super_hook'
+    ).and_return(test_module)
+    flexmock(test_module).should_receive('hook_function').with_args(
+        config['super_hook_databases'], config, 'prefix', 55, value=66
+    ).and_return(expected_return_value).once()
+
+    return_value = module.call_hook('hook_function', config, 'prefix', 'super_hook', 55, value=66)
+
+    assert return_value == expected_return_value
+
+
+def test_call_hook_strips_databases_suffix_from_hook_name():
+    config = {'super_hook_databases': flexmock(), 'other_hook_databases': flexmock()}
+    expected_return_value = flexmock()
+    test_module = sys.modules[__name__]
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.data_source
+    ).and_return(['other_hook'])
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['super_hook', 'third_hook'])
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring.super_hook'
+    ).and_return(test_module)
+    flexmock(test_module).should_receive('hook_function').with_args(
+        config['super_hook_databases'], config, 'prefix', 55, value=66
+    ).and_return(expected_return_value).once()
+
+    return_value = module.call_hook(
+        'hook_function', config, 'prefix', 'super_hook_databases', 55, value=66
+    )
+
+    assert return_value == expected_return_value
+
+
 def test_call_hook_without_hook_config_invokes_module_function_with_arguments_and_returns_value():
 def test_call_hook_without_hook_config_invokes_module_function_with_arguments_and_returns_value():
     config = {'other_hook': flexmock()}
     config = {'other_hook': flexmock()}
     expected_return_value = flexmock()
     expected_return_value = flexmock()
     test_module = sys.modules[__name__]
     test_module = sys.modules[__name__]
-    flexmock(module).HOOK_NAME_TO_MODULE = {'super_hook': test_module}
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.data_source
+    ).and_return(['other_hook'])
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['super_hook', 'third_hook'])
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring.super_hook'
+    ).and_return(test_module)
     flexmock(test_module).should_receive('hook_function').with_args(
     flexmock(test_module).should_receive('hook_function').with_args(
         {}, config, 'prefix', 55, value=66
         {}, config, 'prefix', 55, value=66
     ).and_return(expected_return_value).once()
     ).and_return(expected_return_value).once()
@@ -44,35 +106,69 @@ def test_call_hook_without_hook_config_invokes_module_function_with_arguments_an
 def test_call_hook_without_corresponding_module_raises():
 def test_call_hook_without_corresponding_module_raises():
     config = {'super_hook': flexmock(), 'other_hook': flexmock()}
     config = {'super_hook': flexmock(), 'other_hook': flexmock()}
     test_module = sys.modules[__name__]
     test_module = sys.modules[__name__]
-    flexmock(module).HOOK_NAME_TO_MODULE = {'other_hook': test_module}
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.data_source
+    ).and_return(['other_hook'])
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['some_hook'])
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring.super_hook'
+    ).and_return(test_module)
     flexmock(test_module).should_receive('hook_function').never()
     flexmock(test_module).should_receive('hook_function').never()
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
         module.call_hook('hook_function', config, 'prefix', 'super_hook', 55, value=66)
         module.call_hook('hook_function', config, 'prefix', 'super_hook', 55, value=66)
 
 
 
 
+def test_call_hook_skips_non_hook_modules():
+    config = {'not_a_hook': flexmock(), 'other_hook': flexmock()}
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.data_source
+    ).and_return(['other_hook'])
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['not_a_hook', 'third_hook'])
+    not_a_hook_module = flexmock(IS_A_HOOK=False)
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring.not_a_hook'
+    ).and_return(not_a_hook_module)
+
+    return_value = module.call_hook('hook_function', config, 'prefix', 'not_a_hook', 55, value=66)
+
+    assert return_value is None
+
+
 def test_call_hooks_calls_each_hook_and_collects_return_values():
 def test_call_hooks_calls_each_hook_and_collects_return_values():
     config = {'super_hook': flexmock(), 'other_hook': flexmock()}
     config = {'super_hook': flexmock(), 'other_hook': flexmock()}
     expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
     expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring'
+    ).and_return(module.borgmatic.hooks.monitoring)
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['super_hook', 'other_hook'])
     flexmock(module).should_receive('call_hook').and_return(
     flexmock(module).should_receive('call_hook').and_return(
         expected_return_values['super_hook']
         expected_return_values['super_hook']
     ).and_return(expected_return_values['other_hook'])
     ).and_return(expected_return_values['other_hook'])
 
 
-    return_values = module.call_hooks(
-        'do_stuff', config, 'prefix', ('super_hook', 'other_hook'), 55
-    )
+    return_values = module.call_hooks('do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55)
 
 
     assert return_values == expected_return_values
     assert return_values == expected_return_values
 
 
 
 
-def test_call_hooks_calls_skips_return_values_for_missing_hooks():
+def test_call_hooks_calls_skips_return_values_for_unconfigured_hooks():
     config = {'super_hook': flexmock()}
     config = {'super_hook': flexmock()}
     expected_return_values = {'super_hook': flexmock()}
     expected_return_values = {'super_hook': flexmock()}
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring'
+    ).and_return(module.borgmatic.hooks.monitoring)
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['super_hook', 'other_hook'])
     flexmock(module).should_receive('call_hook').and_return(expected_return_values['super_hook'])
     flexmock(module).should_receive('call_hook').and_return(expected_return_values['super_hook'])
 
 
-    return_values = module.call_hooks(
-        'do_stuff', config, 'prefix', ('super_hook', 'other_hook'), 55
-    )
+    return_values = module.call_hooks('do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55)
 
 
     assert return_values == expected_return_values
     assert return_values == expected_return_values
 
 
@@ -80,13 +176,35 @@ def test_call_hooks_calls_skips_return_values_for_missing_hooks():
 def test_call_hooks_calls_treats_null_hook_as_optionless():
 def test_call_hooks_calls_treats_null_hook_as_optionless():
     config = {'super_hook': flexmock(), 'other_hook': None}
     config = {'super_hook': flexmock(), 'other_hook': None}
     expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
     expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring'
+    ).and_return(module.borgmatic.hooks.monitoring)
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['super_hook', 'other_hook'])
     flexmock(module).should_receive('call_hook').and_return(
     flexmock(module).should_receive('call_hook').and_return(
         expected_return_values['super_hook']
         expected_return_values['super_hook']
     ).and_return(expected_return_values['other_hook'])
     ).and_return(expected_return_values['other_hook'])
 
 
-    return_values = module.call_hooks(
-        'do_stuff', config, 'prefix', ('super_hook', 'other_hook'), 55
-    )
+    return_values = module.call_hooks('do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55)
+
+    assert return_values == expected_return_values
+
+
+def test_call_hooks_calls_looks_up_databases_suffix_in_config():
+    config = {'super_hook_databases': flexmock(), 'other_hook': flexmock()}
+    expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring'
+    ).and_return(module.borgmatic.hooks.monitoring)
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['super_hook', 'other_hook'])
+    flexmock(module).should_receive('call_hook').and_return(
+        expected_return_values['super_hook']
+    ).and_return(expected_return_values['other_hook'])
+
+    return_values = module.call_hooks('do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55)
 
 
     assert return_values == expected_return_values
     assert return_values == expected_return_values
 
 
@@ -94,12 +212,18 @@ def test_call_hooks_calls_treats_null_hook_as_optionless():
 def test_call_hooks_even_if_unconfigured_calls_each_hook_and_collects_return_values():
 def test_call_hooks_even_if_unconfigured_calls_each_hook_and_collects_return_values():
     config = {'super_hook': flexmock(), 'other_hook': flexmock()}
     config = {'super_hook': flexmock(), 'other_hook': flexmock()}
     expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
     expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring'
+    ).and_return(module.borgmatic.hooks.monitoring)
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['super_hook', 'other_hook'])
     flexmock(module).should_receive('call_hook').and_return(
     flexmock(module).should_receive('call_hook').and_return(
         expected_return_values['super_hook']
         expected_return_values['super_hook']
     ).and_return(expected_return_values['other_hook'])
     ).and_return(expected_return_values['other_hook'])
 
 
     return_values = module.call_hooks_even_if_unconfigured(
     return_values = module.call_hooks_even_if_unconfigured(
-        'do_stuff', config, 'prefix', ('super_hook', 'other_hook'), 55
+        'do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55
     )
     )
 
 
     assert return_values == expected_return_values
     assert return_values == expected_return_values
@@ -108,12 +232,18 @@ def test_call_hooks_even_if_unconfigured_calls_each_hook_and_collects_return_val
 def test_call_hooks_even_if_unconfigured_calls_each_hook_configured_or_not_and_collects_return_values():
 def test_call_hooks_even_if_unconfigured_calls_each_hook_configured_or_not_and_collects_return_values():
     config = {'other_hook': flexmock()}
     config = {'other_hook': flexmock()}
     expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
     expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
+    flexmock(module.importlib).should_receive('import_module').with_args(
+        'borgmatic.hooks.monitoring'
+    ).and_return(module.borgmatic.hooks.monitoring)
+    flexmock(module).should_receive('get_submodule_names').with_args(
+        module.borgmatic.hooks.monitoring
+    ).and_return(['super_hook', 'other_hook'])
     flexmock(module).should_receive('call_hook').and_return(
     flexmock(module).should_receive('call_hook').and_return(
         expected_return_values['super_hook']
         expected_return_values['super_hook']
     ).and_return(expected_return_values['other_hook'])
     ).and_return(expected_return_values['other_hook'])
 
 
     return_values = module.call_hooks_even_if_unconfigured(
     return_values = module.call_hooks_even_if_unconfigured(
-        'do_stuff', config, 'prefix', ('super_hook', 'other_hook'), 55
+        'do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55
     )
     )
 
 
     assert return_values == expected_return_values
     assert return_values == expected_return_values