2
0
Эх сурвалжийг харах

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

Dan Helfman 6 сар өмнө
parent
commit
6b2f2b2ac4
58 өөрчлөгдсөн 508 нэмэгдсэн , 349 устгасан
  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
  * #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
  * #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',
             config,
             repository['path'],
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
         ).values()
     )
 

+ 3 - 4
borgmatic/actions/create.py

@@ -10,7 +10,6 @@ import borgmatic.config.paths
 import borgmatic.config.validate
 import borgmatic.hooks.command
 import borgmatic.hooks.dispatch
-import borgmatic.hooks.dump
 
 logger = logging.getLogger(__name__)
 
@@ -186,7 +185,7 @@ def run_create(
             'remove_data_source_dumps',
             config,
             repository['path'],
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
             borgmatic_runtime_directory,
             global_arguments.dry_run,
         )
@@ -195,7 +194,7 @@ def run_create(
             'dump_data_sources',
             config,
             repository['path'],
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
             config_paths,
             borgmatic_runtime_directory,
             source_directories,
@@ -232,7 +231,7 @@ def run_create(
             'remove_data_source_dumps',
             config,
             config_filename,
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
             borgmatic_runtime_directory,
             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.config.paths
 import borgmatic.config.validate
+import borgmatic.hooks.data_source.dump
 import borgmatic.hooks.dispatch
-import borgmatic.hooks.dump
 
 logger = logging.getLogger(__name__)
 
@@ -44,7 +44,8 @@ def get_configured_data_source(
         hooks_to_search = {
             hook_name: value
             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:
         try:
@@ -123,10 +124,10 @@ def restore_single_data_source(
         'make_data_source_dump_patterns',
         config,
         repository['path'],
-        borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+        borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
         borgmatic_runtime_directory,
         data_source['name'],
-    )[hook_name]
+    )[hook_name.split('_databases')[0]]
 
     destination_path = (
         tempfile.mkdtemp(dir=borgmatic_runtime_directory)
@@ -141,7 +142,11 @@ def restore_single_data_source(
             dry_run=global_arguments.dry_run,
             repository=repository['path'],
             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,
             local_borg_version=local_borg_version,
             global_arguments=global_arguments,
@@ -162,11 +167,11 @@ def restore_single_data_source(
             shutil.rmtree(destination_path, ignore_errors=True)
 
     # 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',
         config=config,
         log_prefix=repository['path'],
-        hook_names=[hook_name],
+        hook_name=hook_name,
         data_source=data_source,
         dry_run=global_arguments.dry_run,
         extract_process=extract_process,
@@ -206,7 +211,9 @@ def collect_archive_data_source_names(
         global_arguments,
         list_paths=[
             '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 (
                 'borgmatic',
                 borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
@@ -354,7 +361,7 @@ def run_restore(
             'remove_data_source_dumps',
             config,
             repository['path'],
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
             borgmatic_runtime_directory,
             global_arguments.dry_run,
         )
@@ -451,7 +458,7 @@ def run_restore(
             'remove_data_source_dumps',
             config,
             repository['path'],
-            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
             borgmatic_runtime_directory,
             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.commands.arguments import parse_arguments
 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.signals import configure_signals
 from borgmatic.verbosity import verbosity_to_log_level
@@ -103,7 +104,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                 'initialize_monitor',
                 config,
                 config_filename,
-                monitor.MONITOR_HOOK_NAMES,
+                dispatch.Hook_type.MONITORING,
                 monitoring_log_level,
                 global_arguments.dry_run,
             )
@@ -112,7 +113,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                 'ping_monitor',
                 config,
                 config_filename,
-                monitor.MONITOR_HOOK_NAMES,
+                dispatch.Hook_type.MONITORING,
                 monitor.State.START,
                 monitoring_log_level,
                 global_arguments.dry_run,
@@ -188,7 +189,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                 'ping_monitor',
                 config,
                 config_filename,
-                monitor.MONITOR_HOOK_NAMES,
+                dispatch.Hook_type.MONITORING,
                 monitor.State.LOG,
                 monitoring_log_level,
                 global_arguments.dry_run,
@@ -205,7 +206,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                     'ping_monitor',
                     config,
                     config_filename,
-                    monitor.MONITOR_HOOK_NAMES,
+                    dispatch.Hook_type.MONITORING,
                     monitor.State.FINISH,
                     monitoring_log_level,
                     global_arguments.dry_run,
@@ -214,7 +215,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                     'destroy_monitor',
                     config,
                     config_filename,
-                    monitor.MONITOR_HOOK_NAMES,
+                    dispatch.Hook_type.MONITORING,
                     monitoring_log_level,
                     global_arguments.dry_run,
                 )
@@ -241,7 +242,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                 'ping_monitor',
                 config,
                 config_filename,
-                monitor.MONITOR_HOOK_NAMES,
+                dispatch.Hook_type.MONITORING,
                 monitor.State.FAIL,
                 monitoring_log_level,
                 global_arguments.dry_run,
@@ -250,7 +251,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
                 'destroy_monitor',
                 config,
                 config_filename,
-                monitor.MONITOR_HOOK_NAMES,
+                dispatch.Hook_type.MONITORING,
                 monitoring_log_level,
                 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__)
 
-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):

+ 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_with_processes,
 )
-from borgmatic.hooks import dump
+from borgmatic.hooks.data_source import dump
 
 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
 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__)
 

+ 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_with_processes,
 )
-from borgmatic.hooks import dump
+from borgmatic.hooks.data_source import dump
 
 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_with_processes,
 )
-from borgmatic.hooks import dump
+from borgmatic.hooks.data_source import dump
 
 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
 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__)
 

+ 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 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__)
 
-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):
     '''
     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
-    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 AttributeError if the function name is not found in the module.
     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}')
 
     logger.debug(f'{log_prefix}: Calling {hook_name} hook function {function_name}')
+
     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
-    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
     and omit it from the return values.
@@ -80,22 +75,26 @@ def call_hooks(function_name, config, log_prefix, hook_names, *args, **kwargs):
     '''
     return {
         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
-    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 anything else that a called function raises. An error stops calls to subsequent functions.
     '''
     return {
         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 operator
 
-import borgmatic.hooks.logs
-import borgmatic.hooks.monitor
+import borgmatic.hooks.monitoring.logs
+import borgmatic.hooks.monitoring.monitor
 
 logger = logging.getLogger(__name__)
 
@@ -22,12 +22,12 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
 
     logs_size_limit = max(
         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,
     )
 
-    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
         )
     )
@@ -82,11 +82,13 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
     body = state_config.get('body')
 
     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:
             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
     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
 
-from borgmatic.hooks import monitor
+from borgmatic.hooks.monitoring import monitor
 
 logger = logging.getLogger(__name__)
 

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

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

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

@@ -3,8 +3,8 @@ import re
 
 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__)
 
@@ -30,12 +30,12 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
 
     ping_body_limit = max(
         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,
     )
 
-    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
         )
     )
@@ -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}')
 
     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:
         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
     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
 
+IS_A_HOOK = False
 PAYLOAD_TRUNCATION_INDICATOR = '...\n'
 
 

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

@@ -6,7 +6,7 @@ import time
 
 import requests
 
-from borgmatic.hooks import monitor
+from borgmatic.hooks.monitoring import monitor
 
 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
 
-from borgmatic.hooks import monitor
+from borgmatic.hooks.monitoring import monitor
 
 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.).
    * [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. 
-   * [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.
  * [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.

+ 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 borgmatic.hooks import apprise as module
+from borgmatic.hooks.monitoring import apprise as module
 
 
 def test_destroy_monitor_removes_apprise_handler():
     logger = logging.getLogger()
     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
         )
     )

+ 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 borgmatic.hooks import healthchecks as module
+from borgmatic.hooks.monitoring import healthchecks as module
 
 
 def test_destroy_monitor_removes_healthchecks_handler():
     logger = logging.getLogger()
     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
         )
     )

+ 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 borgmatic.hooks import loki as module
+from borgmatic.hooks.monitoring import loki as module
 
 
 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
     ).and_return({'postgresql': flexmock()})
     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'
     ).and_return(flexmock())
     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()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').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',
         config=object,
         log_prefix=object,
-        hook_names=object,
+        hook_name=object,
         data_source=object,
         dry_run=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(
         '/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'
     ).and_return(flexmock())
     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()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').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',
         config=object,
         log_prefix=object,
-        hook_names=object,
+        hook_name=object,
         data_source=object,
         dry_run=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(
         '/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'
     ).and_return(flexmock())
     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()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').never()
     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',
         config=object,
         log_prefix=object,
-        hook_names=object,
+        hook_name=object,
         data_source=object,
         dry_run=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
     ).and_return({'postgresql': flexmock()})
     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'
     ).and_return(flexmock())
     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()
     flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').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',
         config=object,
         log_prefix=object,
-        hook_names=object,
+        hook_name=object,
         data_source=object,
         dry_run=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(
         'get_borgmatic_source_directory'
     ).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(
         [
             '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(
         'get_borgmatic_source_directory'
     ).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(
         [
             '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(
         'get_borgmatic_source_directory'
     ).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(
         [
             '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(
         'get_borgmatic_source_directory'
     ).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(
         [
             '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 borgmatic.hooks import bootstrap as module
+from borgmatic.hooks.data_source import bootstrap as module
 
 
 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
 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():

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

@@ -3,7 +3,7 @@ import logging
 import pytest
 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():

+ 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 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():

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

@@ -3,7 +3,7 @@ import logging
 import pytest
 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():

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

@@ -3,7 +3,7 @@ import logging
 import pytest
 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():

+ 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 borgmatic.hooks import sqlite as module
+from borgmatic.hooks.data_source import sqlite as module
 
 
 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
 
 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():

+ 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 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'
 
@@ -18,7 +18,7 @@ def mock_apprise():
 
 
 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(
         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():
     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.DEFAULT_LOGS_SIZE_LIMIT_BYTES - truncation_indicator_length,
         1,
     ).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(
         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():
     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.DEFAULT_LOGS_SIZE_LIMIT_BYTES - truncation_indicator_length,
         1,
     ).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(
         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():
-    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'
     ).and_return('loggy log')
     mock_apprise().should_receive('notify').never()
@@ -76,30 +80,32 @@ def test_ping_monitor_respects_dry_run():
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=True,
     )
 
 
 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()
 
     module.ping_monitor(
         {'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': []},
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=True,
     )
 
 
 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'
     ).and_return('')
     mock_apprise().should_receive('notify').with_args(
@@ -109,7 +115,7 @@ def test_ping_monitor_notifies_fail_by_default():
         notify_type=NotifyType.FAILURE,
     ).once()
 
-    for state in borgmatic.hooks.monitor.State:
+    for state in borgmatic.hooks.monitoring.monitor.State:
         module.ping_monitor(
             {'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():
-    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'
     ).and_return('loggy log')
     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,
     ).once()
 
-    for state in borgmatic.hooks.monitor.State:
+    for state in borgmatic.hooks.monitoring.monitor.State:
         module.ping_monitor(
             {'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():
-    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'
     ).and_return('')
     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']},
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
         monitoring_log_level=1,
         dry_run=False,
     )
 
 
 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(
         title='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']},
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
 
 
 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'
     ).and_return('')
     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']},
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
 
 
 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'
     ).and_return('')
     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']},
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.LOG,
+        borgmatic.hooks.monitoring.monitor.State.LOG,
         monitoring_log_level=1,
         dry_run=False,
     )
 
 
 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'
     ).and_return('')
     mock_apprise().should_receive('notify').with_args(
@@ -249,15 +257,15 @@ def test_ping_monitor_passes_through_custom_message_title():
         },
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
 
 
 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'
     ).and_return('')
     mock_apprise().should_receive('notify').with_args(
@@ -275,15 +283,15 @@ def test_ping_monitor_passes_through_custom_message_body():
         },
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
 
 
 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'
     ).and_return('loggy log')
     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',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
 
 
 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'
     ).and_return('')
     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',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
 
 
 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()
 
     module.ping_monitor(
         {'services': []},
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
 
 
 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'
     ).and_return('')
     mock_apprise().should_receive('notify').and_return(False)
     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(
             {'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():
-    flexmock(module.borgmatic.hooks.logs).should_receive('remove_handler')
+    flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('remove_handler')
 
     module.destroy_monitor(
         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 borgmatic.hooks import cronhub as module
+from borgmatic.hooks.monitoring import cronhub as module
 
 
 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 borgmatic.hooks import cronitor as module
+from borgmatic.hooks.monitoring import cronitor as module
 
 
 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 borgmatic.hooks import healthchecks as module
+from borgmatic.hooks.monitoring import healthchecks as module
 
 
 def mock_logger():
@@ -15,9 +15,11 @@ def test_initialize_monitor_creates_log_handler_with_ping_body_limit():
     monitoring_log_level = 1
 
     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,
-        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,
     ).once()
 
@@ -30,10 +32,12 @@ def test_initialize_monitor_creates_log_handler_with_default_ping_body_limit():
     monitoring_log_level = 1
 
     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.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,
     ).once()
 
@@ -45,9 +49,9 @@ def test_initialize_monitor_creates_log_handler_with_zero_ping_body_limit():
     monitoring_log_level = 1
 
     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(
         {'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():
     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(
         {'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():
     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(
         {'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():
-    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'}
     flexmock(module.requests).should_receive('post').with_args(
         '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():
     hook_config = {'ping_url': 'https://example.com'}
     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'
     ).and_return(payload)
     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():
     hook_config = {'ping_url': 'https://example.com'}
     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'
     ).and_return(payload)
     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():
     hook_config = {'ping_url': 'https://example.com'}
     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'
     ).and_return(payload)
     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():
     hook_config = {'ping_url': 'abcd-efgh-ijkl-mnop'}
     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'
     ).and_return(payload)
     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():
     hook_config = {'ping_url': 'https://example.com', 'verify_tls': False}
     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'
     ).and_return(payload)
     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():
     hook_config = {'ping_url': 'https://example.com', 'verify_tls': True}
     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'
     ).and_return(payload)
     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():
-    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'}
     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():
-    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']}
     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():
-    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']}
     flexmock(module.requests).should_receive('post').with_args(
         '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():
-    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}
     flexmock(module.requests).should_receive('post').with_args(
         '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():
-    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}
     flexmock(module.requests).should_receive('post').with_args(
         '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():
-    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'}
     flexmock(module.requests).should_receive('post').with_args(
         '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():
-    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'}
     response = flexmock(ok=False)
     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
 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():

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

@@ -3,7 +3,7 @@ import json
 import requests
 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():

+ 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
 
-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'
 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}
     flexmock(module.requests).should_receive('post').with_args(
         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,
     ).and_return(flexmock(ok=True)).once()
 
@@ -46,7 +46,7 @@ def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         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(
         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'),
     ).and_return(flexmock(ok=True)).once()
 
@@ -67,7 +67,7 @@ def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         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(
         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'),
     ).and_return(flexmock(ok=True)).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,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         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(
         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'),
     ).and_return(flexmock(ok=True)).once()
 
@@ -113,7 +113,7 @@ def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -123,7 +123,7 @@ def test_ping_monitor_with_password_but_no_username_warns():
     hook_config = {'topic': topic, 'password': 'fakepassword'}
     flexmock(module.requests).should_receive('post').with_args(
         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,
     ).and_return(flexmock(ok=True)).once()
     flexmock(module.logger).should_receive('warning').once()
@@ -132,7 +132,7 @@ def test_ping_monitor_with_password_but_no_username_warns():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -142,7 +142,7 @@ def test_ping_monitor_with_username_but_no_password_warns():
     hook_config = {'topic': topic, 'username': 'testuser'}
     flexmock(module.requests).should_receive('post').with_args(
         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,
     ).and_return(flexmock(ok=True)).once()
     flexmock(module.logger).should_receive('warning').once()
@@ -151,7 +151,7 @@ def test_ping_monitor_with_username_but_no_password_warns():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -165,7 +165,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -179,7 +179,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_finish():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
         monitoring_log_level=1,
         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}
     flexmock(module.requests).should_receive('post').with_args(
         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,
     ).and_return(flexmock(ok=True)).once()
 
@@ -197,7 +197,7 @@ def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -211,7 +211,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_fail_dry_run():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=True,
     )
@@ -227,7 +227,7 @@ def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         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']}
     flexmock(module.requests).should_receive('post').with_args(
         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,
     ).and_return(flexmock(ok=True)).once()
 
@@ -245,7 +245,7 @@ def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -255,7 +255,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
     hook_config = {'topic': topic}
     flexmock(module.requests).should_receive('post').with_args(
         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,
     ).and_raise(module.requests.exceptions.ConnectionError)
     flexmock(module.logger).should_receive('warning').once()
@@ -264,7 +264,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -278,7 +278,7 @@ def test_ping_monitor_with_other_error_logs_warning():
     )
     flexmock(module.requests).should_receive('post').with_args(
         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,
     ).and_return(response)
     flexmock(module.logger).should_receive('warning').once()
@@ -287,7 +287,7 @@ def test_ping_monitor_with_other_error_logs_warning():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         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 borgmatic.hooks import pagerduty as module
+from borgmatic.hooks.monitoring import pagerduty as module
 
 
 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
 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():
@@ -26,7 +26,7 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -45,7 +45,7 @@ def test_ping_monitor_config_with_minimum_config_start_state_backup_not_send_to_
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -78,7 +78,7 @@ def test_ping_monitor_start_state_backup_default_message_successfully_send_to_pu
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -111,7 +111,7 @@ def test_ping_monitor_start_state_backup_custom_message_successfully_send_to_pus
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -146,7 +146,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -181,7 +181,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -216,7 +216,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -244,7 +244,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_high_decl
             hook_config,
             {},
             'config.yaml',
-            borgmatic.hooks.monitor.State.START,
+            borgmatic.hooks.monitoring.monitor.State.START,
             monitoring_log_level=1,
             dry_run=False,
         )
@@ -307,7 +307,7 @@ def test_ping_monitor_start_state_backup_based_on_documentation_advanced_example
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -375,7 +375,7 @@ def test_ping_monitor_fail_state_backup_based_on_documentation_advanced_example_
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -440,7 +440,7 @@ def test_ping_monitor_finish_state_backup_based_on_documentation_advanced_exampl
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -459,7 +459,7 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=True,
     )
@@ -480,7 +480,7 @@ def test_ping_monitor_config_incorrect_state_exit_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=True,
     )
@@ -516,7 +516,7 @@ def test_ping_monitor_push_post_error_exits_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         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
 
-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'
 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,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -33,7 +33,7 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -49,7 +49,7 @@ def test_ping_monitor_custom_uptimekuma_on_start():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -65,7 +65,7 @@ def test_ping_monitor_custom_uptimekuma_on_finish():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -79,7 +79,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=True,
     )
@@ -93,7 +93,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=True,
     )
@@ -107,7 +107,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FINISH,
+        borgmatic.hooks.monitoring.monitor.State.FINISH,
         monitoring_log_level=1,
         dry_run=True,
     )
@@ -124,7 +124,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -145,7 +145,7 @@ def test_ping_monitor_with_other_error_logs_warning():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -159,7 +159,7 @@ def test_ping_monitor_with_invalid_run_state():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.LOG,
+        borgmatic.hooks.monitoring.monitor.State.LOG,
         monitoring_log_level=1,
         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
 
-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'
 ITEMID = 55105
@@ -65,7 +65,7 @@ def test_ping_monitor_with_non_matching_state_exits_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.START,
+        borgmatic.hooks.monitoring.monitor.State.START,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -82,7 +82,7 @@ def test_ping_monitor_config_with_api_key_only_exit_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -99,7 +99,7 @@ def test_ping_monitor_config_with_host_only_exit_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -116,7 +116,7 @@ def test_ping_monitor_config_with_key_only_exit_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -133,7 +133,7 @@ def test_ping_monitor_config_with_server_only_exit_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -149,7 +149,7 @@ def test_ping_monitor_config_user_password_no_zabbix_data_exit_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -165,7 +165,7 @@ def test_ping_monitor_config_api_key_no_zabbix_data_exit_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -182,7 +182,7 @@ def test_ping_monitor_config_itemid_no_auth_data_exit_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -199,7 +199,7 @@ def test_ping_monitor_config_host_and_key_no_auth_data_exit_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -220,7 +220,7 @@ def test_ping_monitor_config_host_and_key_with_api_key_auth_data_successful():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -235,7 +235,7 @@ def test_ping_monitor_config_host_and_missing_key_exits_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -250,7 +250,7 @@ def test_ping_monitor_config_key_and_missing_host_exits_early():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -290,7 +290,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_succe
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -329,7 +329,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_and_a
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -350,7 +350,7 @@ def test_ping_monitor_config_host_and_key_with_username_and_missing_password_exi
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -371,7 +371,7 @@ def test_ping_monitor_config_host_and_key_with_passing_and_missing_username_exit
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -392,7 +392,7 @@ def test_ping_monitor_config_itemid_with_api_key_auth_data_successful():
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -426,7 +426,7 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_successful(
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         dry_run=False,
     )
@@ -462,7 +462,7 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_and_push_po
         hook_config,
         {},
         'config.yaml',
-        borgmatic.hooks.monitor.State.FAIL,
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
         monitoring_log_level=1,
         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()}
     expected_return_value = flexmock()
     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(
         config['super_hook'], config, 'prefix', 55, value=66
     ).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
 
 
+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():
     config = {'other_hook': flexmock()}
     expected_return_value = flexmock()
     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(
         {}, config, 'prefix', 55, value=66
     ).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():
     config = {'super_hook': flexmock(), 'other_hook': flexmock()}
     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()
 
     with pytest.raises(ValueError):
         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():
     config = {'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(
         expected_return_values['super_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_skips_return_values_for_missing_hooks():
+def test_call_hooks_calls_skips_return_values_for_unconfigured_hooks():
     config = {'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'])
 
-    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
 
@@ -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():
     config = {'super_hook': flexmock(), 'other_hook': None}
     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', ('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
 
@@ -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():
     config = {'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(
         expected_return_values['super_hook']
     ).and_return(expected_return_values['other_hook'])
 
     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
@@ -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():
     config = {'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_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