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 +0 -0
- sft_ext/errors/__init__.py +0 -0
- sft_ext/errors/err_handling.py +89 -0
- sft_ext/errors/send_alert_email.py +67 -0
- sft_ext/ftp/__init__.py +0 -0
- sft_ext/ftp/adapter.py +622 -0
- sft_ext/logging/__init__.py +0 -0
- sft_ext/logging/init_logging.py +184 -0
- sft_ext/logging/logging_bases.py +226 -0
- sft_ext/logging/logging_config.py +233 -0
- sft_ext/py.typed +0 -0
- sft_ext/rich/__init__.py +0 -0
- sft_ext/rich/progress.py +183 -0
- sft_ext/settings.py +49 -0
- sft_ext/types/__init__.py +0 -0
- sft_ext/types/abc.py +22 -0
- sft_ext/utils.py +61 -0
- sft_ext-0.5.0.dist-info/METADATA +22 -0
- sft_ext-0.5.0.dist-info/RECORD +20 -0
- sft_ext-0.5.0.dist-info/WHEEL +4 -0
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)
|
sft_ext/ftp/__init__.py
ADDED
|
File without changes
|