PyEmailerAJM 1.8__tar.gz → 1.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PKG-INFO +2 -2
  2. pyemailerajm-1.8.2/PyEmailerAJM/_version.py +1 -0
  3. pyemailerajm-1.8.2/PyEmailerAJM/backend/__init__.py +34 -0
  4. pyemailerajm-1.8.2/PyEmailerAJM/backend/enums.py +36 -0
  5. pyemailerajm-1.8.2/PyEmailerAJM/backend/errs.py +31 -0
  6. pyemailerajm-1.8.2/PyEmailerAJM/backend/logger.py +94 -0
  7. pyemailerajm-1.8.2/PyEmailerAJM/backend/the_sandman.py +91 -0
  8. pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/__init__.py +4 -0
  9. pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/backend/__init__.py +6 -0
  10. pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/backend/continuous_colorizer.py +138 -0
  11. pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/backend/continuous_monitor_base.py +114 -0
  12. pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/backend/email_state.py +127 -0
  13. pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/backend/snooze_tracking.py +172 -0
  14. pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/continuous_monitor.py +88 -0
  15. pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/continuous_monitor_alert_send.py +120 -0
  16. pyemailerajm-1.8.2/PyEmailerAJM/msg/__init__.py +5 -0
  17. pyemailerajm-1.8.2/PyEmailerAJM/msg/alert_messages.py +252 -0
  18. pyemailerajm-1.8.2/PyEmailerAJM/msg/factory.py +75 -0
  19. pyemailerajm-1.8.2/PyEmailerAJM/msg/msg.py +253 -0
  20. pyemailerajm-1.8.2/PyEmailerAJM/searchers/__init__.py +3 -0
  21. pyemailerajm-1.8.2/PyEmailerAJM/searchers/searchers.py +137 -0
  22. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM.egg-info/PKG-INFO +2 -2
  23. pyemailerajm-1.8.2/PyEmailerAJM.egg-info/SOURCES.txt +34 -0
  24. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/setup.py +5 -2
  25. pyemailerajm-1.8/PyEmailerAJM/_version.py +0 -1
  26. pyemailerajm-1.8/PyEmailerAJM.egg-info/SOURCES.txt +0 -15
  27. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/LICENSE.txt +0 -0
  28. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM/__init__.py +0 -0
  29. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM/py_emailer_ajm.py +0 -0
  30. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM.egg-info/dependency_links.txt +0 -0
  31. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM.egg-info/requires.txt +0 -0
  32. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM.egg-info/top_level.txt +0 -0
  33. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/README.md +0 -0
  34. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/setup.cfg +0 -0
  35. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/tests/test_PyEmailerAJM.py +0 -0
  36. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/tests/test_logger.py +0 -0
  37. {pyemailerajm-1.8 → pyemailerajm-1.8.2}/tests/test_snooze_tracking.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyEmailerAJM
3
- Version: 1.8
3
+ Version: 1.8.2
4
4
  Summary: Allows for automating sending Email with the Outlook Desktop client. Future releases will add more client support
5
5
  Home-page: https://github.com/amcsparron2793-Water/PyEmailer
6
- Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.8.tar.gz
6
+ Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.8.2.tar.gz
7
7
  Author: Amcsparron
8
8
  Author-email: amcsparron@albanyny.gov
9
9
  License: MIT License
@@ -0,0 +1 @@
1
+ __version__ = '1.8.2'
@@ -0,0 +1,34 @@
1
+ from PyEmailerAJM.backend.errs import *
2
+ from PyEmailerAJM.backend.enums import BasicEmailFolderChoices, AlertTypes
3
+ from PyEmailerAJM.backend.the_sandman import TheSandman
4
+ from PyEmailerAJM.backend.logger import PyEmailerLogger
5
+ import warnings
6
+ import functools
7
+
8
+
9
+ def deprecated(reason: str = ""):
10
+ """
11
+ Decorator that marks a function or method as deprecated.
12
+
13
+ :param reason: Optional message to explain what to use instead
14
+ or when the feature will be removed.
15
+ """
16
+
17
+ def decorator(func):
18
+ @functools.wraps(func)
19
+ def wrapper(*args, **kwargs):
20
+ message = f"Function '{func.__name__}' is deprecated."
21
+ if reason:
22
+ message += f" {reason}"
23
+ warnings.warn(message, category=DeprecationWarning, stacklevel=2)
24
+ return func(*args, **kwargs)
25
+
26
+ return wrapper
27
+
28
+ return decorator
29
+
30
+
31
+ __all__ = ['deprecated', 'EmailerNotSetupError', 'InvalidAlertLevel',
32
+ 'DisplayManualQuit', 'NoMessagesFetched',
33
+ 'UnrecognizedEmailError', 'BasicEmailFolderChoices',
34
+ 'AlertTypes', 'TheSandman', 'PyEmailerLogger']
@@ -0,0 +1,36 @@
1
+ from enum import Enum, IntEnum
2
+
3
+
4
+ class AlertTypes(Enum):
5
+ """
6
+ An enumeration to represent different alert types with associated integer values.
7
+
8
+ Attributes:
9
+ WARNING: An alert type indicating a warning with an associated value of 5.
10
+ CRITICAL_WARNING: An alert type indicating a critical warning with an associated value of 24.
11
+ OVERDUE: An alert type indicating an overdue alert with an associated value of 48.
12
+
13
+ Methods:
14
+ __str__: Returns the name of the alert type as a string.
15
+ """
16
+ WARNING = 5
17
+ CRITICAL_WARNING = 24
18
+ OVERDUE = 48
19
+
20
+ def __str__(self):
21
+ return self.name
22
+
23
+
24
+ class BasicEmailFolderChoices(IntEnum):
25
+ INBOX = 6
26
+ SENT_ITEMS = 5
27
+ DRAFTS = 16
28
+ DELETED_ITEMS = 3
29
+ OUTBOX = 4
30
+
31
+ def __str__(self):
32
+ """Return the enum name as a string."""
33
+ return self.name
34
+
35
+ def __repr__(self):
36
+ return f"<{self.__class__.__name__}.{self.name} ({self.value})>"
@@ -0,0 +1,31 @@
1
+ from typing import Optional
2
+
3
+ # noinspection PyUnresolvedReferences
4
+ from pywintypes import com_error
5
+
6
+
7
+ class EmailerNotSetupError(Exception):
8
+ ...
9
+
10
+
11
+ class DisplayManualQuit(Exception):
12
+ ...
13
+
14
+
15
+ class InvalidAlertLevel(Exception):
16
+ DEFAULT_ERR_MSG = 'Invalid alert level: {}'
17
+
18
+ def __init__(self, msg: '_AlertMessageBase', **kwargs):
19
+ self.msg = msg
20
+ self.err_msg_str = kwargs.get('err_msg_str', self.DEFAULT_ERR_MSG.format(self.msg.ALERT_LEVEL))
21
+ super().__init__(self.err_msg_str)
22
+
23
+
24
+ class NoMessagesFetched(Exception):
25
+ ...
26
+
27
+
28
+ class UnrecognizedEmailError(com_error):
29
+ def __init__(self, err_msg: Optional[str] = None, **kwargs):
30
+ self.err_msg = err_msg
31
+ super().__init__(self.err_msg, **kwargs)
@@ -0,0 +1,94 @@
1
+ from logging import Filter, DEBUG, ERROR, Handler, FileHandler, StreamHandler, Logger, WARNING
2
+ from pathlib import Path
3
+ from typing import Union
4
+
5
+ from EasyLoggerAJM import EasyLogger, OutlookEmailHandler, StreamHandlerIgnoreExecInfo
6
+ from PyEmailerAJM.msg import Msg
7
+ from PyEmailerAJM import __project_name__, __project_root__
8
+
9
+
10
+ class DupeDebugFilter(Filter):
11
+ PREFIXES_TO_IGNORE = ["FW:", "RE:"]
12
+
13
+ def __init__(self, name="DebugDedupeFilter"):
14
+ super().__init__(name)
15
+ self.logged_messages = set()
16
+
17
+ def _clean_str(self, in_str):
18
+ for x in self.__class__.PREFIXES_TO_IGNORE:
19
+ in_str = in_str.replace(x, '')
20
+ return in_str
21
+
22
+ def filter(self, record):
23
+ # We only log the message if it has not been logged before
24
+ if record.levelno != DEBUG:
25
+ return True
26
+ clean_msg = self._clean_str(record.msg)
27
+ if clean_msg not in list(self.logged_messages):
28
+ self.logged_messages.add(clean_msg)
29
+ return True
30
+ return False
31
+
32
+
33
+ class PyEmailerLogger(EasyLogger):
34
+ ROOT_LOG_LOCATION_DEFAULT = Path(__project_root__, 'logs').resolve()
35
+
36
+ def __call__(self):
37
+ return self.logger
38
+
39
+ @staticmethod
40
+ def _add_dupe_debug_to_handler(handler: Handler):
41
+ dupe_debug_filter = DupeDebugFilter()
42
+ handler.addFilter(dupe_debug_filter)
43
+
44
+ def initialize_logger(self, logger=None, **kwargs) -> Union[Logger, '_EasyLoggerCustomLogger']:
45
+ self.logger = super().initialize_logger(logger=logger, **kwargs)
46
+ self.logger.propagate = False
47
+ return self.logger
48
+
49
+ def setup_email_handler(self, **kwargs):
50
+ """
51
+ Sets up the email handler for the logger using the OutlookEmailHandler.
52
+
53
+ :param kwargs: Keyword arguments to configure the email handler.
54
+ - email_msg: Specifies the email message content (default: None).
55
+ - logger_admins: Specifies the list of admin emails (default: None).
56
+ :return: None
57
+ :rtype: None
58
+ """
59
+ # noinspection PyTypeChecker
60
+ OutlookEmailHandler.VALID_EMAIL_MSG_TYPES = [Msg]
61
+ try:
62
+ # noinspection PyTypeChecker
63
+ email_handler = OutlookEmailHandler(email_msg=kwargs.get('email_msg', None),
64
+ project_name=self.project_name,
65
+ logger_dir_path=self.log_location,
66
+ recipient=kwargs.get('logger_admins', None))
67
+ except ValueError as e:
68
+ self.logger.error(e.args[0], exc_info=True)
69
+ raise e from None
70
+
71
+ email_handler.setLevel(ERROR)
72
+ email_handler.setFormatter(self.formatter)
73
+ self.logger.addHandler(email_handler)
74
+
75
+ def _add_filter_to_file_handler(self, handler: FileHandler):
76
+ self._add_dupe_debug_to_handler(handler)
77
+
78
+ def _add_filter_to_stream_handler(self, handler: StreamHandler):
79
+ self._add_dupe_debug_to_handler(handler)
80
+
81
+ @property
82
+ def project_name(self):
83
+ return super().project_name
84
+
85
+ @project_name.setter
86
+ def project_name(self, value):
87
+ if value is None:
88
+ value = __project_name__
89
+ super().__setattr__('_project_name', value)
90
+
91
+ def create_stream_handler(self, log_level_to_stream=WARNING, **kwargs):
92
+ stream_handler = kwargs.get('stream_handler_instance', StreamHandlerIgnoreExecInfo())
93
+ super().create_stream_handler(log_level_to_stream=log_level_to_stream,
94
+ stream_handler_instance=stream_handler, **kwargs)
@@ -0,0 +1,91 @@
1
+ from datetime import datetime
2
+ from logging import getLogger
3
+ from time import sleep
4
+ from typing import Union, Optional
5
+
6
+
7
+ class TheSandman:
8
+ """
9
+ A utility class to facilitate and log time delays with custom sleep durations.
10
+
11
+ Attributes:
12
+ DEFAULT_SLEEP_TIME_SECONDS (int): Default sleep duration in seconds if not specified.
13
+ sleep_time (int): Actual sleep time (in seconds) to be used by the instance.
14
+ sleep_time_string (str): Human-readable string representing the current sleep duration.
15
+ logger (Logger): Logging instance for logging messages related to sleep operations.
16
+
17
+ Methods:
18
+ sleep_time_string:
19
+ Property getter and setter for updating the human-readable sleep time string.
20
+
21
+ sleep_round():
22
+ Splits the sleep duration into two equal parts. It logs and prints messages depicting the current sleep state, including the remaining time, and sleeps for the given durations.
23
+ """
24
+ # default 600 secs = 10 minutes
25
+ DEFAULT_SLEEP_TIME_SECONDS = 600
26
+ DEFAULT_SNOOZE_EXPIRATION_LIMIT_HOURS = 24
27
+ SECONDS_IN_HOUR = 3600
28
+
29
+ def __init__(self, sleep_time_seconds=None, **kwargs):
30
+ self.snooze_expiration_limit_hours = kwargs.get('snooze_expiration_limit_hours',
31
+ self.__class__.DEFAULT_SNOOZE_EXPIRATION_LIMIT_HOURS)
32
+ self.sleep_time: int = sleep_time_seconds or self.__class__.DEFAULT_SLEEP_TIME_SECONDS
33
+ self._is_time_remaining = False
34
+ self._sleep_time_string = None
35
+ self.logger: getLogger = kwargs.get('logger', getLogger(__name__))
36
+ self.sleep_time_string = self.sleep_time
37
+ self.logger.info(f'TheSandman initialized - sleep time set as {self.sleep_time_string}')
38
+
39
+ @property
40
+ def sleep_time_string(self):
41
+ return self._sleep_time_string
42
+
43
+ @sleep_time_string.setter
44
+ def sleep_time_string(self, value: int):
45
+ if self._is_time_remaining:
46
+ more = 'more'
47
+ else:
48
+ more = ''
49
+ if value >= 60:
50
+ self._sleep_time_string = f'sleeping for {value // 60} {more} minute(s)'
51
+ else:
52
+ self._sleep_time_string = f'sleeping for {value} {more} second(s)'
53
+
54
+ def sleep(self, sleep_time_seconds: int, **kwargs):
55
+ """
56
+ :param sleep_time_seconds: The number of seconds the function should pause execution.
57
+ :type sleep_time_seconds: int
58
+ :return: None
59
+ :rtype: None
60
+ """
61
+
62
+ self.sleep_time_string = self.sleep_time if not self._is_time_remaining else sleep_time_seconds
63
+ self.logger.info(self.sleep_time_string, **kwargs)
64
+ sleep(sleep_time_seconds)
65
+
66
+ def sleep_in_rounds(self, rounds=2, **kwargs):
67
+ """
68
+ :param rounds: Number of intervals in which the total sleep time is divided. Defaults to 2.
69
+ :type rounds: int
70
+ :return: None
71
+ :rtype: None
72
+
73
+ """
74
+ dev_mode = kwargs.pop('dev_mode', True)
75
+ print_msg = kwargs.pop('print_msg', True)
76
+ self._is_time_remaining = False
77
+ for sr in range(rounds):
78
+ if sr == rounds - 1:
79
+ self._is_time_remaining = True
80
+ self.sleep((self.sleep_time // rounds), print_msg=print_msg, **kwargs)
81
+
82
+ @classmethod
83
+ def is_snooze_expired(cls, snoozed_at: datetime, snooze_expiration_limit_hours: Optional[int] = None):
84
+ if not snooze_expiration_limit_hours:
85
+ snooze_expiration_limit_hours = cls.DEFAULT_SNOOZE_EXPIRATION_LIMIT_HOURS
86
+ snooze_expiration_limit_seconds = snooze_expiration_limit_hours * cls.SECONDS_IN_HOUR
87
+ time_since_snooze = (datetime.now() - snoozed_at)
88
+ if time_since_snooze.total_seconds() >= snooze_expiration_limit_seconds:
89
+ #print('msg_snoozed expired! Unsnoozing now!')
90
+ return True
91
+ return False
@@ -0,0 +1,4 @@
1
+ from PyEmailerAJM.continuous_monitor.continuous_monitor import ContinuousMonitor
2
+ from PyEmailerAJM.continuous_monitor.continuous_monitor_alert_send import ContinuousMonitorAlertSend
3
+
4
+ __all__ = ['ContinuousMonitor', 'ContinuousMonitorAlertSend']
@@ -0,0 +1,6 @@
1
+ from PyEmailerAJM.continuous_monitor.backend.email_state import EmailState
2
+ from PyEmailerAJM.continuous_monitor.backend.continuous_colorizer import ContinuousColorizer
3
+ from PyEmailerAJM.continuous_monitor.backend.snooze_tracking import SnoozeTracking
4
+ from PyEmailerAJM.continuous_monitor.backend.continuous_monitor_base import ContinuousMonitorBase
5
+
6
+ __all__ = ['EmailState','ContinuousColorizer', 'SnoozeTracking', 'ContinuousMonitorBase']
@@ -0,0 +1,138 @@
1
+ from typing import Union, Tuple
2
+
3
+ from ColorizerAJM import Colorizer
4
+
5
+ from PyEmailerAJM.backend import AlertTypes
6
+
7
+
8
+ class ContinuousColorizer(Colorizer):
9
+ """
10
+ Class providing functionality to handle and apply color coding for alert messages.
11
+ It extends functionality from the parent `Colorizer` class,
12
+ adding customized color management, HTML-based color translation,
13
+ and support for critical warning, overdue, and other alerts.
14
+
15
+ Attributes:
16
+ LIGHT_GRAY: ANSI escape code for light gray colored text.
17
+ DARK_GRAY: ANSI escape code for dark gray colored text.
18
+ ORANGE: ANSI escape code for orange colored text.
19
+ DARK_YELLOW: ANSI escape code for dark yellow colored text.
20
+
21
+ Methods:
22
+ __init__(**kwargs):
23
+ Initialize the OverdueColorizer with optional custom colors and specific default color assignments.
24
+
25
+ get_alert_color(alert: 'AlertTypes') -> str:
26
+ Return the appropriate color for a given alert type.
27
+
28
+ _populate_html(unpopulated_html: str, text_for_population: str) -> str:
29
+ Populate a provided HTML template with the given text before the closing '</span>' tag.
30
+
31
+ colorize(text: str, color: str or None = None, bold: bool = False, **kwargs) -> str:
32
+ Apply colorization to the provided text using ANSI escape codes or HTML formatting.
33
+
34
+ get_color_code(color: Union[str, dict, int, Tuple[int, int, int]], **kwargs) -> str:
35
+ Retrieve the processed color code either in plain format or as HTML, based on the settings.
36
+
37
+ translate_color_to_html(color_code: str) -> str:
38
+ Transform a color code into an HTML-compliant string using a `<span>` element.
39
+ """
40
+ ORANGE = '\x1b[33m'
41
+ DARK_YELLOW = '\x1b[220m'
42
+
43
+ def __init__(self, **kwargs):
44
+ custom_colors = kwargs.pop('custom_colors', {})
45
+ custom_colors.update({'ORANGE': self.__class__.ORANGE,
46
+ # DARKGOLDENROD is the color name HTML will recognize
47
+ 'DARKGOLDENROD': self.__class__.DARK_YELLOW})
48
+ super().__init__(custom_colors=custom_colors, **kwargs)
49
+
50
+ self.overdue_color = self.get_color_code(self.__class__.RED, html_mode=True)
51
+ self.critical_warning_color = self.get_color_code(self.__class__.ORANGE, html_mode=True)
52
+ self.warning_color = self.get_color_code(self.__class__.DARK_YELLOW, html_mode=True)
53
+ self.other_color = self.get_color_code(self.__class__.WHITE, html_mode=True)
54
+
55
+ def get_alert_color(self, alert: 'AlertTypes'):
56
+ """
57
+ :param alert: The type of alert to determine the corresponding color.
58
+ :type alert: AlertTypes
59
+ :return: The color associated with the specified alert type.
60
+ :rtype: str
61
+ """
62
+ if alert == AlertTypes.WARNING:
63
+ return self.warning_color
64
+ elif alert == AlertTypes.CRITICAL_WARNING:
65
+ return self.critical_warning_color
66
+ elif alert == AlertTypes.OVERDUE:
67
+ return self.overdue_color
68
+ else:
69
+ return self.other_color
70
+
71
+ @staticmethod
72
+ def _populate_html(unpopulated_html: str, text_for_population: str):
73
+ """
74
+ :param unpopulated_html: HTML string that contains unpopulated sections marked by the closing '</span>' tag.
75
+ :type unpopulated_html: str
76
+ :param text_for_population: Text value to populate into the unpopulated HTML section before the closing '</span>' tag.
77
+ :type text_for_population: str
78
+ :return: A new HTML string with the text populated into the specified sections.
79
+ :rtype: str
80
+ """
81
+ return unpopulated_html.replace('</span>', f'{text_for_population}</span>')
82
+
83
+ def colorize(self, text, color=None, bold=False, **kwargs):
84
+ """
85
+ :param text: The text to be colorized.
86
+ :type text: str
87
+ :param color: The color to apply to the text. Defaults to None.
88
+ :type color: str or None
89
+ :param bold: A flag to indicate whether the text should be bold. Defaults to False.
90
+ :type bold: bool
91
+ :param kwargs: Additional optional parameters to control various aspects of the function. The 'html_mode' key can be used to specify whether HTML formatting is applied.
92
+ :return: The colorized text, optionally in HTML format if 'html_mode' is enabled in kwargs.
93
+ :rtype: str
94
+ """
95
+ html_mode = kwargs.get('html_mode', False)
96
+ if html_mode:
97
+ blank_html = self.translate_color_to_html(color)
98
+ colorized = self._populate_html(blank_html, text)
99
+ else:
100
+ colorized = super().colorize(text, color=color, bold=bold)
101
+ return colorized
102
+
103
+ def get_color_code(self, color: Union[str, dict, int, Tuple[int, int, int]], **kwargs) -> str:
104
+ """
105
+ :param color: The color input which can be a string, dictionary, integer, or a tuple representing RGB values.
106
+ :type color: Union[str, dict, int, Tuple[int, int, int]]
107
+ :param kwargs: Additional keyword arguments. Specifically, 'html_mode' can be passed to control whether the color code is translated to HTML.
108
+ :return: The processed color code as a string. If 'html_mode' is enabled, the color code is returned in HTML format, otherwise it's returned as is.
109
+ :rtype: str
110
+ """
111
+ html_mode = kwargs.get('html_mode', False)
112
+ if not color.startswith('\x1b') or not color.startswith('\033'):
113
+ cc = super().get_color_code(color)
114
+ else:
115
+ cc = color
116
+ if html_mode:
117
+ return self.translate_color_to_html(cc)
118
+ return cc
119
+
120
+ def translate_color_to_html(self, color_code: str):
121
+ """
122
+ Translate a specified color code to an HTML span element.
123
+
124
+ :param color_code: The color code in text representation. If the `color_code` starts with '<span',
125
+ the method attempts to map it to corresponding custom or default color codes.
126
+ If no mapping is found, an AttributeError is raised.
127
+ :type color_code: str
128
+ :return: The HTML span element string with the appropriate color style applied.
129
+ :rtype: str
130
+ """
131
+ if color_code.startswith('<span'):
132
+ new_color_code = [x[0] for x in {**self.custom_colors, **self.__class__.DEFAULT_COLOR_CODES}.items()
133
+ if x[1] == color_code.split('color:')[1].split('">')[0]]
134
+ if new_color_code:
135
+ color_code = new_color_code[0]
136
+ else:
137
+ raise AttributeError(f'Could not translate color code: {color_code}')
138
+ return f'<span style="color:{color_code}"></span>'
@@ -0,0 +1,114 @@
1
+ from abc import abstractmethod
2
+ from pathlib import Path
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ from PyEmailerAJM import PyEmailer, is_instance_of_dynamic
6
+ from PyEmailerAJM.backend import TheSandman
7
+ from . import ContinuousColorizer, SnoozeTracking, EmailState
8
+
9
+ if TYPE_CHECKING:
10
+ from PyEmailerAJM.backend import AlertTypes
11
+
12
+
13
+ class ContinuousMonitorBase(PyEmailer, EmailState):
14
+ """
15
+ Class ContinuousMonitorBase provides functionality to initialize and manage continuous monitoring
16
+ with optional email notifications. It extends the PyEmailer and EmailState classes and incorporates helper
17
+ classes for additional functionalities.
18
+
19
+ Attributes:
20
+ ADMIN_EMAIL_LOGGER (list): A list to store administrator email loggers.
21
+ ADMIN_EMAIL (list): A list to store administrator email addresses.
22
+ ATTRS_TO_CHECK (list): A list of class attributes to validate during subclass initialization.
23
+
24
+ Methods:
25
+ __init__(display_window: bool, send_emails: bool, **kwargs):
26
+ Initializes an instance of ContinuousMonitorBase, setting up logging, helper classes,
27
+ and initial email configurations. This also checks for a development mode and applies any specified
28
+ behavior accordingly.
29
+
30
+ __init_subclass__(cls, **kwargs):
31
+ Validates certain class attributes for subclasses by ensuring their presence
32
+ and that they are non-empty lists.
33
+
34
+ check_for_class_attrs(cls, class_attrs_to_check):
35
+ Validates a list of class attributes to ensure they are defined, are lists,
36
+ and contain email addresses.
37
+
38
+ initialize_helper_classes(self, **kwargs):
39
+ Sets up and returns instances of helper classes including ContinuousColorizer, SnoozeTracking,
40
+ and TheSandman, each initialized with parameters from **kwargs.
41
+
42
+ log_dev_mode_warnings(self):
43
+ Logs warnings if the `dev_mode` attribute is set to True.
44
+
45
+ email_handler_init(self):
46
+ Configures the email handler unless running in development mode. Provides appropriate logging
47
+ based on the current mode.
48
+ """
49
+ ADMIN_EMAIL_LOGGER = []
50
+ ADMIN_EMAIL = []
51
+ ATTRS_TO_CHECK = []
52
+
53
+ def __init__(self, display_window: bool, send_emails: bool, **kwargs):
54
+ super().__init__(display_window, send_emails, **kwargs)
55
+
56
+ self.dev_mode = kwargs.get('dev_mode', False)
57
+ self.colorizer, self.snooze_tracker, self.sleep_timer = self.initialize_helper_classes(**kwargs)
58
+
59
+ self.log_dev_mode_warnings()
60
+ self.email_handler_init()
61
+
62
+ @classmethod
63
+ def check_for_class_attrs(cls, class_attrs_to_check):
64
+ for c in class_attrs_to_check:
65
+ if hasattr(cls, c) and isinstance(getattr(cls, c), list) and len(getattr(cls, c)) > 0:
66
+ continue
67
+ raise ValueError(f"{c} must be a list of email addresses")
68
+
69
+ def initialize_helper_classes(self, **kwargs):
70
+ colorizer = ContinuousColorizer(logger=self.logger)
71
+ snooze_tracker = SnoozeTracking(Path(kwargs.get('file_name', './snooze_tracker.json')),
72
+ logger=kwargs.get('logger', self.logger))
73
+ sleep_timer = TheSandman(sleep_time_seconds=kwargs.get('sleep_time_seconds', None), logger=self.logger)
74
+ return colorizer, snooze_tracker, sleep_timer
75
+
76
+ def log_dev_mode_warnings(self):
77
+ if self.dev_mode:
78
+ self.logger.warning("DEV MODE ACTIVATED!")
79
+ self.logger.warning(
80
+ f"WARNING: this is a DEVELOPMENT MODE emailer,"
81
+ f" it will mock send emails but not actually send them to {self.__class__.ADMIN_EMAIL}"
82
+ )
83
+
84
+ def email_handler_init(self):
85
+ if self.dev_mode:
86
+ self.logger.warning("email handler disabled for dev mode")
87
+ elif (not type(self).__name__ == "ContinuousMonitorAlertSend"
88
+ and not is_instance_of_dynamic(self, "__main__.ContinuousMonitorAlertSend")):
89
+ self.logger.warning(
90
+ f"email handler not initialized because this is not a ContinuousMonitorAlertSend subclass"
91
+ )
92
+ else:
93
+ self._elog.setup_email_handler(email_msg=self.email,
94
+ logger_admins=self.__class__.ADMIN_EMAIL_LOGGER)
95
+ self.email = self.initialize_new_email()
96
+ self.logger.info("email handler initialized, initialized a new email object for use by monitor")
97
+
98
+ def _print_and_postprocess(self, alert_level):
99
+ """
100
+ :param alert_level: The level of alert to be logged and potentially emailed.
101
+ :type alert_level
102
+ :return: None
103
+ :rtype: None
104
+ """
105
+ if not self.dev_mode:
106
+ self.logger.info(f"{alert_level} found!", print_msg=True)
107
+ self._postprocess_alert(alert_level)
108
+ else:
109
+ self.logger.info(f"{alert_level} found!", print_msg=True)
110
+ self.logger.warning("IS DEV MODE - NOT postprocessing")
111
+
112
+ @abstractmethod
113
+ def _postprocess_alert(self, alert_level: Optional['AlertTypes'] = None, **kwargs):
114
+ ...