diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index d20a88c..11ee8a8 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -38,7 +38,7 @@ jobs: flake8 src tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Type Check with MyPy run: | - mypy src tests + mypy --install-types --non-interactive --check-untyped-defs src tests - name: Unit Tests run: | python -m unittest diff --git a/INTEGRATIONS.md b/INTEGRATIONS.md index 62d3e92..2a2ad49 100644 --- a/INTEGRATIONS.md +++ b/INTEGRATIONS.md @@ -38,7 +38,15 @@ verify the sender email. Test with: ``` -SENDER="sender@example.com" SENDER_NAME="ChiaDog" RECIPIENT="you@example.com" HOST=smtp.example.com PORT=587 USERNAME_SMTP=username PASSWORD_SMTP=password python3 -m unittest tests.notifier.test_smtp_notifier +SMTP_SENDER="sender@example.com" \ +SMTP_SENDER_NAME="ChiaDog" \ +SMTP_RECIPIENT="you@example.com" \ +SMTP_HOST=smtp.example.com \ +SMTP_PORT=587 \ +SMTP_ENABLE_AUTH=true \ +SMTP_USERNAME=username \ +SMTP_PASSWORD=password \ +python3 -m unittest tests.notifier.test_smtp_notifier ``` ## Slack @@ -113,7 +121,7 @@ Messages sent to the MQTT topic look like this: Test with: ``` -HOST= PORT= TOPIC= python3 -m unittest tests.notifier.test_mqtt_notifier +MQTT_HOST= MQTT_PORT= MQTT_TOPIC= python3 -m unittest tests.notifier.test_mqtt_notifier ``` Or with full parameters: diff --git a/README.md b/README.md index e212558..dff8327 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ cd chiadog cp config-example.yaml config.yaml ``` -4. Open up `config.yaml` in your editor and configure it to your preferences. +4. Open up `config.yaml` in your editor and configure it to your preferences. The example is large, feel free to omit any portions where you're fine with the defaults! ### Updating to the latest release @@ -141,8 +141,6 @@ git pull ./install.sh ``` -> Important: Automated migration of config is not supported. Please check that your `config.yaml` has all new fields introduced in `config-example.yaml` and add anything missing. If correctly migrated, you shouldn't get any ERROR logs. - ## Monitoring a local harvester / farmer 1. Open `config.yaml` and configure `file_log_consumer`: diff --git a/config-example.yaml b/config-example.yaml index 179f316..1d80ec6 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -1,5 +1,7 @@ # Please copy this example config to config.yaml # and adjust it to your needs. +# Most config values have sane defaults! This example is more verbose than it needs to be, +# your config only needs to override what you want to change. # This is useful to differentiate multiple chiadog # instances monitoring multiple harvesters @@ -29,7 +31,7 @@ keep_alive_monitor: # Enable this and you'll receive a daily summary notification # on your farm performance at the specified time of the day. daily_stats: - enable: true + enable: true # default: false time_of_day: "21:00" frequency_hours: 24 @@ -40,7 +42,7 @@ handlers: enable: true # Transactions with lower amount mojos will be ignored # Use this to avoid notification spam during dust storms - min_mojos_amount: 5 + min_mojos_amount: 5 # default: 0 # Checks for skipped signage points (full node) finished_signage_point_handler: enable: true @@ -61,6 +63,9 @@ handlers: # notifications to each of them. E.g. enable daily_stats only to E-mail. # If you enable wallet_events you'll get notifications anytime your # wallet receives some XCH (e.g. farming reward). +# +# NOTE: No notifier is enabled by default, and all notification topics are disabled by default. +# You'll need to enable the notifiers and topics that you want to see! notifier: pushover: enable: false @@ -134,7 +139,7 @@ notifier: decreasing_plot_events: true increasing_plot_events: false topic: chia/chiadog/alert - qos: 1 + qos: 1 # default: 0 retain: false credentials: host: '192.168.0.10' diff --git a/main.py b/main.py index f54ab86..28bd706 100644 --- a/main.py +++ b/main.py @@ -8,11 +8,14 @@ from pathlib import Path from typing import Tuple +# lib +import confuse + # project from src.chia_log.handlers.daily_stats.stats_manager import StatsManager from src.chia_log.log_consumer import create_log_consumer_from_config from src.chia_log.log_handler import LogHandler -from src.config import Config, is_win_platform +from src.util import is_win_platform from src.notifier.keep_alive_monitor import KeepAliveMonitor from src.notifier.notify_manager import NotifyManager @@ -43,8 +46,8 @@ def get_log_level(log_level: str) -> int: return logging.INFO -def init(config:Config): - log_level = get_log_level(config.get_log_level_config()) +def init(config: confuse.core.Configuration): + log_level = get_log_level(config["log_level"].get()) logging.basicConfig( format="[%(asctime)s] [%(levelname)8s] --- %(message)s (%(filename)s:%(lineno)s)", level=log_level, @@ -54,24 +57,24 @@ def init(config:Config): logging.info(f"Starting Chiadog ({version()})") # Create log consumer based on provided configuration - chia_logs_config = config.get_chia_logs_config() + chia_logs_config = config['chia_logs'] log_consumer = create_log_consumer_from_config(chia_logs_config) if log_consumer is None: exit(0) # Keep a reference here so we can stop the thread # TODO: read keep-alive thresholds from config - keep_alive_monitor = KeepAliveMonitor(config=config.get_keep_alive_monitor_config()) + keep_alive_monitor = KeepAliveMonitor(config=config['keep_alive_monitor']) # Notify manager is responsible for the lifecycle of all notifiers notify_manager = NotifyManager(config=config, keep_alive_monitor=keep_alive_monitor) # Stats manager accumulates stats over 24 hours and sends a summary each day - stats_manager = StatsManager(config=config.get_daily_stats_config(), notify_manager=notify_manager) + stats_manager = StatsManager(config=config['daily_stats'], notify_manager=notify_manager) # Link stuff up in the log handler # Pipeline: Consume -> Handle -> Notify - log_handler = LogHandler(config=config.get_handlers_config(), log_consumer=log_consumer, notify_manager=notify_manager, + log_handler = LogHandler(config=config, log_consumer=log_consumer, notify_manager=notify_manager, stats_manager=stats_manager) def interrupt(signal_number, frame): @@ -108,8 +111,15 @@ def version(): # Parse config and configure logger argparse, args = parse_arguments() + # init sane config defaults + source_path = Path(__file__).resolve() + source_dir = source_path.parent + config = confuse.Configuration('chiadog', __name__) + config.set_file(source_dir / 'src/default_config.yaml') + + # Override with given config if args.config: - conf = Config(Path(args.config)) - init(conf) + config.set_file(Path(args.config)) + init(config) elif args.version: print(version()) diff --git a/pyproject.toml b/pyproject.toml index e34796e..9fc61c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,7 @@ [tool.black] -line-length = 120 \ No newline at end of file +line-length = 120 + +# No type hints: https://github.com/beetbox/confuse/issues/121 +[[tool.mypy.overrides]] +module = ["confuse", "confuse.exceptions"] +ignore_missing_imports = true diff --git a/requirements.txt b/requirements.txt index d1588ea..75337dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ python-dateutil~=2.8.1 PyYAML==5.4 retry==0.9.2 pygtail==0.11.1 +confuse==2.0.0 diff --git a/src/chia_log/handlers/__init__.py b/src/chia_log/handlers/__init__.py index 17f1e05..f67fa20 100644 --- a/src/chia_log/handlers/__init__.py +++ b/src/chia_log/handlers/__init__.py @@ -11,6 +11,9 @@ class that analyses a specific part of the log from typing import List, Optional import logging +# lib +from confuse import ConfigView + # project from .daily_stats.stats_manager import StatsManager from src.notifier import Event @@ -24,7 +27,7 @@ class LogHandlerInterface(ABC): def config_name() -> str: pass - def __init__(self, config: Optional[dict] = None): + def __init__(self, config: ConfigView): logging.info(f"Initializing handler: {self.config_name()}") @abstractmethod diff --git a/src/chia_log/handlers/condition_checkers/non_skipped_signage_points.py b/src/chia_log/handlers/condition_checkers/non_skipped_signage_points.py index efde006..7f29e24 100644 --- a/src/chia_log/handlers/condition_checkers/non_skipped_signage_points.py +++ b/src/chia_log/handlers/condition_checkers/non_skipped_signage_points.py @@ -1,6 +1,7 @@ # std import logging from typing import Optional +from datetime import datetime # project from . import FinishedSignageConditionChecker @@ -17,11 +18,11 @@ class NonSkippedSignagePoints(FinishedSignageConditionChecker): def __init__(self): logging.info("Enabled check for finished signage points.") - self._last_signage_point_timestamp = None - self._last_signage_point = None + self._last_signage_point_timestamp: datetime = datetime.fromtimestamp(0) + self._last_signage_point: int = 0 def check(self, obj: FinishedSignagePointMessage) -> Optional[Event]: - if self._last_signage_point is None: + if self._last_signage_point == 0: self._last_signage_point_timestamp = obj.timestamp self._last_signage_point = obj.signage_point return None diff --git a/src/chia_log/handlers/daily_stats/stat_accumulators/search_time_stats.py b/src/chia_log/handlers/daily_stats/stat_accumulators/search_time_stats.py index 34fc97d..66ddd10 100644 --- a/src/chia_log/handlers/daily_stats/stat_accumulators/search_time_stats.py +++ b/src/chia_log/handlers/daily_stats/stat_accumulators/search_time_stats.py @@ -9,14 +9,14 @@ class SearchTimeStats(HarvesterActivityConsumer, StatAccumulator): def __init__(self): self._last_reset_time = datetime.now() self._num_measurements = 0 - self._avg_time_seconds = 0 + self._avg_time_seconds = 0.0 self._over_5_seconds = 0 self._over_15_seconds = 0 def reset(self): self._last_reset_time = datetime.now() self._num_measurements = 0 - self._avg_time_seconds = 0 + self._avg_time_seconds = 0.0 self._over_5_seconds = 0 self._over_15_seconds = 0 @@ -29,7 +29,6 @@ def consume(self, obj: HarvesterActivityMessage): self._over_15_seconds += 1 def get_summary(self) -> str: - pct_over_5seconds: float = 0 pct_over_15seconds: float = 0 diff --git a/src/chia_log/handlers/daily_stats/stat_accumulators/signage_point_stats.py b/src/chia_log/handlers/daily_stats/stat_accumulators/signage_point_stats.py index fd6820f..5147f3e 100644 --- a/src/chia_log/handlers/daily_stats/stat_accumulators/signage_point_stats.py +++ b/src/chia_log/handlers/daily_stats/stat_accumulators/signage_point_stats.py @@ -9,8 +9,8 @@ class SignagePointStats(FinishedSignageConsumer, StatAccumulator): def __init__(self): self._last_reset_time = datetime.now() - self._last_signage_point_timestamp = None - self._last_signage_point = None + self._last_signage_point_timestamp: datetime = datetime.fromtimestamp(0) + self._last_signage_point: int = 0 self._skips_total = 0 self._total = 0 @@ -20,13 +20,16 @@ def reset(self): self._total = 0 def consume(self, obj: FinishedSignagePointMessage): - if self._last_signage_point is None: + if self._last_signage_point == 0: self._last_signage_point_timestamp = obj.timestamp self._last_signage_point = obj.signage_point return valid, skips = calculate_skipped_signage_points( - self._last_signage_point_timestamp, self._last_signage_point, obj.timestamp, obj.signage_point + self._last_signage_point_timestamp, + self._last_signage_point, + obj.timestamp, + obj.signage_point, ) if not valid: diff --git a/src/chia_log/handlers/daily_stats/stats_manager.py b/src/chia_log/handlers/daily_stats/stats_manager.py index 168b5d2..80203b2 100644 --- a/src/chia_log/handlers/daily_stats/stats_manager.py +++ b/src/chia_log/handlers/daily_stats/stats_manager.py @@ -6,6 +6,9 @@ from threading import Thread from time import sleep +# lib +from confuse import ConfigView + # project from . import ( HarvesterActivityConsumer, @@ -36,10 +39,10 @@ class StatsManager: with a summary from all stats that have been collected for the past 24 hours. """ - def __init__(self, config: dict, notify_manager: NotifyManager): - self._enable = config.get("enable", False) - self._notify_time = self._parse_notify_time(config.get("time_of_day", "21:00")) - self._frequency_hours = config.get("frequency_hours", 24) + def __init__(self, config: ConfigView, notify_manager: NotifyManager): + self._enable = config["enable"].get(bool) + self._notify_time = self._parse_notify_time(config["time_of_day"].get()) + self._frequency_hours = config["frequency_hours"].get(int) if not self._enable: logging.warning("Disabled stats and daily notifications") diff --git a/src/chia_log/handlers/wallet_added_coin_handler.py b/src/chia_log/handlers/wallet_added_coin_handler.py index c1dcd1b..911f8fd 100644 --- a/src/chia_log/handlers/wallet_added_coin_handler.py +++ b/src/chia_log/handlers/wallet_added_coin_handler.py @@ -2,6 +2,9 @@ import logging from typing import List, Optional +# lib +from confuse import ConfigView + # project from . import LogHandlerInterface from ..parsers.wallet_added_coin_parser import WalletAddedCoinParser @@ -18,11 +21,10 @@ class WalletAddedCoinHandler(LogHandlerInterface): def config_name() -> str: return "wallet_added_coin_handler" - def __init__(self, config: Optional[dict] = None): + def __init__(self, config: ConfigView): super().__init__(config) self._parser = WalletAddedCoinParser() - config = config or {} - self.min_mojos_amount = config.get("min_mojos_amount", 0) + self.min_mojos_amount = config["min_mojos_amount"].get(int) logging.info(f"Filtering transaction with mojos less than {self.min_mojos_amount}") def handle(self, logs: str, stats_manager: Optional[StatsManager] = None) -> List[Event]: diff --git a/src/chia_log/log_consumer.py b/src/chia_log/log_consumer.py index ad4e53b..d9614b6 100644 --- a/src/chia_log/log_consumer.py +++ b/src/chia_log/log_consumer.py @@ -16,17 +16,31 @@ from typing import List, Optional, Tuple # project -from src.config import Config -from src.config import check_keys from src.util import OS # lib import paramiko +import confuse +from confuse import ConfigView from paramiko.channel import ChannelStdinFile, ChannelStderrFile, ChannelFile from pygtail import Pygtail # type: ignore from retry import retry +# Define the minimum valid 'chia_logs' config sections as needed by the log consumers +file_log_consumer_template = { + "enable": bool, + "file_path": confuse.Path(), +} +network_log_consumer_template = { + "enable": bool, + "remote_file_path": confuse.Path(), + "remote_host": str, + "remote_user": str, + "remote_port": int, +} + + class LogConsumerSubscriber(ABC): """Interface for log consumer subscribers (i.e. handlers)""" @@ -59,7 +73,7 @@ def __init__(self, log_path: Path): super().__init__() logging.info("Enabled local file log consumer.") self._expanded_log_path = str(log_path.expanduser()) - self._offset_path = mkdtemp() / Config.get_log_offset_path() + self._offset_path = mkdtemp() / Path("debug.log.offset") logging.info(f"Using temporary directory {self._offset_path}") self._is_running = True self._thread = Thread(target=self._consume_loop) @@ -181,7 +195,6 @@ def _has_rotated(self, path: PurePath) -> bool: def get_host_info(host: str, user: str, path: str, port: int) -> Tuple[OS, PurePath]: - client = paramiko.client.SSHClient() client.load_system_host_keys() client.connect(hostname=host, username=user, port=port) @@ -202,10 +215,10 @@ def get_host_info(host: str, user: str, path: str, port: int) -> Tuple[OS, PureP return OS.LINUX, PurePosixPath(path) -def create_log_consumer_from_config(config: dict) -> Optional[LogConsumer]: +def create_log_consumer_from_config(config: ConfigView) -> Optional[LogConsumer]: enabled_consumer = None for consumer in config.keys(): - if config[consumer]["enable"]: + if config[consumer]["enable"].get(bool): if enabled_consumer: logging.error("Detected multiple enabled consumers. This is unsupported configuration!") return None @@ -217,40 +230,35 @@ def create_log_consumer_from_config(config: dict) -> Optional[LogConsumer]: enabled_consumer_config = config[enabled_consumer] if enabled_consumer == "file_log_consumer": - if not check_keys(required_keys=["file_path"], config=enabled_consumer_config): - return None - - return FileLogConsumer(log_path=Path(enabled_consumer_config["file_path"])) + # Validate config against template + valid_config = enabled_consumer_config.get(file_log_consumer_template) + return FileLogConsumer(log_path=valid_config["file_path"]) if enabled_consumer == "network_log_consumer": - if not check_keys( - required_keys=["remote_file_path", "remote_host", "remote_user"], config=enabled_consumer_config - ): - return None - - # default SSH Port : 22 - remote_port = enabled_consumer_config.get("remote_port", 22) + # Validate config against template + valid_config = enabled_consumer_config.get(network_log_consumer_template) + remote_port = valid_config["remote_port"] platform, path = get_host_info( - enabled_consumer_config["remote_host"], - enabled_consumer_config["remote_user"], - enabled_consumer_config["remote_file_path"], + valid_config["remote_host"], + valid_config["remote_user"], + valid_config["remote_file_path"], remote_port, ) if platform == OS.WINDOWS: return WindowsNetworkLogConsumer( remote_log_path=path, - remote_host=enabled_consumer_config["remote_host"], - remote_user=enabled_consumer_config["remote_user"], + remote_host=valid_config["remote_host"], + remote_user=valid_config["remote_user"], remote_port=remote_port, remote_platform=platform, ) else: return PosixNetworkLogConsumer( remote_log_path=path, - remote_host=enabled_consumer_config["remote_host"], - remote_user=enabled_consumer_config["remote_user"], + remote_host=valid_config["remote_host"], + remote_user=valid_config["remote_user"], remote_port=remote_port, remote_platform=platform, ) diff --git a/src/chia_log/log_handler.py b/src/chia_log/log_handler.py index e86ce7f..10ad795 100644 --- a/src/chia_log/log_handler.py +++ b/src/chia_log/log_handler.py @@ -2,6 +2,10 @@ from typing import Optional, List, Type import logging +# lib +from confuse import ConfigView +from confuse.exceptions import ConfigTypeError + # project from src.chia_log.handlers import LogHandlerInterface from src.chia_log.handlers.daily_stats.stats_manager import StatsManager @@ -17,9 +21,9 @@ def _check_handler_enabled(config: dict, handler_name: str) -> bool: """Fallback to True for backwards compatability""" try: - return config[handler_name].get("enable", True) - except KeyError as key: - logging.error(f"Invalid config.yaml. Missing key: {key}") + return config["handlers"][handler_name]["enable"].get(bool) + except ConfigTypeError as e: + logging.error(f"Invalid config.yaml, enabling handler anyway: {e}") return True @@ -39,7 +43,7 @@ class LogHandler(LogConsumerSubscriber): def __init__( self, - config: Optional[dict], + config: ConfigView, log_consumer: LogConsumer, notify_manager: NotifyManager, stats_manager: Optional[StatsManager] = None, @@ -47,7 +51,6 @@ def __init__( self._notify_manager = notify_manager self._stats_manager = stats_manager - config = config or {} available_handlers: List[Type[LogHandlerInterface]] = [ HarvesterActivityHandler, PartialHandler, @@ -58,7 +61,7 @@ def __init__( self._handlers = [] for handler in available_handlers: if _check_handler_enabled(config, handler.config_name()): - self._handlers.append(handler(config.get(handler.config_name()))) + self._handlers.append(handler(config["handlers"][handler.config_name()])) else: logging.info(f"Disabled handler: {handler.config_name()}") diff --git a/src/config.py b/src/config.py deleted file mode 100644 index bd66ea6..0000000 --- a/src/config.py +++ /dev/null @@ -1,63 +0,0 @@ -# std -import logging -import sys -from pathlib import Path -from typing import Optional - -# lib -import yaml - - -class Config: - def __init__(self, config_path: Path): - if not config_path.is_file(): - raise ValueError(f"Invalid config.yaml path: {config_path}") - - with open(config_path, "r", encoding="UTF-8") as config_file: - self._config = yaml.safe_load(config_file) - - def _get_child_config(self, key: str, required: bool = True) -> Optional[dict]: - if key not in self._config.keys(): - if required: - raise ValueError(f"Invalid config - cannot find {key} key") - else: - return None - - return self._config[key] - - def get_config(self): - return self._config - - def get_notifier_config(self): - return self._get_child_config("notifier") - - def get_chia_logs_config(self): - return self._get_child_config("chia_logs") - - def get_handlers_config(self): - return self._get_child_config("handlers", required=False) - - def get_log_level_config(self): - return self._get_child_config("log_level") - - def get_keep_alive_monitor_config(self): - return self._get_child_config("keep_alive_monitor", required=False) - - def get_daily_stats_config(self): - return self._get_child_config("daily_stats") - - @staticmethod - def get_log_offset_path() -> Path: - return Path("debug.log.offset") - - -def check_keys(required_keys, config) -> bool: - for key in required_keys: - if key not in config.keys(): - logging.error(f"Incompatible configuration. Missing {key} in {config}.") - return False - return True - - -def is_win_platform() -> bool: - return sys.platform.startswith("win") diff --git a/src/default_config.yaml b/src/default_config.yaml new file mode 100644 index 0000000..af4cc7b --- /dev/null +++ b/src/default_config.yaml @@ -0,0 +1,113 @@ +# !!YOU SHOULD NOT MODIFY THIS FILE!! +# Instead create and provide a minimal config.yaml that overrides what you need to change. +# While this can be a useful reference for all available options, +# config-example.yaml provides more useful comments and examples. + +notification_title_prefix: 'Chia' +log_level: INFO + +# Only one consumer can be enabled at a time, we default to local default path +chia_logs: + file_log_consumer: + enable: true + file_path: '~/.chia/mainnet/log/debug.log' + network_log_consumer: + enable: false + remote_file_path: '~/.chia/mainnet/log/debug.log' + remote_host: null # no sane default can be set + remote_user: "chia" + remote_port: 22 + +keep_alive_monitor: + enable_remote_ping: false + ping_url: null + +daily_stats: + enable: false + time_of_day: "21:00" + frequency_hours: 24 + +# All handlers are enabled by default +handlers: + wallet_added_coin_handler: + enable: true + min_mojos_amount: 0 + finished_signage_point_handler: + enable: true + block_handler: + enable: true + partial_handler: + enable: true + harvester_activity_handler: + enable: true + + +# No notifier or notifier feature is enabled by default +# This section is dense, read config-example.yaml instead! +notifier_defaults: ¬ifier_defaults + enable: false + daily_stats: false + wallet_events: false + decreasing_plot_events: false + increasing_plot_events: false + +notifier: + pushover: + <<: *notifier_defaults + credentials: + api_token: null + user_key: null + pushcut: + <<: *notifier_defaults + credentials: + api_token: null + user_key: null + telegram: + <<: *notifier_defaults + credentials: + bot_token: null + chat_id: null + smtp: + <<: *notifier_defaults + credentials: + sender: null + sender_name: 'chiadog' + recipient: null + username_smtp: null + password_smtp: null + enable_smtp_auth: true + host: null + port: 587 + script: + <<: *notifier_defaults + script_path: 'tests/test_script.sh' + discord: + <<: *notifier_defaults + credentials: + webhook_url: null + slack: + <<: *notifier_defaults + credentials: + webhook_url: null + mqtt: + <<: *notifier_defaults + topic: chia/chiadog/alert + qos: 0 + retain: false + credentials: + host: null + port: 8883 + username: null + password: null + grafana: + enable: false + credentials: + base_url: null + api_token: null + dashboard_id: -1 + panel_id: -1 + ifttt: + <<: *notifier_defaults + credentials: + api_token: null + webhook_name: null diff --git a/src/notifier/__init__.py b/src/notifier/__init__.py index 83cd55d..2425949 100644 --- a/src/notifier/__init__.py +++ b/src/notifier/__init__.py @@ -8,6 +8,9 @@ from typing import List from enum import Enum +# lib +from confuse import ConfigView + class EventPriority(Enum): """Event priority dictates how urgently @@ -60,17 +63,17 @@ class Notifier(ABC): Pushover, E-mail, Slack, WhatsApp, etc """ - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): self._title_prefix = title_prefix self._config = config self._conn_timeout_seconds = 10 self._notification_types = [EventType.USER] self._notification_services = [EventService.HARVESTER, EventService.FARMER, EventService.FULL_NODE] - daily_stats = config.get("daily_stats", False) - wallet_events = config.get("wallet_events", False) - decreasing_plot_events = config.get("decreasing_plot_events", False) - increasing_plot_events = config.get("increasing_plot_events", False) + daily_stats = config["daily_stats"].get(bool) + wallet_events = config["wallet_events"].get(bool) + decreasing_plot_events = config["decreasing_plot_events"].get(bool) + increasing_plot_events = config["increasing_plot_events"].get(bool) if daily_stats: self._notification_types.append(EventType.DAILY_STATS) self._notification_services.append(EventService.DAILY) diff --git a/src/notifier/discord_notifier.py b/src/notifier/discord_notifier.py index a2a9545..6275239 100644 --- a/src/notifier/discord_notifier.py +++ b/src/notifier/discord_notifier.py @@ -4,16 +4,19 @@ import urllib.parse from typing import List +# lib +from confuse import ConfigView + # project from . import Notifier, Event class DiscordNotifier(Notifier): - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): logging.info("Initializing Discord notifier.") super().__init__(title_prefix, config) try: - credentials = config["credentials"] + credentials = config["credentials"].get(dict) self.webhook_url = credentials["webhook_url"] except KeyError as key: logging.error(f"Invalid config.yaml. Missing key: {key}") diff --git a/src/notifier/grafana_notifier.py b/src/notifier/grafana_notifier.py index 60ffdde..c7318bf 100644 --- a/src/notifier/grafana_notifier.py +++ b/src/notifier/grafana_notifier.py @@ -9,16 +9,19 @@ from typing import List, Tuple from urllib.parse import ParseResult +# lib +from confuse import ConfigView + # project from . import Notifier, Event class GrafanaNotifier(Notifier): - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): logging.info("Initializing Grafana notifier.") super().__init__(title_prefix, config) try: - credentials = config["credentials"] + credentials = config["credentials"].get(dict) self._base_url = str(credentials["base_url"]).rstrip("/") self._api_token = credentials["api_token"] self._dashboard_id = credentials.get("dashboard_id", -1) diff --git a/src/notifier/ifttt_notifier.py b/src/notifier/ifttt_notifier.py index 2a38805..916e4c4 100644 --- a/src/notifier/ifttt_notifier.py +++ b/src/notifier/ifttt_notifier.py @@ -4,16 +4,19 @@ import json from typing import List +# lib +from confuse import ConfigView + # project from . import Notifier, Event class IftttNotifier(Notifier): - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): logging.info("Initializing Ifttt notifier.") super().__init__(title_prefix, config) try: - credentials = config["credentials"] + credentials = config["credentials"].get(dict) self.token = credentials["api_token"] self.webhook_name = credentials["webhook_name"] except KeyError as key: diff --git a/src/notifier/mqtt_notifier.py b/src/notifier/mqtt_notifier.py index a58c4a3..a83bac5 100644 --- a/src/notifier/mqtt_notifier.py +++ b/src/notifier/mqtt_notifier.py @@ -3,12 +3,16 @@ import logging from typing import List +# lib +from confuse import ConfigView +from confuse.exceptions import ConfigTypeError + # project from . import Notifier, Event class MqttNotifier(Notifier): - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): logging.info("Initializing MQTT notifier.") super().__init__(title_prefix, config) @@ -16,23 +20,22 @@ def __init__(self, title_prefix: str, config: dict): self._password = None try: - credentials = config["credentials"] self._set_config(config) - self._set_credentials(credentials) - except KeyError as key: - logging.error(f"Invalid config.yaml. Missing key: {key}") + self._set_credentials(config["credentials"]) + except ConfigTypeError as e: + logging.error(f"Invalid config.yaml: {e}") self._init_mqtt() - def _set_config(self, config: dict): + def _set_config(self, config: ConfigView): """ - :raises KeyError: If a key in the config doesn't exist - :param config: The YAML config for this notifier + :raises ConfigError: If a key in the config doesn't exist + :param config: The confuse ConfigView of this notifier :returns: None """ - self._topic = config["topic"] - self._qos: int = config.get("qos", 0) - self._retain: bool = config.get("retain", False) + self._topic = config["topic"].get() + self._qos: int = config["qos"].get(int) + self._retain: bool = config["retain"].get(bool) if self._qos not in [0, 1, 2]: logging.warning( @@ -41,19 +44,19 @@ def _set_config(self, config: dict): ) self._qos = 0 - def _set_credentials(self, credentials: dict): + def _set_credentials(self, credentials: ConfigView): """ - :raises KeyError: If a key in the config doesn't exist - :param config: The YAML config for this notifier + :raises ConfigError: If a key in the config doesn't exist + :param credentials: The ConfigView of the credentials section :returns: None """ - self._host = credentials["host"] - self._port: int = credentials["port"] + self._host = credentials["host"].get(str) + self._port: int = credentials["port"].get(int) - if "username" in credentials and credentials["username"] != "": - self._username = credentials["username"] - if "password" in credentials and credentials["password"] != "": - self._password = credentials["password"] + if credentials["username"]: + self._username = credentials["username"].get(str) + if credentials["password"]: + self._password = credentials["password"].get(str) def _init_mqtt(self) -> bool: try: @@ -116,12 +119,12 @@ def send_events_to_user(self, events: List[Event]) -> bool: for event in events: if event.type in self._notification_types and event.service in self._notification_services: - payload = json.dumps({"type": event.type.name, "prio": event.priority.name, "msg": event.message}) response = self._client.publish(self._topic, payload=payload, qos=self._qos, retain=self._retain) if response.rc == paho.MQTT_ERR_SUCCESS: + logging.debug("MQTT message sent successfully.") pass elif response.rc == paho.MQTT_ERR_NO_CONN: logging.warning("Message delivery failed because the MQTT Client was not connected") diff --git a/src/notifier/notify_manager.py b/src/notifier/notify_manager.py index c2f17c0..2745b67 100644 --- a/src/notifier/notify_manager.py +++ b/src/notifier/notify_manager.py @@ -1,7 +1,10 @@ # std import logging import time -from typing import List, Dict +from typing import List, Dict, Type + +# lib +from confuse import ConfigView # project from . import Event, Notifier @@ -16,7 +19,6 @@ from .discord_notifier import DiscordNotifier from .slack_notifier import SlackNotifier from .ifttt_notifier import IftttNotifier -from src.config import Config class NotifyManager: @@ -25,16 +27,16 @@ class NotifyManager: delivered to multiple services at once. """ - def __init__(self, config: Config, keep_alive_monitor: KeepAliveMonitor): + def __init__(self, config: ConfigView, keep_alive_monitor: KeepAliveMonitor): self._keep_alive_monitor = keep_alive_monitor self._keep_alive_monitor.set_notify_manager(self) self._notifiers: Dict[str, Notifier] = {} - self._config = config.get_notifier_config() - self._notification_title_prefix = config.get_config()["notification_title_prefix"] + self._config = config["notifier"] + self._notification_title_prefix = config["notification_title_prefix"].get(str) self._initialize_notifiers() - def _initialize_notifiers(self): - key_notifier_mapping = { + def _initialize_notifiers(self) -> None: + key_notifier_mapping: Dict[str, Type[Notifier]] = { "pushover": PushoverNotifier, "pushcut": PushcutNotifier, "script": ScriptNotifier, @@ -46,10 +48,10 @@ def _initialize_notifiers(self): "grafana": GrafanaNotifier, "ifttt": IftttNotifier, } - for key in self._config.keys(): + for key in self._config: if key not in key_notifier_mapping.keys(): logging.warning(f"Cannot find mapping for {key} notifier.") - if self._config[key]["enable"]: + if self._config[key]["enable"].get(bool): self._notifiers[key] = key_notifier_mapping[key]( title_prefix=self._notification_title_prefix, config=self._config[key] ) diff --git a/src/notifier/pushcut_notifier.py b/src/notifier/pushcut_notifier.py index 5bf7ae2..c01ab49 100644 --- a/src/notifier/pushcut_notifier.py +++ b/src/notifier/pushcut_notifier.py @@ -4,16 +4,19 @@ import json from typing import List +# lib +from confuse import ConfigView + # project from . import Notifier, Event class PushcutNotifier(Notifier): - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): logging.info("Initializing PushCut notifier.") super().__init__(title_prefix, config) try: - credentials = config["credentials"] + credentials = config["credentials"].get(dict) self.token = credentials["api_token"] self.notification_name = credentials["notification_name"] except KeyError as key: diff --git a/src/notifier/pushover_notifier.py b/src/notifier/pushover_notifier.py index a0401db..b7f397b 100644 --- a/src/notifier/pushover_notifier.py +++ b/src/notifier/pushover_notifier.py @@ -4,16 +4,19 @@ import urllib.parse from typing import List +# lib +from confuse import ConfigView + # project from . import Notifier, Event class PushoverNotifier(Notifier): - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): logging.info("Initializing Pushover notifier.") super().__init__(title_prefix, config) try: - credentials = config["credentials"] + credentials = config["credentials"].get(dict) self.token = credentials["api_token"] self.user = credentials["user_key"] except KeyError as key: diff --git a/src/notifier/script_notifier.py b/src/notifier/script_notifier.py index bdf085b..f2617eb 100644 --- a/src/notifier/script_notifier.py +++ b/src/notifier/script_notifier.py @@ -4,16 +4,19 @@ import subprocess from typing import List +# lib +from confuse import ConfigView, Path + # project from . import Notifier, Event class ScriptNotifier(Notifier): - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): logging.info("Initializing script notifier.") super().__init__(title_prefix, config) try: - self.script_path = config["script_path"] + self.script_path = config["script_path"].get(Path()) except KeyError as key: logging.error(f"Invalid config.yaml. Missing key: {key}") if self.script_path: diff --git a/src/notifier/slack_notifier.py b/src/notifier/slack_notifier.py index 84f506a..e693429 100644 --- a/src/notifier/slack_notifier.py +++ b/src/notifier/slack_notifier.py @@ -5,16 +5,19 @@ import urllib.parse from typing import List +# lib +from confuse import ConfigView + # project from . import Notifier, Event class SlackNotifier(Notifier): - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): logging.info("Initializing Slack notifier.") super().__init__(title_prefix, config) try: - credentials = config["credentials"] + credentials = config["credentials"].get(dict) self.webhook_url = credentials["webhook_url"] except KeyError as key: logging.error(f"Invalid config.yaml. Missing key: {key}") diff --git a/src/notifier/smtp_notifier.py b/src/notifier/smtp_notifier.py index ac2305e..f7e2cb4 100644 --- a/src/notifier/smtp_notifier.py +++ b/src/notifier/smtp_notifier.py @@ -6,28 +6,39 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +# lib +from confuse import ConfigView, OneOf # project from . import Notifier, Event +# Config validation template for SMTP credentials section +smtp_credentials_template = { + "sender": str, + "sender_name": str, + "recipient": str, + "username_smtp": OneOf([str, None]), + "password_smtp": OneOf([str, None]), + "host": str, + "port": int, + "enable_smtp_auth": bool, +} + class SMTPNotifier(Notifier): - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): logging.info("Initializing Email notifier.") super().__init__(title_prefix, config) - try: - credentials = config["credentials"] - self.sender = credentials["sender"] - self.sender_name = credentials["sender_name"] - self.recipient = credentials["recipient"] - self.username_smtp = credentials["username_smtp"] - self.password_smtp = credentials["password_smtp"] - self.host = credentials["host"] - self.port = credentials["port"] - self.enable_smtp_auth = credentials.get("enable_smtp_auth", True) - except KeyError as key: - logging.error(f"Invalid config.yaml. Missing key: {key}") + credentials = config["credentials"].get(smtp_credentials_template) + self.sender = credentials["sender"] + self.sender_name = credentials["sender_name"] + self.recipient = credentials["recipient"] + self.username_smtp = credentials["username_smtp"] + self.password_smtp = credentials["password_smtp"] + self.host = credentials["host"] + self.port = credentials["port"] + self.enable_smtp_auth = credentials["enable_smtp_auth"] def send_events_to_user(self, events: List[Event]) -> bool: errors = False diff --git a/src/notifier/telegram_notifier.py b/src/notifier/telegram_notifier.py index 52a92a2..d267a64 100644 --- a/src/notifier/telegram_notifier.py +++ b/src/notifier/telegram_notifier.py @@ -4,16 +4,19 @@ import json from typing import List +# lib +from confuse import ConfigView + # project from . import Notifier, Event class TelegramNotifier(Notifier): - def __init__(self, title_prefix: str, config: dict): + def __init__(self, title_prefix: str, config: ConfigView): logging.info("Initializing Telegram notifier.") super().__init__(title_prefix, config) try: - credentials = config["credentials"] + credentials = config["credentials"].get(dict) self.bot_token = credentials["bot_token"] self.chat_id = credentials["chat_id"] except KeyError as key: diff --git a/src/util.py b/src/util.py index 8fa66df..2962ef3 100644 --- a/src/util.py +++ b/src/util.py @@ -1,8 +1,12 @@ +import sys from enum import Enum class OS(Enum): - LINUX = "LINUX" MACOS = "MACOS" WINDOWS = "WINDOWS" + + +def is_win_platform() -> bool: + return sys.platform.startswith("win") diff --git a/tests/chia_log/handlers/test_wallet_added_coin_handler.py b/tests/chia_log/handlers/test_wallet_added_coin_handler.py index 6d33479..9a61b64 100644 --- a/tests/chia_log/handlers/test_wallet_added_coin_handler.py +++ b/tests/chia_log/handlers/test_wallet_added_coin_handler.py @@ -3,24 +3,30 @@ from pathlib import Path import copy +# lib +import confuse + # project from src.chia_log.handlers.wallet_added_coin_handler import WalletAddedCoinHandler from src.notifier import EventType, EventService, EventPriority -from src.config import Config class TestWalledAddedCoinHandler(unittest.TestCase): def setUp(self) -> None: config_dir = Path(__file__).resolve().parents[3] - config = Config(config_dir / "config-example.yaml") - self.handler_config = config.get_handlers_config()[WalletAddedCoinHandler.config_name()] + self.config = confuse.Configuration("chiadog", __name__) + self.config.set_file(config_dir / "src/default_config.yaml") + self.handler_config = self.config["handlers"][WalletAddedCoinHandler.config_name()] - self.handler = WalletAddedCoinHandler(config=None) + self.handler = WalletAddedCoinHandler(config=self.handler_config) self.example_logs_path = Path(__file__).resolve().parents[1] / "logs/wallet_added_coin" + def tearDown(self) -> None: + self.config.clear() + def testConfig(self): - self.assertEqual(self.handler_config["enable"], True) - self.assertEqual(self.handler_config["min_mojos_amount"], 5) + self.assertEqual(self.handler_config["enable"].get(bool), True) + self.assertEqual(self.handler_config["min_mojos_amount"].get(int), 0) # Dependent on default value being 0 def testNominal(self): with open(self.example_logs_path / "nominal-before-1.4.0.txt", encoding="UTF-8") as f: @@ -48,11 +54,11 @@ def testFloatPrecision(self): self.assertEqual(events[0].message, "Cha-ching! Just received 0.000000000001 XCH ☘️") def testTransactionAmountFilter(self): - default_config = self.handler_config - no_filter_config = copy.deepcopy(default_config) - no_filter_config["min_mojos_amount"] = 0 + no_filter_config = self.handler_config + filter_config = copy.deepcopy(self.handler_config) + filter_config["min_mojos_amount"].set(5) - filter_handler = WalletAddedCoinHandler(default_config) + filter_handler = WalletAddedCoinHandler(filter_config) no_filter_handler = WalletAddedCoinHandler(no_filter_config) with open(self.example_logs_path / "small_values.txt", encoding="UTF-8") as f: logs = f.readlines() diff --git a/tests/chia_log/parsers/test_harvester_activity_parser.py b/tests/chia_log/parsers/test_harvester_activity_parser.py index 4c89c8d..eb87f74 100644 --- a/tests/chia_log/parsers/test_harvester_activity_parser.py +++ b/tests/chia_log/parsers/test_harvester_activity_parser.py @@ -20,7 +20,6 @@ def tearDown(self) -> None: def testBasicParsing(self): for logs in [self.nominal_logs, self.nominal_logs_old_format]: - # Check that important fields are correctly parsed activity_messages = self.parser.parse(logs) self.assertNotEqual(len(activity_messages), 0, "No log messages found") diff --git a/tests/notifier/test_discord_notifier.py b/tests/notifier/test_discord_notifier.py index a9ed538..680985d 100644 --- a/tests/notifier/test_discord_notifier.py +++ b/tests/notifier/test_discord_notifier.py @@ -2,6 +2,9 @@ import os import unittest +# lib +import confuse + # project from src.notifier.discord_notifier import DiscordNotifier from .dummy_events import DummyEvents @@ -11,16 +14,20 @@ class TestDiscordNotifier(unittest.TestCase): def setUp(self) -> None: webhook_url = os.getenv("DISCORD_WEBHOOK_URL") self.assertIsNotNone(webhook_url, "You must export DISCORD_WEBHOOK_URL as env variable") - self.notifier = DiscordNotifier( - title_prefix="Test", - config={ + self.config = confuse.Configuration("chiadog", __name__) + self.config.set( + { "enable": True, "daily_stats": True, "wallet_events": True, "decreasing_plot_events": True, "increasing_plot_events": True, "credentials": {"webhook_url": webhook_url}, - }, + } + ) + self.notifier = DiscordNotifier( + title_prefix="Test", + config=self.config, ) @unittest.skipUnless(os.getenv("DISCORD_WEBHOOK_URL"), "Run only if webhook available") diff --git a/tests/notifier/test_grafana_notifier.py b/tests/notifier/test_grafana_notifier.py index 064c4b6..0d47b4b 100644 --- a/tests/notifier/test_grafana_notifier.py +++ b/tests/notifier/test_grafana_notifier.py @@ -2,6 +2,9 @@ import os import unittest +# lib +import confuse + # project from src.notifier.grafana_notifier import GrafanaNotifier from .dummy_events import DummyEvents @@ -13,15 +16,19 @@ def setUp(self) -> None: api_token = os.getenv("GRAFANA_API_TOKEN") self.assertIsNotNone(base_url, "You must export GRAFANA_BASE_URL as env variable") self.assertIsNotNone(api_token, "You must export GRAFANA_API_TOKEN as env variable") - self.notifier = GrafanaNotifier( - title_prefix="Test", - config={ + self.config = confuse.Configuration("chiadog", __name__) + self.config.set( + { "enable": True, "credentials": { "base_url": base_url, "api_token": api_token, }, - }, + } + ) + self.notifier = GrafanaNotifier( + title_prefix="Test", + config=self.config, ) @unittest.skipUnless( diff --git a/tests/notifier/test_ifttt_notifier.py b/tests/notifier/test_ifttt_notifier.py index 5ba9b41..320f62b 100644 --- a/tests/notifier/test_ifttt_notifier.py +++ b/tests/notifier/test_ifttt_notifier.py @@ -2,6 +2,9 @@ import os import unittest +# lib +import confuse + # project from src.notifier import Event, EventType, EventPriority, EventService from src.notifier.ifttt_notifier import IftttNotifier @@ -14,16 +17,20 @@ def setUp(self) -> None: self.webhook_name = os.getenv("IFTTT_WEBHOOK_NAME") self.assertIsNotNone(self.api_token, "You must export IFTTT_API_TOKEN as env variable") self.assertIsNotNone(self.webhook_name, "You must export IFTTT_WEBHOOK_NAME as env variable") - self.notifier = IftttNotifier( - title_prefix="Test", - config={ + self.config = confuse.Configuration("chiadog", __name__) + self.config.set( + { "enable": True, "daily_stats": True, "wallet_events": True, "decreasing_plot_events": True, "increasing_plot_events": True, "credentials": {"api_token": self.api_token, "webhook_name": self.webhook_name}, - }, + } + ) + self.notifier = IftttNotifier( + title_prefix="Test", + config=self.config, ) @unittest.skipUnless(os.getenv("IFTTT_API_TOKEN"), "Run only if token available") @@ -41,20 +48,21 @@ def testHighPriorityNotifications(self): success = self.notifier.send_events_to_user(events=DummyEvents.get_high_priority_events()) self.assertTrue(success) + @unittest.skipUnless(os.getenv("IFTTT_API_TOKEN"), "Run only if token available") @unittest.skipUnless(os.getenv("SHOWCASE_NOTIFICATIONS"), "Only for showcasing") def testShowcaseGoodNotifications(self): notifiers = [ IftttNotifier( title_prefix="Harvester 1", - config={"enable": True, "api_token": self.api_token, "webhook_name": self.webhook_name}, + config=self.config, ), IftttNotifier( title_prefix="Harvester 2", - config={"enable": True, "api_token": self.api_token, "webhook_name": self.webhook_name}, + config=self.config, ), IftttNotifier( title_prefix="Harvester 3", - config={"enable": True, "api_token": self.api_token, "webhook_name": self.webhook_name}, + config=self.config, ), ] found_proof_event = Event( @@ -67,20 +75,21 @@ def testShowcaseGoodNotifications(self): success = notifier.send_events_to_user(events=[found_proof_event]) self.assertTrue(success) + @unittest.skipUnless(os.getenv("IFTTT_API_TOKEN"), "Run only if token available") @unittest.skipUnless(os.getenv("SHOWCASE_NOTIFICATIONS"), "Only for showcasing") def testShowcaseBadNotifications(self): notifiers = [ IftttNotifier( title_prefix="Harvester 1", - config={"enable": True, "api_token": self.api_token, "webhook_name": self.webhook_name}, + config=self.config, ), IftttNotifier( title_prefix="Harvester 2", - config={"enable": True, "api_token": self.api_token, "webhook_name": self.webhook_name}, + config=self.config, ), IftttNotifier( title_prefix="Harvester 3", - config={"enable": True, "api_token": self.api_token, "webhook_name": self.webhook_name}, + config=self.config, ), ] disconnected_hdd = Event( diff --git a/tests/notifier/test_mqtt_notifier.py b/tests/notifier/test_mqtt_notifier.py index a5aa5f8..1892b8e 100644 --- a/tests/notifier/test_mqtt_notifier.py +++ b/tests/notifier/test_mqtt_notifier.py @@ -2,6 +2,9 @@ import os import unittest +# lib +import confuse + # project from src.notifier.mqtt_notifier import MqttNotifier from .dummy_events import DummyEvents @@ -9,25 +12,25 @@ class TestMqttNotifier(unittest.TestCase): def setUp(self) -> None: - host = os.getenv("HOST") - topic = os.getenv("TOPIC") + host = os.getenv("MQTT_HOST") + topic = os.getenv("MQTT_TOPIC") username = os.getenv("MQTT_USERNAME") password = os.getenv("MQTT_PASSWORD") - port = int(os.getenv("PORT", 1883)) - qos = int(os.getenv("QOS", 0)) - retain = bool(os.getenv("RETAIN", False)) + port = int(os.getenv("MQTT_PORT", 1883)) + qos = int(os.getenv("MQTT_QOS", 0)) + retain = bool(os.getenv("MQTT_RETAIN", False)) - self.assertIsNotNone(host, "You must export HOST as env variable") - self.assertIsNotNone(port, "You must export PORT as env variable") - self.assertIsNotNone(topic, "You must export TOPIC as env variable") + self.assertIsNotNone(host, "You must export MQTT_HOST as env variable") + self.assertIsNotNone(port, "You must export MQTT_PORT as env variable") + self.assertIsNotNone(topic, "You must export MQTT_TOPIC as env variable") self.assertIn( qos, [0, 1, 2], "QoS level must be set to 0 (At most once), 1 (at least once) or " "2 (Exactly once)" ) - self.notifier = MqttNotifier( - title_prefix="Test", - config={ + self.config = confuse.Configuration("chiadog", __name__) + self.config.set( + { "enable": True, "daily_stats": True, "wallet_events": True, @@ -42,20 +45,25 @@ def setUp(self) -> None: "username": username, "password": password, }, - }, + } + ) + + self.notifier = MqttNotifier( + title_prefix="Test", + config=self.config, ) - @unittest.skipUnless(os.getenv("TOPIC"), "Run only if MQTT topic available") + @unittest.skipUnless(os.getenv("MQTT_TOPIC"), "Run only if MQTT topic available") def testMqttLowPriorityNotifications(self): success = self.notifier.send_events_to_user(events=DummyEvents.get_low_priority_events()) self.assertTrue(success) - @unittest.skipUnless(os.getenv("TOPIC"), "Run only if MQTT topic available") + @unittest.skipUnless(os.getenv("MQTT_TOPIC"), "Run only if MQTT topic available") def testMqttNormalPriorityNotifications(self): success = self.notifier.send_events_to_user(events=DummyEvents.get_normal_priority_events()) self.assertTrue(success) - @unittest.skipUnless(os.getenv("TOPIC"), "Run only if MQTT topic available") + @unittest.skipUnless(os.getenv("MQTT_TOPIC"), "Run only if MQTT topic available") def testMqttHighPriorityNotifications(self): success = self.notifier.send_events_to_user(events=DummyEvents.get_high_priority_events()) self.assertTrue(success) diff --git a/tests/notifier/test_pushcut_notifier.py b/tests/notifier/test_pushcut_notifier.py index 645586c..6154ad2 100644 --- a/tests/notifier/test_pushcut_notifier.py +++ b/tests/notifier/test_pushcut_notifier.py @@ -2,6 +2,9 @@ import os import unittest +# lib +import confuse + # project from src.notifier import Event, EventType, EventPriority, EventService from src.notifier.pushcut_notifier import PushcutNotifier @@ -14,14 +17,18 @@ def setUp(self) -> None: self.notification_name = os.getenv("PUSHCUT_NOTIFICATION_NAME") self.assertIsNotNone(self.api_token, "You must export PUSHCUT_API_TOKEN as env variable") self.assertIsNotNone(self.notification_name, "You must export PUSHCUT_NOTIFICATION_NAME as env variable") - self.notifier = PushcutNotifier( - title_prefix="Test", - config={ + self.config = confuse.Configuration("chiadog", __name__) + self.config.set( + { "enable": True, "daily_stats": True, "wallet_events": True, "credentials": {"api_token": self.api_token, "notification_name": self.notification_name}, - }, + } + ) + self.notifier = PushcutNotifier( + title_prefix="Test", + config=self.config, ) @unittest.skipUnless(os.getenv("PUSHCUT_API_TOKEN"), "Run only if token available") @@ -39,20 +46,21 @@ def testHighPriorityNotifications(self): success = self.notifier.send_events_to_user(events=DummyEvents.get_high_priority_events()) self.assertTrue(success) + @unittest.skipUnless(os.getenv("PUSHCUT_API_TOKEN"), "Run only if token available") @unittest.skipUnless(os.getenv("SHOWCASE_NOTIFICATIONS"), "Only for showcasing") def testShowcaseGoodNotifications(self): notifiers = [ PushcutNotifier( title_prefix="Harvester 1", - config={"enable": True, "api_token": self.api_token, "notification_name": self.notification_name}, + config=self.config, ), PushcutNotifier( title_prefix="Harvester 2", - config={"enable": True, "api_token": self.api_token, "notification_name": self.notification_name}, + config=self.config, ), PushcutNotifier( title_prefix="Harvester 3", - config={"enable": True, "api_token": self.api_token, "notification_name": self.notification_name}, + config=self.config, ), ] found_proof_event = Event( @@ -65,20 +73,21 @@ def testShowcaseGoodNotifications(self): success = notifier.send_events_to_user(events=[found_proof_event]) self.assertTrue(success) + @unittest.skipUnless(os.getenv("PUSHCUT_API_TOKEN"), "Run only if token available") @unittest.skipUnless(os.getenv("SHOWCASE_NOTIFICATIONS"), "Only for showcasing") def testShowcaseBadNotifications(self): notifiers = [ PushcutNotifier( title_prefix="Harvester 1", - config={"enable": True, "api_token": self.api_token, "notification_name": self.notification_name}, + config=self.config, ), PushcutNotifier( title_prefix="Harvester 2", - config={"enable": True, "api_token": self.api_token, "notification_name": self.notification_name}, + config=self.config, ), PushcutNotifier( title_prefix="Harvester 3", - config={"enable": True, "api_token": self.api_token, "notification_name": self.notification_name}, + config=self.config, ), ] disconnected_hdd = Event( diff --git a/tests/notifier/test_pushover_notifier.py b/tests/notifier/test_pushover_notifier.py index 2c57962..46a48d2 100644 --- a/tests/notifier/test_pushover_notifier.py +++ b/tests/notifier/test_pushover_notifier.py @@ -2,6 +2,9 @@ import os import unittest +# lib +import confuse + # project from src.notifier import Event, EventType, EventPriority, EventService from src.notifier.pushover_notifier import PushoverNotifier @@ -14,16 +17,20 @@ def setUp(self) -> None: self.user_key = os.getenv("PUSHOVER_USER_KEY") self.assertIsNotNone(self.api_token, "You must export PUSHOVER_API_TOKEN as env variable") self.assertIsNotNone(self.user_key, "You must export PUSHOVER_USER_KEY as env variable") - self.notifier = PushoverNotifier( - title_prefix="Test", - config={ + self.config = confuse.Configuration("chiadog", __name__) + self.config.set( + { "enable": True, "daily_stats": True, "wallet_events": True, "decreasing_plot_events": True, "increasing_plot_events": True, "credentials": {"api_token": self.api_token, "user_key": self.user_key}, - }, + } + ) + self.notifier = PushoverNotifier( + title_prefix="Test", + config=self.config, ) @unittest.skipUnless(os.getenv("PUSHOVER_API_TOKEN"), "Run only if token available") @@ -41,20 +48,21 @@ def testHighPriorityNotifications(self): success = self.notifier.send_events_to_user(events=DummyEvents.get_high_priority_events()) self.assertTrue(success) + @unittest.skipUnless(os.getenv("PUSHOVER_API_TOKEN"), "Run only if token available") @unittest.skipUnless(os.getenv("SHOWCASE_NOTIFICATIONS"), "Only for showcasing") def testShowcaseGoodNotifications(self): notifiers = [ PushoverNotifier( title_prefix="Harvester 1", - config={"enable": True, "api_token": self.api_token, "user_key": self.user_key}, + config=self.config, ), PushoverNotifier( title_prefix="Harvester 2", - config={"enable": True, "api_token": self.api_token, "user_key": self.user_key}, + config=self.config, ), PushoverNotifier( title_prefix="Harvester 3", - config={"enable": True, "api_token": self.api_token, "user_key": self.user_key}, + config=self.config, ), ] found_proof_event = Event( @@ -67,20 +75,21 @@ def testShowcaseGoodNotifications(self): success = notifier.send_events_to_user(events=[found_proof_event]) self.assertTrue(success) + @unittest.skipUnless(os.getenv("PUSHOVER_API_TOKEN"), "Run only if token available") @unittest.skipUnless(os.getenv("SHOWCASE_NOTIFICATIONS"), "Only for showcasing") def testShowcaseBadNotifications(self): notifiers = [ PushoverNotifier( title_prefix="Harvester 1", - config={"enable": True, "api_token": self.api_token, "user_key": self.user_key}, + config=self.config, ), PushoverNotifier( title_prefix="Harvester 2", - config={"enable": True, "api_token": self.api_token, "user_key": self.user_key}, + config=self.config, ), PushoverNotifier( title_prefix="Harvester 3", - config={"enable": True, "api_token": self.api_token, "user_key": self.user_key}, + config=self.config, ), ] disconnected_hdd = Event( diff --git a/tests/notifier/test_script_notifier.py b/tests/notifier/test_script_notifier.py index 1136154..a28ab07 100644 --- a/tests/notifier/test_script_notifier.py +++ b/tests/notifier/test_script_notifier.py @@ -1,6 +1,9 @@ # std import unittest +# lib +import confuse + # project from src.notifier.script_notifier import ScriptNotifier from .dummy_events import DummyEvents @@ -8,16 +11,20 @@ class TestScriptNotifier(unittest.TestCase): def setUp(self) -> None: - self.notifier = ScriptNotifier( - title_prefix="Test", - config={ + self.config = confuse.Configuration("chiadog", __name__) + self.config.set( + { "enable": True, "daily_stats": True, "wallet_events": True, "decreasing_plot_events": True, "increasing_plot_events": True, "script_path": "tests/test_script.sh", - }, + } + ) + self.notifier = ScriptNotifier( + title_prefix="Test", + config=self.config, ) def testLowPriorityNotifications(self): diff --git a/tests/notifier/test_slack_notifier.py b/tests/notifier/test_slack_notifier.py index 54b20a3..7329971 100644 --- a/tests/notifier/test_slack_notifier.py +++ b/tests/notifier/test_slack_notifier.py @@ -2,6 +2,9 @@ import os import unittest +# lib +import confuse + # project from src.notifier.slack_notifier import SlackNotifier from .dummy_events import DummyEvents @@ -11,16 +14,20 @@ class TestSlackNotifier(unittest.TestCase): def setUp(self) -> None: webhook_url = os.getenv("SLACK_WEBHOOK_URL") self.assertIsNotNone(webhook_url, "You must export SLACK_WEBHOOK_URL as env variable") - self.notifier = SlackNotifier( - title_prefix="Test", - config={ + self.config = confuse.Configuration("chiadog", __name__) + self.config.set( + { "enable": True, "daily_stats": True, "wallet_events": True, "decreasing_plot_events": True, "increasing_plot_events": True, "credentials": {"webhook_url": webhook_url}, - }, + } + ) + self.notifier = SlackNotifier( + title_prefix="Test", + config=self.config, ) @unittest.skipUnless(os.getenv("SLACK_WEBHOOK_URL"), "Run only if webhook available") diff --git a/tests/notifier/test_smtp_notifier.py b/tests/notifier/test_smtp_notifier.py index 3db136d..2cce211 100644 --- a/tests/notifier/test_smtp_notifier.py +++ b/tests/notifier/test_smtp_notifier.py @@ -2,6 +2,9 @@ import os import unittest +# lib +import confuse + # project from src.notifier.smtp_notifier import SMTPNotifier from .dummy_events import DummyEvents @@ -9,24 +12,26 @@ class TestSMTPNotifier(unittest.TestCase): def setUp(self) -> None: - sender = os.getenv("SENDER") - sender_name = os.getenv("SENDER_NAME") - recipient = os.getenv("RECIPIENT") - username_smtp = os.getenv("USERNAME_SMTP") - password_smtp = os.getenv("PASSWORD_SMTP") - host = os.getenv("HOST") - port = os.getenv("PORT") - self.assertIsNotNone(sender, "You must export SENDER as env variable") - self.assertIsNotNone(sender_name, "You must export SENDER_NAME as env variable") - self.assertIsNotNone(recipient, "You must export RECIPIENT as env variable") - self.assertIsNotNone(username_smtp, "You must export USERNAME_SMTP as env variable") - self.assertIsNotNone(password_smtp, "You must export PASSWORD_SMTP as env variable") - self.assertIsNotNone(host, "You must export HOST as env variable") - self.assertIsNotNone(port, "You must export PORT as env variable") + sender = os.getenv("SMTP_SENDER") + sender_name = os.getenv("SMTP_SENDER_NAME") + recipient = os.getenv("SMTP_RECIPIENT") + username_smtp = os.getenv("SMTP_USERNAME") + password_smtp = os.getenv("SMTP_PASSWORD") + host = os.getenv("SMTP_HOST") + port: int = 587 + if os.getenv("SMTP_PORT"): + port = int(os.environ["SMTP_PORT"]) + smtp_auth = True + if os.getenv("SMTP_ENABLE_AUTH"): + smtp_auth = os.environ["SMTP_ENABLE_AUTH"].lower() in ["true", "1", "yes"] + self.assertIsNotNone(sender, "You must export SMTP_SENDER as env variable") + self.assertIsNotNone(sender_name, "You must export SMTP_SENDER_NAME as env variable") + self.assertIsNotNone(recipient, "You must export SMTP_RECIPIENT as env variable") + self.assertIsNotNone(host, "You must export SMTP_HOST as env variable") - self.notifier = SMTPNotifier( - title_prefix="Test", - config={ + self.config = confuse.Configuration("chiadog", __name__) + self.config.set( + { "enable": True, "daily_stats": True, "wallet_events": True, @@ -36,25 +41,31 @@ def setUp(self) -> None: "sender": sender, "sender_name": sender_name, "recipient": recipient, + "enable_smtp_auth": smtp_auth, "username_smtp": username_smtp, "password_smtp": password_smtp, "host": host, "port": port, }, - }, + } + ) + + self.notifier = SMTPNotifier( + title_prefix="Test", + config=self.config, ) - @unittest.skipUnless(os.getenv("USERNAME_SMTP"), "Run only if SMTP available") + @unittest.skipUnless(os.getenv("SMTP_HOST"), "Run only if SMTP available") def testSTMPLowPriorityNotifications(self): success = self.notifier.send_events_to_user(events=DummyEvents.get_low_priority_events()) self.assertTrue(success) - @unittest.skipUnless(os.getenv("USERNAME_SMTP"), "Run only if SMTP available") + @unittest.skipUnless(os.getenv("SMTP_HOST"), "Run only if SMTP available") def testSMTPNormalPriorityNotifications(self): success = self.notifier.send_events_to_user(events=DummyEvents.get_normal_priority_events()) self.assertTrue(success) - @unittest.skipUnless(os.getenv("USERNAME_SMTP"), "Run only if SMTP available") + @unittest.skipUnless(os.getenv("SMTP_HOST"), "Run only if SMTP available") def testSTMPHighPriorityNotifications(self): success = self.notifier.send_events_to_user(events=DummyEvents.get_high_priority_events()) self.assertTrue(success) diff --git a/tests/notifier/test_telegram_notifier.py b/tests/notifier/test_telegram_notifier.py index b34a12a..f5a17b4 100644 --- a/tests/notifier/test_telegram_notifier.py +++ b/tests/notifier/test_telegram_notifier.py @@ -2,6 +2,9 @@ import os import unittest +# lib +import confuse + # project from src.notifier.telegram_notifier import TelegramNotifier from .dummy_events import DummyEvents @@ -13,16 +16,20 @@ def setUp(self) -> None: chat_id = os.getenv("TELEGRAM_CHAT_ID") self.assertIsNotNone(bot_token, "You must export TELEGRAM_API_KEY as env variable") self.assertIsNotNone(chat_id, "You must export TELEGRAM_CHAT_ID as env variable") - self.notifier = TelegramNotifier( - title_prefix="Test", - config={ + self.config = confuse.Configuration("chiadog", __name__) + self.config.set( + { "enable": True, "daily_stats": True, "wallet_events": True, "decreasing_plot_events": True, "increasing_plot_events": True, "credentials": {"bot_token": bot_token, "chat_id": chat_id}, - }, + } + ) + self.notifier = TelegramNotifier( + title_prefix="Test", + config=self.config, ) @unittest.skipUnless(os.getenv("TELEGRAM_BOT_TOKEN"), "Run only if token available") diff --git a/tests/test_config.py b/tests/test_config.py deleted file mode 100644 index 10ac7d6..0000000 --- a/tests/test_config.py +++ /dev/null @@ -1,29 +0,0 @@ -# std -import unittest -from pathlib import Path - -# project -from src.config import Config - - -class TestConfig(unittest.TestCase): - def setUp(self) -> None: - self.config_dir = Path(__file__).resolve().parents[1] - - def testBasic(self): - with self.assertRaises(ValueError): - _ = Config(self.config_dir / "wrong.yaml") - - config = Config(self.config_dir / "config-example.yaml") - notifier_config = config.get_notifier_config() - self.assertEqual(notifier_config["pushover"]["enable"], False) - self.assertEqual(notifier_config["pushover"]["credentials"]["api_token"], "dummy_token") - self.assertEqual(notifier_config["pushover"]["credentials"]["user_key"], "dummy_key") - - chia_logs_config = config.get_chia_logs_config() - self.assertEqual(chia_logs_config["file_log_consumer"]["enable"], True) - self.assertEqual(chia_logs_config["file_log_consumer"]["file_path"], "~/.chia/mainnet/log/debug.log") - - -if __name__ == "__main__": - unittest.main()