PyEmailerAJM 1.7__tar.gz → 1.8.1__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.7 → pyemailerajm-1.8.1}/PKG-INFO +4 -2
- pyemailerajm-1.8.1/PyEmailerAJM/__init__.py +33 -0
- pyemailerajm-1.8.1/PyEmailerAJM/_version.py +1 -0
- pyemailerajm-1.7/PyEmailerAJM/helpers.py → pyemailerajm-1.8.1/PyEmailerAJM/backend/__init__.py +10 -16
- pyemailerajm-1.8.1/PyEmailerAJM/backend/enums.py +36 -0
- pyemailerajm-1.8.1/PyEmailerAJM/backend/errs.py +31 -0
- pyemailerajm-1.8.1/PyEmailerAJM/backend/logger.py +94 -0
- pyemailerajm-1.8.1/PyEmailerAJM/backend/the_sandman.py +91 -0
- pyemailerajm-1.8.1/PyEmailerAJM/continuous_monitor/__init__.py +4 -0
- pyemailerajm-1.8.1/PyEmailerAJM/continuous_monitor/continuous_monitor.py +88 -0
- pyemailerajm-1.8.1/PyEmailerAJM/continuous_monitor/continuous_monitor_alert_send.py +120 -0
- pyemailerajm-1.8.1/PyEmailerAJM/msg/__init__.py +5 -0
- pyemailerajm-1.8.1/PyEmailerAJM/msg/alert_messages.py +252 -0
- pyemailerajm-1.8.1/PyEmailerAJM/msg/factory.py +75 -0
- pyemailerajm-1.8.1/PyEmailerAJM/msg/msg.py +253 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8.1}/PyEmailerAJM/py_emailer_ajm.py +66 -86
- pyemailerajm-1.8.1/PyEmailerAJM/searchers/__init__.py +3 -0
- pyemailerajm-1.8.1/PyEmailerAJM/searchers/searchers.py +137 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8.1}/PyEmailerAJM.egg-info/PKG-INFO +4 -2
- pyemailerajm-1.8.1/PyEmailerAJM.egg-info/SOURCES.txt +29 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8.1}/PyEmailerAJM.egg-info/requires.txt +2 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8.1}/setup.py +4 -2
- {pyemailerajm-1.7 → pyemailerajm-1.8.1}/tests/test_PyEmailerAJM.py +2 -0
- pyemailerajm-1.8.1/tests/test_logger.py +83 -0
- pyemailerajm-1.8.1/tests/test_snooze_tracking.py +93 -0
- pyemailerajm-1.7/PyEmailerAJM/__init__.py +0 -8
- pyemailerajm-1.7/PyEmailerAJM/_version.py +0 -1
- pyemailerajm-1.7/PyEmailerAJM.egg-info/SOURCES.txt +0 -14
- {pyemailerajm-1.7 → pyemailerajm-1.8.1}/LICENSE.txt +0 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8.1}/PyEmailerAJM.egg-info/dependency_links.txt +0 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8.1}/PyEmailerAJM.egg-info/top_level.txt +0 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8.1}/README.md +0 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8.1}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyEmailerAJM
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.1
|
|
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.
|
|
6
|
+
Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.8.1.tar.gz
|
|
7
7
|
Author: Amcsparron
|
|
8
8
|
Author-email: amcsparron@albanyny.gov
|
|
9
9
|
License: MIT License
|
|
@@ -13,6 +13,8 @@ Requires-Dist: pywin32
|
|
|
13
13
|
Requires-Dist: extract_msg
|
|
14
14
|
Requires-Dist: email_validator
|
|
15
15
|
Requires-Dist: questionary
|
|
16
|
+
Requires-Dist: EasyLoggerAJM
|
|
17
|
+
Requires-Dist: ColorizerAJM
|
|
16
18
|
Dynamic: author
|
|
17
19
|
Dynamic: author-email
|
|
18
20
|
Dynamic: download-url
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
# TODO: add these to other projects? and or EasyLogger?
|
|
4
|
+
__project_root__ = Path(__file__).parent.parent
|
|
5
|
+
__project_name__ = __project_root__.name
|
|
6
|
+
print(f'logs for {__project_name__} found in {__project_root__ / "logs"}')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_instance_of_dynamic(obj: object, base_class_path: str) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Check if an object is an instance of a class or its subclass specified by its module path.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
module_path, class_name = base_class_path.rsplit('.', 1)
|
|
15
|
+
module = import_module(module_path)
|
|
16
|
+
base_class = getattr(module, class_name)
|
|
17
|
+
return isinstance(obj, base_class)
|
|
18
|
+
except (ImportError, AttributeError):
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
from PyEmailerAJM.backend import deprecated
|
|
23
|
+
from PyEmailerAJM.backend.errs import EmailerNotSetupError, DisplayManualQuit
|
|
24
|
+
from PyEmailerAJM.msg import Msg, FailedMsg
|
|
25
|
+
from PyEmailerAJM.searchers import BaseSearcher, SubjectSearcher
|
|
26
|
+
from PyEmailerAJM.py_emailer_ajm import PyEmailer, EmailerInitializer
|
|
27
|
+
from PyEmailerAJM.continuous_monitor.continuous_monitor import ContinuousMonitor
|
|
28
|
+
|
|
29
|
+
__all__ = ['EmailerNotSetupError', 'DisplayManualQuit', 'deprecated',
|
|
30
|
+
'Msg', 'FailedMsg', 'PyEmailer', 'EmailerInitializer',
|
|
31
|
+
'BaseSearcher', 'SubjectSearcher', 'ContinuousMonitor',
|
|
32
|
+
'__project_root__', '__project_name__', 'is_instance_of_dynamic']
|
|
33
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.8.1'
|
pyemailerajm-1.7/PyEmailerAJM/helpers.py → pyemailerajm-1.8.1/PyEmailerAJM/backend/__init__.py
RENAMED
|
@@ -1,21 +1,9 @@
|
|
|
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
|
|
1
5
|
import warnings
|
|
2
6
|
import functools
|
|
3
|
-
from enum import IntEnum
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class BasicEmailFolderChoices(IntEnum):
|
|
7
|
-
INBOX = 6
|
|
8
|
-
SENT_ITEMS = 5
|
|
9
|
-
DRAFTS = 16
|
|
10
|
-
DELETED_ITEMS = 3
|
|
11
|
-
OUTBOX = 4
|
|
12
|
-
|
|
13
|
-
def __str__(self):
|
|
14
|
-
"""Return the enum name as a string."""
|
|
15
|
-
return self.name
|
|
16
|
-
|
|
17
|
-
def __repr__(self):
|
|
18
|
-
return f"<{self.__class__.__name__}.{self.name} ({self.value})>"
|
|
19
7
|
|
|
20
8
|
|
|
21
9
|
def deprecated(reason: str = ""):
|
|
@@ -38,3 +26,9 @@ def deprecated(reason: str = ""):
|
|
|
38
26
|
return wrapper
|
|
39
27
|
|
|
40
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,88 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from PyEmailerAJM.backend import AlertTypes
|
|
5
|
+
from PyEmailerAJM.continuous_monitor.backend.continuous_monitor_base import ContinuousMonitorBase
|
|
6
|
+
from PyEmailerAJM.msg import MsgFactory
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContinuousMonitor(ContinuousMonitorBase):
|
|
10
|
+
TITLE_STRING = " Watching for emails with alerts in {} folder ".center(100, '*')
|
|
11
|
+
MSG_FACTORY_CLASS: MsgFactory = MsgFactory
|
|
12
|
+
|
|
13
|
+
def GetMessages(self, folder_index=None):
|
|
14
|
+
"""
|
|
15
|
+
:param folder_index: Index of the folder from which messages are retrieved. Defaults to None if not specified.
|
|
16
|
+
:type folder_index: int, optional
|
|
17
|
+
:return: A list of sorted and filtered message objects, each containing an alert.
|
|
18
|
+
:rtype: list
|
|
19
|
+
"""
|
|
20
|
+
msgs = super().GetMessages(folder_index)
|
|
21
|
+
sorted_msgs = [self.__class__.MSG_FACTORY_CLASS.get_msg(x, logger=self.logger, snooze_checker=self.snooze_tracker) for x in msgs]
|
|
22
|
+
alert_messages = [x for x in sorted_msgs if x is not None and x.msg_alert]
|
|
23
|
+
return alert_messages
|
|
24
|
+
|
|
25
|
+
def _set_args_for_endless_watch(self):
|
|
26
|
+
"""
|
|
27
|
+
Sets specific arguments for the endless_watch process.
|
|
28
|
+
|
|
29
|
+
:return: None
|
|
30
|
+
:rtype: None
|
|
31
|
+
"""
|
|
32
|
+
self.send_emails = False
|
|
33
|
+
self.auto_send = False
|
|
34
|
+
self.display_window = False
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def _postprocess_alert(self, alert_level=None, **kwargs):
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def check_for_alerts(self):
|
|
41
|
+
"""
|
|
42
|
+
Checks for emails in the specified folder and identifies if there are any alerts. Alerts,
|
|
43
|
+
if present, are categorized as overdue, warning, or critical warning, and are processed accordingly.
|
|
44
|
+
Then logs the result of the check.
|
|
45
|
+
|
|
46
|
+
:return: None
|
|
47
|
+
:rtype: None
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
self.logger.info("\nChecking for emails with an alert...", print_msg=True)
|
|
51
|
+
self.refresh_messages()
|
|
52
|
+
if self.has_overdue:
|
|
53
|
+
self._print_and_postprocess(AlertTypes.OVERDUE)
|
|
54
|
+
|
|
55
|
+
elif self.has_warning:
|
|
56
|
+
self._print_and_postprocess(AlertTypes.WARNING)
|
|
57
|
+
|
|
58
|
+
elif self.has_critical_warning:
|
|
59
|
+
self._print_and_postprocess(AlertTypes.CRITICAL_WARNING)
|
|
60
|
+
|
|
61
|
+
else:
|
|
62
|
+
self.logger.info(f"No emails with an alert detected in {self.read_folder}", print_msg=True)
|
|
63
|
+
|
|
64
|
+
self.snooze_tracker.snooze_msgs(self.all_messages)
|
|
65
|
+
|
|
66
|
+
def endless_watch(self, stop_condition: Callable[[], bool] = None):
|
|
67
|
+
if not self.dev_mode:
|
|
68
|
+
self._set_args_for_endless_watch()
|
|
69
|
+
|
|
70
|
+
stop_condition = stop_condition or (lambda: False) # Default stop_condition
|
|
71
|
+
email_dir_name = self.read_folder.name if self.read_folder else None
|
|
72
|
+
|
|
73
|
+
self.logger.info(self.__class__.TITLE_STRING.format(email_dir_name), print_msg=True)
|
|
74
|
+
|
|
75
|
+
while not stop_condition():
|
|
76
|
+
try:
|
|
77
|
+
self.check_for_alerts()
|
|
78
|
+
self._was_refreshed = False
|
|
79
|
+
self.sleep_timer.sleep_in_rounds()
|
|
80
|
+
except KeyboardInterrupt:
|
|
81
|
+
self.logger.error("KeyboardInterrupt detected, exiting program.")
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == '__main__':
|
|
86
|
+
ContinuousMonitor.MSG_FACTORY_CLASS.ALERT_SUBJECT_KEYWORDS = ['training']
|
|
87
|
+
cm = ContinuousMonitor(False, False, dev_mode=False, show_warning_logs_in_console=True, )
|
|
88
|
+
cm.endless_watch()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from PyEmailerAJM.continuous_monitor import ContinuousMonitor
|
|
4
|
+
|
|
5
|
+
NO_COLORIZER = False
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ContinuousMonitorAlertSend(ContinuousMonitor):
|
|
9
|
+
ADMIN_EMAIL_LOGGER = []
|
|
10
|
+
ADMIN_EMAIL = []
|
|
11
|
+
DEFAULT_SUBJECT = "Email Alert"
|
|
12
|
+
DEFAULT_MSG_BODY = ("Dear {admin_email_names},\n\n"
|
|
13
|
+
"There is an Email in the inbox that has an alert ({msg_tuple}). \n\n"
|
|
14
|
+
"Thanks,\n"
|
|
15
|
+
"{email_sender}")
|
|
16
|
+
ATTRS_TO_CHECK = ['ADMIN_EMAIL', 'ADMIN_EMAIL_LOGGER']
|
|
17
|
+
|
|
18
|
+
def __init__(self, display_window: bool, send_emails: bool, **kwargs):
|
|
19
|
+
|
|
20
|
+
super().__init__(display_window, send_emails, **kwargs)
|
|
21
|
+
if not self.dev_mode:
|
|
22
|
+
if type(self) is ContinuousMonitorAlertSend:
|
|
23
|
+
self.__class__.check_for_class_attrs(self.__class__.ATTRS_TO_CHECK)
|
|
24
|
+
else:
|
|
25
|
+
self.logger.warning(f"IS DEV MODE - NOT checking for class attributes "
|
|
26
|
+
f"({', '.join(self.__class__.ATTRS_TO_CHECK)}) for ContinuousMonitorAlertSend")
|
|
27
|
+
|
|
28
|
+
def __init_subclass__(cls, **kwargs):
|
|
29
|
+
cls.check_for_class_attrs(cls.ATTRS_TO_CHECK)
|
|
30
|
+
|
|
31
|
+
def _set_args_for_endless_watch(self):
|
|
32
|
+
self.send_emails = True
|
|
33
|
+
self.auto_send = True
|
|
34
|
+
self.display_window = False
|
|
35
|
+
self.logger.debug("send_emails, auto_send, and display_window set to True for endless_watch()")
|
|
36
|
+
|
|
37
|
+
def SetupEmail(self, recipient: Optional[str] = None, subject: str = DEFAULT_SUBJECT,
|
|
38
|
+
text: str = None, attachments: list = None, **kwargs):
|
|
39
|
+
"""
|
|
40
|
+
:param recipient: Email recipient(s). If not provided, defaults to ADMIN_EMAIL or a semicolon-separated string of recipients in case of a list.
|
|
41
|
+
:type recipient: Optional[str]
|
|
42
|
+
:param subject: Subject of the email. Defaults to DEFAULT_SUBJECT.
|
|
43
|
+
:type subject: str
|
|
44
|
+
:param text: Body text of the email. If not provided, defaults to the response_body attribute.
|
|
45
|
+
:type text: str
|
|
46
|
+
:param attachments: A list of attachments to include in the email.
|
|
47
|
+
:type attachments: list
|
|
48
|
+
:param kwargs: Additional keyword arguments passed to the parent SetupEmail method.
|
|
49
|
+
:type kwargs: dict
|
|
50
|
+
:return: The resulting email setup performed by the superclass's SetupEmail method.
|
|
51
|
+
:rtype: Any
|
|
52
|
+
"""
|
|
53
|
+
if not recipient:
|
|
54
|
+
recipient = self.__class__.ADMIN_EMAIL
|
|
55
|
+
if isinstance(recipient, list):
|
|
56
|
+
recipient = ' ;'.join(recipient)
|
|
57
|
+
if not text:
|
|
58
|
+
text = self.response_body
|
|
59
|
+
return super().SetupEmail(recipient=recipient, subject=subject,
|
|
60
|
+
text=text, attachments=attachments, **kwargs)
|
|
61
|
+
|
|
62
|
+
def get_response_body_alert_level(self, msg: '_AlertMsgBase'):
|
|
63
|
+
"""
|
|
64
|
+
:param msg: The message object which contains the alert level information.
|
|
65
|
+
:type msg: _AlertMsgBase
|
|
66
|
+
:return: The alert level string, optionally colorized if coloring is enabled.
|
|
67
|
+
:rtype: str
|
|
68
|
+
"""
|
|
69
|
+
if NO_COLORIZER:
|
|
70
|
+
self.logger.debug("colorizer not available, using plain text for alert level")
|
|
71
|
+
rb_alert_string = msg.__class__.ALERT_LEVEL.name
|
|
72
|
+
else:
|
|
73
|
+
self.logger.debug("colorizer available, using colorized alert level")
|
|
74
|
+
color = self.colorizer.get_alert_color(msg.__class__.ALERT_LEVEL)
|
|
75
|
+
rb_alert_string = self.colorizer.colorize(msg.__class__.ALERT_LEVEL.name,
|
|
76
|
+
color=color,
|
|
77
|
+
html_mode=True)
|
|
78
|
+
return rb_alert_string
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def email_signature(self):
|
|
82
|
+
return ('<br>'.join(super().email_signature.split('\n'))
|
|
83
|
+
if super().email_signature is not None else None)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def response_body(self):
|
|
87
|
+
"""
|
|
88
|
+
Processes and formats the response body by compiling alert messages and their corresponding alert levels,
|
|
89
|
+
then generating a formatted string containing a summary of these messages.
|
|
90
|
+
|
|
91
|
+
:return: Processed and formatted response body string
|
|
92
|
+
:rtype: str
|
|
93
|
+
"""
|
|
94
|
+
alert_msgs = [(x.subject, self.get_response_body_alert_level(x)) for x in self.GetMessages()]
|
|
95
|
+
msg_tuple = ', '.join([' - '.join(x) for x in alert_msgs])
|
|
96
|
+
formatted_admin_email_names = ', '.join([x.split('@')[0] for
|
|
97
|
+
x in self.__class__.ADMIN_EMAIL]
|
|
98
|
+
).replace('\n', '<br>')
|
|
99
|
+
formatted_full_body = self.__class__.DEFAULT_MSG_BODY.format(email_sender=self.email_signature,
|
|
100
|
+
msg_tuple=msg_tuple,
|
|
101
|
+
admin_email_names=formatted_admin_email_names
|
|
102
|
+
).replace('\n', '<br>')
|
|
103
|
+
return formatted_full_body
|
|
104
|
+
|
|
105
|
+
def _postprocess_alert(self, alert_level=None, **kwargs):
|
|
106
|
+
self.SendOrDisplay(**kwargs)
|
|
107
|
+
|
|
108
|
+
def refresh_messages(self):
|
|
109
|
+
self.SetupEmail()
|
|
110
|
+
super().refresh_messages()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == '__main__':
|
|
114
|
+
ContinuousMonitorAlertSend.MSG_FACTORY_CLASS.ALERT_SUBJECT_KEYWORDS = ['training']
|
|
115
|
+
ContinuousMonitorAlertSend.ADMIN_EMAIL = ['amcsparron@albanyny.gov']
|
|
116
|
+
ContinuousMonitorAlertSend.ADMIN_EMAIL_LOGGER = ContinuousMonitorAlertSend.ADMIN_EMAIL
|
|
117
|
+
cm = ContinuousMonitorAlertSend(False, False,
|
|
118
|
+
dev_mode=False,
|
|
119
|
+
show_warning_logs_in_console=True) #, email_sig_filename='Andrew Full.txt')
|
|
120
|
+
cm.endless_watch()
|