PyEmailerAJM 1.7__tar.gz → 1.8__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.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyEmailerAJM
3
- Version: 1.7
3
+ Version: 1.8
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.7.tar.gz
6
+ Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.8.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'
@@ -15,7 +15,7 @@ import win32com.client as win32
15
15
  # This is installed as part of pywin32
16
16
  # noinspection PyUnresolvedReferences
17
17
  from pythoncom import com_error
18
- from logging import Logger, basicConfig, StreamHandler, FileHandler
18
+ from logging import Logger, basicConfig, StreamHandler, FileHandler, getLogger
19
19
  from email_validator import validate_email, EmailNotValidError
20
20
  import questionary
21
21
  # this is usually thrown when questionary is used in the dev/Non Win32 environment
@@ -25,7 +25,7 @@ from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
25
25
  from PyEmailerAJM import (EmailerNotSetupError, DisplayManualQuit,
26
26
  deprecated,
27
27
  Msg, FailedMsg)
28
- from PyEmailerAJM.backend import BasicEmailFolderChoices
28
+ from PyEmailerAJM.backend import BasicEmailFolderChoices, PyEmailerLogger
29
29
  from PyEmailerAJM.searchers import SubjectSearcher
30
30
 
31
31
 
@@ -38,8 +38,11 @@ class EmailerInitializer:
38
38
  auto_send: bool = False,
39
39
  email_app_name: str = DEFAULT_EMAIL_APP_NAME,
40
40
  namespace_name: str = DEFAULT_NAMESPACE_NAME, **kwargs):
41
-
42
- self._logger = self._initialize_logger(logger, use_default_logger=kwargs.get('use_default_logger', False))
41
+ if logger:
42
+ self.logger = logger
43
+ else:
44
+ self._elog = PyEmailerLogger(**kwargs)
45
+ self.logger = self._elog()
43
46
  # print("Dummy logger in use!")
44
47
 
45
48
  self.email_app_name = email_app_name
@@ -51,54 +54,9 @@ class EmailerInitializer:
51
54
  self.auto_send = auto_send
52
55
  self.send_emails = send_emails
53
56
 
54
- # TODO: replace me with EasyLoggerAJM
55
- def _initialize_logger(self, logger=None, **kwargs):
56
- if logger:
57
- self._logger = logger
58
- return self._logger
59
- else:
60
- self._logger = Logger(__name__)
61
-
62
- if self._logger.hasHandlers():
63
- return self._logger
64
- if not kwargs.get('use_default_logger', True):
65
- print("not using default logger")
66
- return self._logger
67
- return self._initialize_default_logger()
68
-
69
- def _initialize_default_logger(self, **kwargs):
70
- def init_handlers():
71
- sh = StreamHandler()
72
- sh.set_name('StreamHandler')
73
- fh = FileHandler(kwargs.get('log_file_path', join('./', 'PyEmailer.log')))
74
- fh.set_name('FileHandler')
75
- return sh, fh
76
-
77
- def set_handler_levels(**kw):
78
- fh_level = kw.get('FileHandler_level', 'DEBUG')
79
- sh_level = kw.get('StreamHandler_level', 'INFO')
80
- for h in self._logger.handlers:
81
- if isinstance(h, FileHandler):
82
- h.setLevel(fh_level)
83
- elif isinstance(h, StreamHandler):
84
- h.setLevel(sh_level)
85
- else:
86
- h.setLevel(kw.get('handler_default_level', 'DEBUG'))
87
-
88
- stream_handle, file_handle = init_handlers()
89
-
90
- if kwargs.get('log_to_stdout', True):
91
- self._logger.addHandler(stream_handle)
92
- self._logger.addHandler(file_handle)
93
- set_handler_levels(**kwargs)
94
-
95
- basicConfig(level='INFO', handlers=self._logger.handlers)
96
- self._logger.info("basic logger initialized.")
97
- return self._logger
98
-
99
57
  def initialize_new_email(self):
100
58
  if hasattr(self, 'email_app') and self.email_app is not None:
101
- self.email = Msg(self.email_app.CreateItem(0), logger=self._logger)
59
+ self.email = Msg(self.email_app.CreateItem(0), logger=self.logger)
102
60
  return self.email
103
61
  raise AttributeError("email_app is not defined. Run 'initialize_email_item_app_and_namespace' first")
104
62
 
@@ -107,17 +65,17 @@ class EmailerInitializer:
107
65
  email_app, namespace = self._setup_email_app_and_namespace()
108
66
  email = self.initialize_new_email()
109
67
  except com_error as e:
110
- self._logger.error(e, exc_info=True)
68
+ self.logger.error(e, exc_info=True)
111
69
  raise e
112
70
  return email_app, namespace, email
113
71
 
114
72
  def _setup_email_app_and_namespace(self):
115
73
  self.email_app = win32.Dispatch(self.email_app_name)
116
74
 
117
- self._logger.debug(f"{self.email_app_name} app in use.")
75
+ self.logger.debug(f"{self.email_app_name} app in use.")
118
76
  self.namespace = self.email_app.GetNamespace(self.namespace_name)
119
77
 
120
- self._logger.debug(f"{self.namespace_name} namespace in use.")
78
+ self.logger.debug(f"{self.namespace_name} namespace in use.")
121
79
  return self.email_app, self.namespace
122
80
 
123
81
 
@@ -160,7 +118,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
160
118
  if validate_email(value, check_deliverability=False):
161
119
  self._current_user_email = value
162
120
  except EmailNotValidError as e:
163
- self._logger.error(e, exc_info=True)
121
+ self.logger.error(e, exc_info=True)
164
122
  value = None
165
123
  self._current_user_email = value
166
124
 
@@ -178,7 +136,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
178
136
  try:
179
137
  raise NotADirectoryError(f"{self.signature_dir_path} does not exist.")
180
138
  except NotADirectoryError as e:
181
- self._logger.warning(e)
139
+ self.logger.warning(e)
182
140
  self._email_signature = None
183
141
 
184
142
  if isfile(signature_full_path):
@@ -188,10 +146,11 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
188
146
  try:
189
147
  raise FileNotFoundError(f"{signature_full_path} does not exist.")
190
148
  except FileNotFoundError as e:
191
- self._logger.warning(e)
149
+ self.logger.warning(e)
192
150
  self._email_signature = None
193
151
  else:
194
152
  self._email_signature = None
153
+ self.logger.info("email_sig_filename not specified, no email signature will be attached.")
195
154
 
196
155
  return self._email_signature
197
156
 
@@ -212,12 +171,12 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
212
171
  except Exception as e:
213
172
  # TODO: slated for removal
214
173
  # this is here purely as a compatibility thing, to be taken out later.
215
- self._logger.warning(e)
216
- self._logger.warning("Defaulting to basic y/n prompt.")
174
+ self.logger.warning(e)
175
+ self.logger.warning("Defaulting to basic y/n prompt.")
217
176
  while True:
218
177
  q = input(f"{self.DisplayEmailSendTrackingWarning}. Do you understand? (y/n): ").lower().strip()
219
178
  if q == 'y':
220
- self._logger.warning(self.DisplayEmailSendTrackingWarning)
179
+ self.logger.warning(self.DisplayEmailSendTrackingWarning)
221
180
  return True
222
181
  elif q == 'n':
223
182
  return False
@@ -233,10 +192,10 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
233
192
  try:
234
193
  raise DisplayManualQuit("User cancelled operation due to DisplayTrackingWarning.")
235
194
  except DisplayManualQuit as e:
236
- self._logger.error(e, exc_info=True)
195
+ self.logger.error(e, exc_info=True)
237
196
  raise e
238
197
 
239
- def _GetReadFolder(self, email_dir_index: int = BasicEmailFolderChoices.INBOX):
198
+ def _get_default_folder_for_email_dir(self, email_dir_index: int = None, **kwargs):
240
199
  # 6 = inbox
241
200
  if email_dir_index in self.__class__.VALID_EMAIL_FOLDER_CHOICES:
242
201
  self.read_folder = self.namespace.GetDefaultFolder(email_dir_index)
@@ -245,9 +204,30 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
245
204
  try:
246
205
  raise ValueError(f"email_dir_index must be one of {self.__class__.VALID_EMAIL_FOLDER_CHOICES}")
247
206
  except ValueError as e:
248
- self._logger.error(e, exc_info=True)
207
+ self.logger.error(e, exc_info=True)
249
208
  raise e
250
209
 
210
+ def _GetReadFolder(self, email_dir_index: int = None, **kwargs):
211
+ """
212
+ :param email_dir_index: Specifies the email directory index to be accessed. Defaults to None.
213
+ :type email_dir_index: int, optional
214
+ :param kwargs: Additional optional arguments that may be passed. Can include `subfolder_name` to specify a subfolder name, defaulting to 'Inbox'.
215
+ :type kwargs: dict
216
+ :return: The folder specified either by the email directory index or the default folder along with the subfolder if applicable.
217
+ :rtype: object
218
+ """
219
+ subfolder_name = kwargs.get('subfolder_name', 'Inbox')
220
+ if not email_dir_index:
221
+ email_dir_index = BasicEmailFolderChoices.INBOX
222
+ self.logger.debug(f">>> email_dir_index not specified, defaulting to '{email_dir_index}' folder. <<<")
223
+ if not isinstance(email_dir_index, int):
224
+ self.logger.debug(f">>> email_dir_index is not an int, "
225
+ f"defaulting to {email_dir_index} folder and {subfolder_name} subfolder. <<<")
226
+ return self.namespace.Folders[email_dir_index].Folders[subfolder_name]
227
+
228
+ else:
229
+ return self._get_default_folder_for_email_dir(email_dir_index)
230
+
251
231
  def GetMessages(self, folder_index=None):
252
232
  if isinstance(folder_index, int):
253
233
  self.read_folder = self._GetReadFolder(folder_index)
@@ -259,9 +239,9 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
259
239
  try:
260
240
  raise TypeError("folder_index must be an integer or self.read_folder must be defined")
261
241
  except TypeError as e:
262
- self._logger.error(e, exc_info=True)
242
+ self.logger.error(e, exc_info=True)
263
243
  raise e
264
- return [Msg(m, logger=self._logger) for m in self.read_folder.Items]
244
+ return [Msg(m, logger=self.logger) for m in self.read_folder.Items]
265
245
 
266
246
  @deprecated("use Msg classes body attribute instead")
267
247
  def GetEmailMessageBody(self, msg):
@@ -273,7 +253,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
273
253
  try:
274
254
  raise ValueError("This message has no body.")
275
255
  except ValueError as e:
276
- self._logger.error(e, exc_info=True)
256
+ self.logger.error(e, exc_info=True)
277
257
  raise e
278
258
 
279
259
  @deprecated("use find_messages_by_subject instead")
@@ -289,15 +269,15 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
289
269
  full_save_path = join(save_dir_path, str(attachment))
290
270
  try:
291
271
  attachment.SaveAsFile(full_save_path)
292
- self._logger.debug(f"{full_save_path} saved from email with subject {msg.subject}")
272
+ self.logger.debug(f"{full_save_path} saved from email with subject {msg.subject}")
293
273
  except Exception as e:
294
- self._logger.error(e, exc_info=True)
274
+ self.logger.error(e, exc_info=True)
295
275
  raise e
296
276
 
297
277
  def SetupEmail(self, recipient: str, subject: str, text: str, attachments: list = None, **kwargs):
298
278
  self.email = self.email.SetupMsg(sender=self.current_user_email, email_item=self.email(),
299
279
  recipient=recipient, subject=subject, body=text, attachments=attachments,
300
- logger=self._logger, **kwargs)
280
+ logger=self.logger, **kwargs)
301
281
  self._setup_was_run = True
302
282
  return self.email
303
283
 
@@ -308,34 +288,34 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
308
288
  self.email.send()
309
289
  return
310
290
  elif not send:
311
- self._logger.info(f"Mail not sent to {self.email.to}")
291
+ self.logger.info(f"Mail not sent to {self.email.to}")
312
292
  print(f"Mail not sent to {self.email.to}")
313
293
  q = questionary.confirm("do you want to quit early?", default=False).ask()
314
294
  if q:
315
295
  print("ok quitting!")
316
- self._logger.warning("Quitting early due to user input.")
296
+ self.logger.warning("Quitting early due to user input.")
317
297
  exit(-1)
318
298
  else:
319
299
  return
320
300
  except com_error as e:
321
- self._logger.error(e, exc_info=True)
301
+ self.logger.error(e, exc_info=True)
322
302
  except NoConsoleScreenBufferError as e:
323
303
  # TODO: slated for removal
324
304
  # this is here purely as a compatibility thing, to be taken out later.
325
- self._logger.warning(e)
326
- self._logger.warning("defaulting to basic input style...")
305
+ self.logger.warning(e)
306
+ self.logger.warning("defaulting to basic input style...")
327
307
  while True:
328
308
  yn = input("Send Mail? (y/n/q): ").lower()
329
309
  if yn == 'y':
330
310
  self.email.send()
331
311
  break
332
312
  elif yn == 'n':
333
- self._logger.info(f"Mail not sent to {self.email.to}")
313
+ self.logger.info(f"Mail not sent to {self.email.to}")
334
314
  print(f"Mail not sent to {self.email.to}")
335
315
  break
336
316
  elif yn == 'q':
337
317
  print("ok quitting!")
338
- self._logger.warning("Quitting early due to user input.")
318
+ self.logger.warning("Quitting early due to user input.")
339
319
  exit(-1)
340
320
  else:
341
321
  print("Please choose \'y\', \'n\' or \'q\'")
@@ -344,18 +324,18 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
344
324
  if self._setup_was_run:
345
325
  if print_ready_msg:
346
326
  print(f"Ready to send/display mail to/for {self.email.to}...")
347
- self._logger.info(f"Ready to send/display mail to/for {self.email.to}...")
327
+ self.logger.info(f"Ready to send/display mail to/for {self.email.to}...")
348
328
  if self.send_emails and self.display_window:
349
329
  send_and_display_warning = ("Sending email while also displaying the email "
350
330
  "in the app is not possible. Defaulting to Display only")
351
331
  # print(send_and_display_warning)
352
- self._logger.warning(send_and_display_warning)
332
+ self.logger.warning(send_and_display_warning)
353
333
  self.send_emails = False
354
334
  self.display_window = True
355
335
 
356
336
  if self.send_emails:
357
337
  if self.auto_send:
358
- self._logger.info("Sending emails with auto_send...")
338
+ self.logger.info("Sending emails with auto_send...")
359
339
  self.email.send()
360
340
 
361
341
  else:
@@ -366,13 +346,13 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
366
346
  else:
367
347
  both_disabled_warning = ("Both sending and displaying the email are disabled. "
368
348
  "No errors were encountered.")
369
- self._logger.warning(both_disabled_warning)
349
+ self.logger.warning(both_disabled_warning)
370
350
  # print(both_disabled_warning)
371
351
  else:
372
352
  try:
373
353
  raise EmailerNotSetupError("Setup has not been run, sending or displaying an email cannot occur.")
374
354
  except EmailerNotSetupError as e:
375
- self._logger.error(e, exc_info=True)
355
+ self.logger.error(e, exc_info=True)
376
356
  raise e
377
357
 
378
358
  @staticmethod
@@ -389,8 +369,8 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
389
369
 
390
370
  if msg_candidates:
391
371
  msg_candidates = [FailedMsg(m) for m in msg_candidates]
392
- self._logger.info(f"{len(msg_candidates)} 'failed send' candidates found.")
393
- self._logger.info("mutating msg_candidates (Msg instances) into FailedMsg instances.")
372
+ self.logger.info(f"{len(msg_candidates)} 'failed send' candidates found.")
373
+ self.logger.info("mutating msg_candidates (Msg instances) into FailedMsg instances.")
394
374
 
395
375
  for m in msg_candidates:
396
376
  failed_info = m.process_failed_msg(m(), recent_days_cap=recent_days_cap)
@@ -402,10 +382,10 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
402
382
  'err_info': failed_info})
403
383
  results_string = self.__class__.FAILED_SEND_LOGGER_STRING.format(num=len(failed_sends),
404
384
  recent_days_cap=recent_days_cap)
405
- if (not self._logger.hasHandlers() or not any([isinstance(x, StreamHandler)
406
- for x in self._logger.handlers])):
385
+ if (not self.logger.hasHandlers() or not any([isinstance(x, StreamHandler)
386
+ for x in self.logger.handlers])):
407
387
  print(results_string)
408
- self._logger.info(results_string)
388
+ self.logger.info(results_string)
409
389
  return failed_sends
410
390
 
411
391
 
@@ -428,7 +408,7 @@ def __setup_and_send_test(emailer):
428
408
  if __name__ == "__main__":
429
409
  module_name = __file__.split('\\')[-1].split('.py')[0]
430
410
 
431
- em = PyEmailer(display_window=False, send_emails=True, auto_send=False, use_default_logger=True)
411
+ em = PyEmailer(display_window=False, send_emails=True, auto_send=False, use_default_logger=False)
432
412
  m = em.find_messages_by_subject('Andrew', partial_match_ok=True)
433
413
  print([type(x) for x in m])
434
414
  # __setup_and_send_test(em)
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyEmailerAJM
3
- Version: 1.7
3
+ Version: 1.8
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.7.tar.gz
6
+ Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.8.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
@@ -4,11 +4,12 @@ setup.cfg
4
4
  setup.py
5
5
  PyEmailerAJM/__init__.py
6
6
  PyEmailerAJM/_version.py
7
- PyEmailerAJM/helpers.py
8
7
  PyEmailerAJM/py_emailer_ajm.py
9
8
  PyEmailerAJM.egg-info/PKG-INFO
10
9
  PyEmailerAJM.egg-info/SOURCES.txt
11
10
  PyEmailerAJM.egg-info/dependency_links.txt
12
11
  PyEmailerAJM.egg-info/requires.txt
13
12
  PyEmailerAJM.egg-info/top_level.txt
14
- tests/test_PyEmailerAJM.py
13
+ tests/test_PyEmailerAJM.py
14
+ tests/test_logger.py
15
+ tests/test_snooze_tracking.py
@@ -2,3 +2,5 @@ pywin32
2
2
  extract_msg
3
3
  email_validator
4
4
  questionary
5
+ EasyLoggerAJM
6
+ ColorizerAJM
@@ -17,7 +17,7 @@ setup(
17
17
  url='https://github.com/amcsparron2793-Water/PyEmailer',
18
18
  download_url=f'https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/{get_property("__version__", project_name)}.tar.gz',
19
19
  keywords=["Outlook", "Email", "Automation"],
20
- install_requires=['pywin32', 'extract_msg', 'email_validator', 'questionary'],
20
+ install_requires=['pywin32', 'extract_msg', 'email_validator', 'questionary','EasyLoggerAJM', 'ColorizerAJM'],
21
21
  license='MIT License',
22
22
  author='Amcsparron',
23
23
  author_email='amcsparron@albanyny.gov',
@@ -7,8 +7,10 @@ from PyEmailerAJM import PyEmailer, Msg
7
7
 
8
8
  class TestPyEmailer(unittest.TestCase):
9
9
  TEST_ATTACHMENT_NAMES = ['attachment1', 'attachment2']
10
+ TEST_ADMIN_EMAIL = ['example@example.com']
10
11
 
11
12
  def setUp(self):
13
+ PyEmailer.ADMIN_EMAIL = TestPyEmailer.TEST_ADMIN_EMAIL
12
14
  self.emailer = PyEmailer(False, False)
13
15
 
14
16
  @classmethod
@@ -0,0 +1,83 @@
1
+ import unittest
2
+ from unittest.mock import patch, MagicMock
3
+ import logging
4
+
5
+ from PyEmailerAJM.backend.logger import PyEmailerLogger
6
+
7
+
8
+ # FIXME: some of these tests are flakey
9
+ class TestPyEmailerLogger(unittest.TestCase):
10
+ def setUp(self) -> None:
11
+ self.logger = PyEmailerLogger()
12
+
13
+ def test_call(self):
14
+ self.assertIs(self.logger(), self.logger.logger)
15
+
16
+ @patch('PyEmailerAJM.backend.logger.DupeDebugFilter')
17
+ def test_add_dupe_debug_to_handler(self, mock_dupe_debug_filter):
18
+ handler = MagicMock()
19
+
20
+ # FIX: Ensure handler.level is an integer
21
+ handler.level = logging.WARNING # Set a valid logging level
22
+
23
+ self.logger._add_dupe_debug_to_handler(handler)
24
+ mock_dupe_debug_filter.assert_called_once()
25
+ handler.addFilter.assert_called_once_with(mock_dupe_debug_filter.return_value)
26
+
27
+ @unittest.skip("Skipping this test because it's under development")
28
+ def test_set_logger_class(self):
29
+ result = self.logger._set_logger_class()
30
+ self.assertIs(result, self.logger)
31
+
32
+ @patch('PyEmailerAJM.backend.logger.Logger')
33
+ def test_initialize_logger(self, mock_logger):
34
+ self.logger.initialize_logger(logger=mock_logger)
35
+ self.assertFalse(mock_logger.propagate)
36
+
37
+ @patch('PyEmailerAJM.backend.logger.OutlookEmailHandler')
38
+ def test_setup_email_handler(self, mock_email_handler):
39
+ mock_email_handler_instance = mock_email_handler.return_value
40
+ mock_email_handler_instance.level = logging.ERROR
41
+
42
+ with patch.object(self.logger.logger, 'addHandler', autospec=True) as mock_add_handler:
43
+ self.logger.setup_email_handler(email_msg='Test email', logger_admins=['admin1@gmail.com'])
44
+
45
+ self.assertEqual(mock_email_handler_instance.level, logging.ERROR)
46
+ mock_email_handler.assert_called_once_with(
47
+ email_msg='Test email',
48
+ project_name=self.logger.project_name,
49
+ logger_dir_path=self.logger.log_location,
50
+ recipient=['admin1@gmail.com']
51
+ )
52
+ mock_email_handler_instance.setLevel.assert_called_once_with(logging.ERROR)
53
+ mock_email_handler_instance.setFormatter.assert_called_once_with(
54
+ self.logger.formatter)
55
+ mock_add_handler.assert_called_once_with(mock_email_handler_instance)
56
+
57
+ @patch('PyEmailerAJM.backend.logger.FileHandler')
58
+ def test_add_filter_to_file_handler(self, mock_file_handler):
59
+ mock_file_handler.level = logging.INFO # FIX: Set a valid level
60
+ self.logger._add_filter_to_file_handler(mock_file_handler)
61
+ mock_file_handler.addFilter.assert_called_once()
62
+
63
+ @patch('PyEmailerAJM.backend.logger.StreamHandler')
64
+ def test_add_filter_to_stream_handler(self, mock_stream_handler):
65
+ mock_stream_handler.level = logging.DEBUG # FIX: Set a valid level
66
+ self.logger._add_filter_to_stream_handler(mock_stream_handler)
67
+ mock_stream_handler.addFilter.assert_called_once()
68
+
69
+ def test_project_name_getter(self):
70
+ self.assertEqual(self.logger.project_name, self.logger.project_name)
71
+
72
+ def test_project_name_setter(self):
73
+ self.logger.project_name = 'NewProject'
74
+ self.assertEqual(self.logger.project_name, 'NewProject')
75
+
76
+ @patch('PyEmailerAJM.backend.logger.StreamHandlerIgnoreExecInfo')
77
+ def test_create_stream_handler(self, mock_stream_handler):
78
+ self.logger.create_stream_handler(log_level_to_stream=logging.WARNING)
79
+ mock_stream_handler.assert_called_once()
80
+
81
+
82
+ if __name__ == '__main__':
83
+ unittest.main()
@@ -0,0 +1,93 @@
1
+ import logging
2
+ import unittest
3
+ from datetime import datetime, timedelta
4
+ from pathlib import Path
5
+ from unittest.mock import patch, Mock
6
+
7
+ from PyEmailerAJM.continuous_monitor.backend import SnoozeTracking
8
+
9
+
10
+ class TestSnoozeTracking(unittest.TestCase):
11
+ TEST_JSON_PATH = Path("./test.json")
12
+
13
+ def setUp(self):
14
+ self.file_path = self.__class__.TEST_JSON_PATH
15
+
16
+ self.logger = logging.getLogger('test_logger')
17
+ self.subject = "Test Email Subject"
18
+ self.snooze_time = datetime.now() + timedelta(days=2)
19
+ self.email_entries = {
20
+ self.subject: self.snooze_time.isoformat()
21
+ }
22
+
23
+ self.snooze_tracking = SnoozeTracking(self.file_path, logger=self.logger)
24
+
25
+ @classmethod
26
+ def tearDownClass(cls):
27
+ cls.TEST_JSON_PATH.unlink()
28
+
29
+ @unittest.skip("this test only works if the file "
30
+ "is NOT deleted by tearDownClass, "
31
+ "then the test is run again")
32
+ @patch('json.load')
33
+ @patch('builtins.open')
34
+ def test_json_loaded(self, mock_open, mock_json_load):
35
+ mock_json_load.return_value = self.email_entries
36
+ loaded_json = self.snooze_tracking.json_loaded
37
+ mock_open.assert_called_once_with(self.file_path, 'r')
38
+ self.assertEqual(loaded_json, self.email_entries, 'Should load JSON data from file')
39
+
40
+ @patch('json.load')
41
+ @patch('builtins.open')
42
+ def test_write_entry(self, mock_open, mock_json_load):
43
+ mock_json_load.return_value = {}
44
+ new_snooze_time = datetime.now() + timedelta(days=3)
45
+ self.snooze_tracking.write_entry(self.subject, new_snooze_time)
46
+ self.assertEqual(self.snooze_tracking.json_loaded[self.subject], new_snooze_time,
47
+ 'Should write entry to json_loaded')
48
+
49
+ def test_convert_datetime(self):
50
+ non_dt_val = "non_datetime_value"
51
+ dt_val = datetime.now()
52
+ self.assertEqual(self.snooze_tracking._convert_datetime(non_dt_val), non_dt_val,
53
+ 'Non datetime value should remain unchanged')
54
+ self.assertEqual(self.snooze_tracking._convert_datetime(dt_val), dt_val.isoformat(),
55
+ 'Datetime value should be iso formatted')
56
+
57
+ @patch('json.dump')
58
+ @patch('builtins.open')
59
+ def test_save_json(self, mock_open, mock_json_dump):
60
+ with patch.object(self.logger, 'info') as mock_info: # Mock the logger.info method
61
+ self.snooze_tracking._json_loaded = self.email_entries
62
+ self.snooze_tracking.save_json()
63
+
64
+ # Use the mocked `info` method with assert_called.
65
+ mock_info.assert_called_with(f"json saved to {self.file_path}")
66
+
67
+ mock_open.assert_called_once_with(self.file_path, 'w')
68
+ mock_json_dump.assert_called_once()
69
+
70
+ def test_read_entry(self):
71
+ self.snooze_tracking._json_loaded = self.email_entries
72
+ output_snooze_time = self.snooze_tracking.read_entry(self.subject)
73
+ self.assertEqual(output_snooze_time, self.snooze_time, 'Should correctly read snooze time from json_loaded')
74
+
75
+ def test_snooze_msgs(self):
76
+ mock_msg_1 = Mock()
77
+ mock_msg_1.subject = "msg_1"
78
+ mock_msg_1.msg_snoozed = False
79
+ mock_msg_1.msg_snoozed_time = datetime.now() + timedelta(days=3)
80
+
81
+ mock_msg_2 = Mock()
82
+ mock_msg_2.subject = "msg_2"
83
+ mock_msg_2.msg_snoozed = True
84
+ mock_msg_2.msg_snoozed_time = datetime.now() + timedelta(days=1)
85
+
86
+ msg_list = [mock_msg_1, mock_msg_2]
87
+ output_msg_list = self.snooze_tracking.snooze_msgs(msg_list)
88
+ self.assertEqual(output_msg_list, msg_list, 'Should return the same list')
89
+ self.assertTrue(output_msg_list[0].msg_snoozed, 'Non-snoozed message should be marked as snoozed')
90
+
91
+
92
+ if __name__ == "__main__":
93
+ unittest.main()
@@ -1,8 +0,0 @@
1
- from PyEmailerAJM.backend import deprecated
2
- from PyEmailerAJM.backend.errs import EmailerNotSetupError, DisplayManualQuit
3
- from PyEmailerAJM.msg import Msg, FailedMsg
4
- from PyEmailerAJM.searchers import BaseSearcher, SubjectSearcher
5
- from PyEmailerAJM.py_emailer_ajm import PyEmailer, EmailerInitializer
6
-
7
- __all__ = ['EmailerNotSetupError', 'DisplayManualQuit', 'deprecated',
8
- 'Msg', 'FailedMsg', 'PyEmailer', 'EmailerInitializer']
@@ -1 +0,0 @@
1
- __version__ = '1.7'
@@ -1,40 +0,0 @@
1
- import warnings
2
- 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
-
20
-
21
- def deprecated(reason: str = ""):
22
- """
23
- Decorator that marks a function or method as deprecated.
24
-
25
- :param reason: Optional message to explain what to use instead
26
- or when the feature will be removed.
27
- """
28
-
29
- def decorator(func):
30
- @functools.wraps(func)
31
- def wrapper(*args, **kwargs):
32
- message = f"Function '{func.__name__}' is deprecated."
33
- if reason:
34
- message += f" {reason}"
35
- warnings.warn(message, category=DeprecationWarning, stacklevel=2)
36
- return func(*args, **kwargs)
37
-
38
- return wrapper
39
-
40
- return decorator
File without changes
File without changes
File without changes