sft-ext 0.5.0__py3-none-any.whl

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.
sft_ext/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,89 @@
1
+ import sys
2
+ from asyncio import CancelledError
3
+ from functools import wraps
4
+ from io import StringIO
5
+ from logging import getLogger
6
+
7
+ from aiologic import Event
8
+ from rich.console import Console
9
+ from sft_ext.errors.send_alert_email import send_alert_email
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable, Coroutine
14
+
15
+ main_module = sys.modules["__main__"]
16
+ RICH_CONSOLE = getattr(main_module, "RICH_CONSOLE", None)
17
+
18
+ logger = getLogger(__name__)
19
+
20
+
21
+ FATAL_EVENT = Event()
22
+
23
+
24
+ def handle_fatal_exc_sync[**TP, TR](func: Callable[TP, TR]) -> Callable[TP, TR | None]:
25
+ @wraps(func)
26
+ def wrapper(*args: TP.args, **kwargs: TP.kwargs) -> TR | None:
27
+ try:
28
+ return func(*args, **kwargs)
29
+ except CancelledError:
30
+ pass
31
+ raise # raise whatever to make the type checker happy about return values
32
+ except BaseException as e:
33
+ if isinstance(e, CancelledError):
34
+ raise
35
+ logger.critical(f"Fatal exception in {func.__qualname__}: {e}", exc_info=True)
36
+
37
+ strio = StringIO()
38
+
39
+ tmp = Console(force_terminal=False, force_interactive=False, color_system=None, markup=False, file=strio, no_color=True)
40
+
41
+ with tmp.capture() as capture:
42
+ tmp.print_exception(show_locals=True)
43
+ content = capture.get()
44
+
45
+ send_alert_email(f"Fatal exception in {func.__qualname__}", f"{e}:\n\n{content}")
46
+ FATAL_EVENT.set()
47
+ return None
48
+
49
+ return func if __debug__ and __name__ != "__main__" else wrapper
50
+
51
+
52
+ def handle_fatal_exc_async[**TP, TR](func: Callable[TP, Coroutine[None, None, TR]]) -> Callable[TP, Coroutine[None, None, TR | None]]:
53
+ @wraps(func)
54
+ async def wrapper(*args: TP.args, **kwargs: TP.kwargs) -> TR | None:
55
+ try:
56
+ return await func(*args, **kwargs)
57
+ except CancelledError:
58
+ pass
59
+ raise # raise whatever to make the type checker happy about return values
60
+ except GeneratorExit:
61
+ pass
62
+ return None # if a GeneratorExit is caught, that means a coroutine is being cancelled for a graceful shutdown.
63
+ except BaseException as e:
64
+ if isinstance(e, CancelledError):
65
+ raise
66
+ logger.critical(f"Fatal exception in {func.__qualname__}: {e}", exc_info=True)
67
+
68
+ strio = StringIO()
69
+
70
+ tmp = Console(force_terminal=False, force_interactive=False, color_system=None, markup=False, file=strio, no_color=True)
71
+
72
+ with tmp.capture() as capture:
73
+ tmp.print_exception(show_locals=True)
74
+ content = capture.get()
75
+
76
+ send_alert_email(f"Fatal exception in {func.__qualname__}", f"{e}:\n\n{content}")
77
+ FATAL_EVENT.set()
78
+ return None
79
+
80
+ return func if __debug__ and __name__ != "__main__" else wrapper
81
+
82
+
83
+ if __name__ == "__main__":
84
+
85
+ @handle_fatal_exc_sync
86
+ def test_func():
87
+ raise ValueError("This is a test exception.")
88
+
89
+ test_func()
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import smtplib
4
+ import ssl
5
+ import sys
6
+ from email.message import EmailMessage
7
+ from logging import getLogger
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from sft_ext.settings import BaseSettings
12
+
13
+ main_module = sys.modules["__main__"]
14
+ RICH_CONSOLE = getattr(main_module, "RICH_CONSOLE", None)
15
+
16
+ logger = getLogger(__name__)
17
+
18
+
19
+ try:
20
+ import sys
21
+
22
+ try:
23
+ settings_module = sys.modules["environment_init_vars"]
24
+ SETTINGS: BaseSettings = settings_module.SETTINGS
25
+ except KeyError:
26
+ settings_module = sys.modules["environment_settings"]
27
+ SETTINGS: BaseSettings = settings_module.SETTINGS() # type: ignore
28
+ except (KeyError, AttributeError):
29
+ from sft_ext.settings import BaseSettings
30
+
31
+ SETTINGS: BaseSettings = BaseSettings() # type: ignore
32
+
33
+
34
+ ALERTS_EMAIL = SETTINGS.alerts_email
35
+ ALERTS_EMAIL_PWD = SETTINGS.alerts_email_pwd
36
+ ALERTS_RECIPIENTS = SETTINGS.alerts_recipients
37
+
38
+
39
+ def send_alert_email(subject: str, content: str) -> None:
40
+ if not ALERTS_RECIPIENTS:
41
+ logger.warning("Skipping alert email because no recipients are configured.")
42
+ return
43
+
44
+ msg = EmailMessage()
45
+ msg.set_content("View attachment")
46
+ msg["Subject"] = subject
47
+ msg["From"] = ALERTS_EMAIL
48
+ msg["To"] = ", ".join([str(recipient) for recipient in ALERTS_RECIPIENTS])
49
+ context = ssl.create_default_context()
50
+
51
+ msg.add_attachment(
52
+ "\ufeff" + content, # UTF-8 BOM so Windows apps detect encoding correctly
53
+ subtype="plain",
54
+ filename="alert.txt",
55
+ charset="utf-8",
56
+ )
57
+
58
+ try:
59
+ with smtplib.SMTP(SETTINGS.alerts_smtp_server, SETTINGS.alerts_smtp_port) as server:
60
+ server.ehlo()
61
+ server.starttls(context=context)
62
+ server.ehlo()
63
+ server.login(ALERTS_EMAIL, ALERTS_EMAIL_PWD)
64
+ server.send_message(msg)
65
+ logger.debug("Alert email sent successfully.")
66
+ except Exception:
67
+ logger.error("Failed to send alert email.", exc_info=True)
File without changes