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.
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PKG-INFO +2 -2
- pyemailerajm-1.8.2/PyEmailerAJM/_version.py +1 -0
- pyemailerajm-1.8.2/PyEmailerAJM/backend/__init__.py +34 -0
- pyemailerajm-1.8.2/PyEmailerAJM/backend/enums.py +36 -0
- pyemailerajm-1.8.2/PyEmailerAJM/backend/errs.py +31 -0
- pyemailerajm-1.8.2/PyEmailerAJM/backend/logger.py +94 -0
- pyemailerajm-1.8.2/PyEmailerAJM/backend/the_sandman.py +91 -0
- pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/__init__.py +4 -0
- pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/backend/__init__.py +6 -0
- pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/backend/continuous_colorizer.py +138 -0
- pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/backend/continuous_monitor_base.py +114 -0
- pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/backend/email_state.py +127 -0
- pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/backend/snooze_tracking.py +172 -0
- pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/continuous_monitor.py +88 -0
- pyemailerajm-1.8.2/PyEmailerAJM/continuous_monitor/continuous_monitor_alert_send.py +120 -0
- pyemailerajm-1.8.2/PyEmailerAJM/msg/__init__.py +5 -0
- pyemailerajm-1.8.2/PyEmailerAJM/msg/alert_messages.py +252 -0
- pyemailerajm-1.8.2/PyEmailerAJM/msg/factory.py +75 -0
- pyemailerajm-1.8.2/PyEmailerAJM/msg/msg.py +253 -0
- pyemailerajm-1.8.2/PyEmailerAJM/searchers/__init__.py +3 -0
- pyemailerajm-1.8.2/PyEmailerAJM/searchers/searchers.py +137 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM.egg-info/PKG-INFO +2 -2
- pyemailerajm-1.8.2/PyEmailerAJM.egg-info/SOURCES.txt +34 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/setup.py +5 -2
- pyemailerajm-1.8/PyEmailerAJM/_version.py +0 -1
- pyemailerajm-1.8/PyEmailerAJM.egg-info/SOURCES.txt +0 -15
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/LICENSE.txt +0 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM/__init__.py +0 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM/py_emailer_ajm.py +0 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM.egg-info/dependency_links.txt +0 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM.egg-info/requires.txt +0 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/PyEmailerAJM.egg-info/top_level.txt +0 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/README.md +0 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/setup.cfg +0 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/tests/test_PyEmailerAJM.py +0 -0
- {pyemailerajm-1.8 → pyemailerajm-1.8.2}/tests/test_logger.py +0 -0
- {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,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
|
+
...
|