scitex-notification 0.1.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.
- scitex_notification/__init__.py +321 -0
- scitex_notification/__main__.py +7 -0
- scitex_notification/_backends/__init__.py +69 -0
- scitex_notification/_backends/_audio.py +100 -0
- scitex_notification/_backends/_config.py +320 -0
- scitex_notification/_backends/_desktop.py +184 -0
- scitex_notification/_backends/_emacs.py +210 -0
- scitex_notification/_backends/_email.py +204 -0
- scitex_notification/_backends/_matplotlib.py +118 -0
- scitex_notification/_backends/_playwright.py +109 -0
- scitex_notification/_backends/_twilio.py +359 -0
- scitex_notification/_backends/_types.py +58 -0
- scitex_notification/_backends/_webhook.py +81 -0
- scitex_notification/_backends.py +37 -0
- scitex_notification/_cli/__init__.py +11 -0
- scitex_notification/_cli/_helpers.py +54 -0
- scitex_notification/_cli/_main.py +133 -0
- scitex_notification/_cli/_mcp_cmds.py +171 -0
- scitex_notification/_cli/_notify_cmds.py +334 -0
- scitex_notification/_env_loader.py +146 -0
- scitex_notification/_linter_plugin.py +27 -0
- scitex_notification/_mcp/__init__.py +23 -0
- scitex_notification/_mcp/handlers.py +262 -0
- scitex_notification/_mcp/tool_schemas.py +107 -0
- scitex_notification/mcp_server.py +159 -0
- scitex_notification-0.1.0.dist-info/METADATA +247 -0
- scitex_notification-0.1.0.dist-info/RECORD +30 -0
- scitex_notification-0.1.0.dist-info/WHEEL +4 -0
- scitex_notification-0.1.0.dist-info/entry_points.txt +8 -0
- scitex_notification-0.1.0.dist-info/licenses/LICENSE +20 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-03-16 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-notification/src/scitex_notification/__init__.py
|
|
4
|
+
|
|
5
|
+
"""SciTeX Notification Module - User alerts and feedback.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
import scitex_notification as stxn
|
|
9
|
+
|
|
10
|
+
# Simple alert - uses fallback priority (audio -> emacs -> desktop -> ...)
|
|
11
|
+
stxn.alert("2FA required!")
|
|
12
|
+
|
|
13
|
+
# Specify backend (no fallback)
|
|
14
|
+
stxn.alert("Error", backend="email")
|
|
15
|
+
|
|
16
|
+
# Multiple backends (tries all)
|
|
17
|
+
stxn.alert("Critical", backend=["audio", "email"])
|
|
18
|
+
|
|
19
|
+
# Use fallback explicitly
|
|
20
|
+
stxn.alert("Important", fallback=True)
|
|
21
|
+
|
|
22
|
+
# Make a phone call via Twilio
|
|
23
|
+
stxn.call("Critical alert!")
|
|
24
|
+
|
|
25
|
+
# Send an SMS via Twilio
|
|
26
|
+
stxn.sms("Build done!")
|
|
27
|
+
|
|
28
|
+
Environment Variables:
|
|
29
|
+
SCITEX_NOTIFY_DEFAULT_BACKEND: audio, email, desktop, webhook
|
|
30
|
+
SCITEX_UI_DEFAULT_BACKEND: (deprecated) use SCITEX_NOTIFY_DEFAULT_BACKEND
|
|
31
|
+
SCITEX_NOTIFICATION_ENV_SRC: path to .env file to auto-load on import
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import asyncio
|
|
37
|
+
import os
|
|
38
|
+
from typing import Optional, Union
|
|
39
|
+
|
|
40
|
+
from ._env_loader import load_scitex_notification_env as _load_env
|
|
41
|
+
|
|
42
|
+
_load_env()
|
|
43
|
+
|
|
44
|
+
from ._backends import NotifyLevel as _AlertLevel
|
|
45
|
+
from ._backends import available_backends as _available_backends
|
|
46
|
+
from ._backends import get_backend as _get_backend
|
|
47
|
+
|
|
48
|
+
__version__ = "0.1.0"
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"alert",
|
|
52
|
+
"alert_async",
|
|
53
|
+
"available_backends",
|
|
54
|
+
"call",
|
|
55
|
+
"call_async",
|
|
56
|
+
"sms",
|
|
57
|
+
"sms_async",
|
|
58
|
+
"__version__",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
# Default fallback priority order
|
|
62
|
+
DEFAULT_FALLBACK_ORDER = [
|
|
63
|
+
"audio", # 1st: TTS audio (non-blocking, immediate)
|
|
64
|
+
"emacs", # 2nd: Emacs minibuffer (if in Emacs)
|
|
65
|
+
"matplotlib", # 3rd: Visual popup
|
|
66
|
+
"playwright", # 4th: Browser popup
|
|
67
|
+
"email", # 5th: Email (slowest, most reliable)
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def available_backends() -> list[str]:
|
|
72
|
+
"""Return list of available alert backends."""
|
|
73
|
+
return _available_backends()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def alert_async(
|
|
77
|
+
message: str,
|
|
78
|
+
title: Optional[str] = None,
|
|
79
|
+
backend: Optional[Union[str, list[str]]] = None,
|
|
80
|
+
level: str = "info",
|
|
81
|
+
fallback: bool = True,
|
|
82
|
+
**kwargs,
|
|
83
|
+
) -> bool:
|
|
84
|
+
"""Send alert asynchronously.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
message : str
|
|
89
|
+
Alert message
|
|
90
|
+
title : str, optional
|
|
91
|
+
Alert title
|
|
92
|
+
backend : str or list[str], optional
|
|
93
|
+
Backend(s) to use. If None, uses default with fallback.
|
|
94
|
+
level : str
|
|
95
|
+
Alert level: info, warning, error, critical
|
|
96
|
+
fallback : bool
|
|
97
|
+
If True and backend fails, try next in priority order.
|
|
98
|
+
Default True when backend=None, False when backend specified.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
bool
|
|
103
|
+
True if any backend succeeded
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
lvl = _AlertLevel(level.lower())
|
|
107
|
+
except ValueError:
|
|
108
|
+
lvl = _AlertLevel.INFO
|
|
109
|
+
|
|
110
|
+
# Determine backends to try
|
|
111
|
+
if backend is None:
|
|
112
|
+
# No backend specified: use fallback priority
|
|
113
|
+
default = (
|
|
114
|
+
os.getenv("SCITEX_NOTIFICATION_DEFAULT_BACKEND")
|
|
115
|
+
or os.getenv("SCITEX_NOTIFY_DEFAULT_BACKEND")
|
|
116
|
+
or os.getenv("SCITEX_UI_DEFAULT_BACKEND", "audio")
|
|
117
|
+
)
|
|
118
|
+
if fallback:
|
|
119
|
+
# Start with default, then try others in priority order
|
|
120
|
+
backends = [default] + [b for b in DEFAULT_FALLBACK_ORDER if b != default]
|
|
121
|
+
else:
|
|
122
|
+
backends = [default]
|
|
123
|
+
else:
|
|
124
|
+
# Backend specified: use it (with optional fallback)
|
|
125
|
+
backends = [backend] if isinstance(backend, str) else list(backend)
|
|
126
|
+
if fallback and len(backends) == 1:
|
|
127
|
+
# Add fallback backends after the specified one
|
|
128
|
+
backends = backends + [
|
|
129
|
+
b for b in DEFAULT_FALLBACK_ORDER if b not in backends
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# Try backends until one succeeds
|
|
133
|
+
available = _available_backends()
|
|
134
|
+
for name in backends:
|
|
135
|
+
if name not in available:
|
|
136
|
+
continue
|
|
137
|
+
try:
|
|
138
|
+
b = _get_backend(name)
|
|
139
|
+
result = await b.send(message, title=title, level=lvl, **kwargs)
|
|
140
|
+
if result.success:
|
|
141
|
+
return True
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def alert(
|
|
149
|
+
message: str,
|
|
150
|
+
title: Optional[str] = None,
|
|
151
|
+
backend: Optional[Union[str, list[str]]] = None,
|
|
152
|
+
level: str = "info",
|
|
153
|
+
fallback: bool = True,
|
|
154
|
+
**kwargs,
|
|
155
|
+
) -> bool:
|
|
156
|
+
"""Send alert synchronously.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
message : str
|
|
161
|
+
Alert message
|
|
162
|
+
title : str, optional
|
|
163
|
+
Alert title
|
|
164
|
+
backend : str or list[str], optional
|
|
165
|
+
Backend(s) to use. If None, uses fallback priority order.
|
|
166
|
+
level : str
|
|
167
|
+
Alert level: info, warning, error, critical
|
|
168
|
+
fallback : bool
|
|
169
|
+
If True and backend fails, try next in priority order.
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
bool
|
|
174
|
+
True if any backend succeeded
|
|
175
|
+
|
|
176
|
+
Fallback Order
|
|
177
|
+
--------------
|
|
178
|
+
1. audio - TTS (fast, non-blocking)
|
|
179
|
+
2. emacs - Minibuffer message
|
|
180
|
+
3. matplotlib - Visual popup
|
|
181
|
+
4. playwright - Browser popup
|
|
182
|
+
5. email - Email (slowest)
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
asyncio.get_running_loop()
|
|
186
|
+
import concurrent.futures
|
|
187
|
+
|
|
188
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
189
|
+
future = executor.submit(
|
|
190
|
+
asyncio.run,
|
|
191
|
+
alert_async(message, title, backend, level, fallback, **kwargs),
|
|
192
|
+
)
|
|
193
|
+
return future.result(timeout=30)
|
|
194
|
+
except RuntimeError:
|
|
195
|
+
return asyncio.run(
|
|
196
|
+
alert_async(message, title, backend, level, fallback, **kwargs)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def call(
|
|
201
|
+
message: str,
|
|
202
|
+
title: Optional[str] = None,
|
|
203
|
+
level: str = "info",
|
|
204
|
+
to_number: Optional[str] = None,
|
|
205
|
+
**kwargs,
|
|
206
|
+
) -> bool:
|
|
207
|
+
"""Make a phone call via Twilio.
|
|
208
|
+
|
|
209
|
+
Convenience wrapper for alert(backend="twilio").
|
|
210
|
+
"""
|
|
211
|
+
return alert(
|
|
212
|
+
message,
|
|
213
|
+
title=title,
|
|
214
|
+
backend="twilio",
|
|
215
|
+
level=level,
|
|
216
|
+
fallback=False,
|
|
217
|
+
to_number=to_number,
|
|
218
|
+
**kwargs,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def call_async(
|
|
223
|
+
message: str,
|
|
224
|
+
title: Optional[str] = None,
|
|
225
|
+
level: str = "info",
|
|
226
|
+
to_number: Optional[str] = None,
|
|
227
|
+
**kwargs,
|
|
228
|
+
) -> bool:
|
|
229
|
+
"""Make a phone call via Twilio (async)."""
|
|
230
|
+
return await alert_async(
|
|
231
|
+
message,
|
|
232
|
+
title=title,
|
|
233
|
+
backend="twilio",
|
|
234
|
+
level=level,
|
|
235
|
+
fallback=False,
|
|
236
|
+
to_number=to_number,
|
|
237
|
+
**kwargs,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def sms_async(
|
|
242
|
+
message: str,
|
|
243
|
+
title: Optional[str] = None,
|
|
244
|
+
to_number: Optional[str] = None,
|
|
245
|
+
**kwargs,
|
|
246
|
+
) -> bool:
|
|
247
|
+
"""Send an SMS via Twilio (async).
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
message : str
|
|
252
|
+
SMS body text
|
|
253
|
+
title : str, optional
|
|
254
|
+
Prepended to message if provided
|
|
255
|
+
to_number : str, optional
|
|
256
|
+
Override SCITEX_NOTIFY_TWILIO_TO
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
bool
|
|
261
|
+
True if SMS sent successfully
|
|
262
|
+
"""
|
|
263
|
+
from ._backends._twilio import send_sms as _send_sms
|
|
264
|
+
|
|
265
|
+
result = await _send_sms(
|
|
266
|
+
message,
|
|
267
|
+
title=title,
|
|
268
|
+
to_number=to_number,
|
|
269
|
+
**kwargs,
|
|
270
|
+
)
|
|
271
|
+
return result.success
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def sms(
|
|
275
|
+
message: str,
|
|
276
|
+
title: Optional[str] = None,
|
|
277
|
+
to_number: Optional[str] = None,
|
|
278
|
+
**kwargs,
|
|
279
|
+
) -> bool:
|
|
280
|
+
"""Send an SMS via Twilio.
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
message : str
|
|
285
|
+
SMS body text
|
|
286
|
+
title : str, optional
|
|
287
|
+
Prepended to message if provided
|
|
288
|
+
to_number : str, optional
|
|
289
|
+
Override SCITEX_NOTIFY_TWILIO_TO
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
bool
|
|
294
|
+
True if SMS sent successfully
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
asyncio.get_running_loop()
|
|
298
|
+
import concurrent.futures
|
|
299
|
+
|
|
300
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
301
|
+
future = executor.submit(
|
|
302
|
+
asyncio.run,
|
|
303
|
+
sms_async(message, title, to_number, **kwargs),
|
|
304
|
+
)
|
|
305
|
+
return future.result(timeout=30)
|
|
306
|
+
except RuntimeError:
|
|
307
|
+
return asyncio.run(sms_async(message, title, to_number, **kwargs))
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# Apply @supports_return_as decorator if scitex_dev is available
|
|
311
|
+
try:
|
|
312
|
+
from scitex_dev.decorators import supports_return_as as _supports_return_as
|
|
313
|
+
|
|
314
|
+
alert = _supports_return_as(alert)
|
|
315
|
+
call = _supports_return_as(call)
|
|
316
|
+
sms = _supports_return_as(sms)
|
|
317
|
+
except ImportError:
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# EOF
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-13 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-notification/src/scitex_notification/_backends/__init__.py
|
|
4
|
+
|
|
5
|
+
"""Notification backend registry and utilities."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ._audio import AudioBackend
|
|
10
|
+
from ._desktop import DesktopBackend
|
|
11
|
+
from ._emacs import EmacsBackend
|
|
12
|
+
from ._email import EmailBackend
|
|
13
|
+
from ._matplotlib import MatplotlibBackend
|
|
14
|
+
from ._playwright import PlaywrightBackend
|
|
15
|
+
from ._twilio import TwilioBackend
|
|
16
|
+
from ._types import BaseNotifyBackend, NotifyLevel, NotifyResult
|
|
17
|
+
from ._webhook import WebhookBackend
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"NotifyLevel",
|
|
21
|
+
"NotifyResult",
|
|
22
|
+
"BaseNotifyBackend",
|
|
23
|
+
"AudioBackend",
|
|
24
|
+
"EmailBackend",
|
|
25
|
+
"DesktopBackend",
|
|
26
|
+
"EmacsBackend",
|
|
27
|
+
"WebhookBackend",
|
|
28
|
+
"MatplotlibBackend",
|
|
29
|
+
"PlaywrightBackend",
|
|
30
|
+
"TwilioBackend",
|
|
31
|
+
"BACKENDS",
|
|
32
|
+
"get_backend",
|
|
33
|
+
"available_backends",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Registry of available backends
|
|
37
|
+
BACKENDS: dict[str, type[BaseNotifyBackend]] = {
|
|
38
|
+
"audio": AudioBackend,
|
|
39
|
+
"email": EmailBackend,
|
|
40
|
+
"desktop": DesktopBackend,
|
|
41
|
+
"emacs": EmacsBackend,
|
|
42
|
+
"webhook": WebhookBackend,
|
|
43
|
+
"matplotlib": MatplotlibBackend,
|
|
44
|
+
"playwright": PlaywrightBackend,
|
|
45
|
+
"twilio": TwilioBackend,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_backend(name: str, **kwargs) -> BaseNotifyBackend:
|
|
50
|
+
"""Get a notification backend by name."""
|
|
51
|
+
if name not in BACKENDS:
|
|
52
|
+
raise ValueError(f"Unknown backend: {name}. Available: {list(BACKENDS.keys())}")
|
|
53
|
+
return BACKENDS[name](**kwargs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def available_backends() -> list[str]:
|
|
57
|
+
"""Return list of available notification backends."""
|
|
58
|
+
available = []
|
|
59
|
+
for name, cls in BACKENDS.items():
|
|
60
|
+
try:
|
|
61
|
+
backend = cls()
|
|
62
|
+
if backend.is_available():
|
|
63
|
+
available.append(name)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return available
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# EOF
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-13 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-notification/src/scitex_notification/_backends/_audio.py
|
|
4
|
+
|
|
5
|
+
"""Audio notification backend via TTS."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from ._types import BaseNotifyBackend, NotifyLevel, NotifyResult
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from scitex_audio import available_backends as _audio_available_backends
|
|
17
|
+
from scitex_audio import speak as _audio_speak
|
|
18
|
+
|
|
19
|
+
_AUDIO_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
_AUDIO_AVAILABLE = False
|
|
22
|
+
_audio_speak = None
|
|
23
|
+
_audio_available_backends = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AudioBackend(BaseNotifyBackend):
|
|
27
|
+
"""Audio notification via scitex_audio TTS."""
|
|
28
|
+
|
|
29
|
+
name = "audio"
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
backend: str = "gtts",
|
|
34
|
+
speed: float = 1.5,
|
|
35
|
+
rate: int = 180,
|
|
36
|
+
):
|
|
37
|
+
self.tts_backend = backend
|
|
38
|
+
self.speed = speed
|
|
39
|
+
self.rate = rate
|
|
40
|
+
|
|
41
|
+
def is_available(self) -> bool:
|
|
42
|
+
if not _AUDIO_AVAILABLE:
|
|
43
|
+
return False
|
|
44
|
+
try:
|
|
45
|
+
return len(_audio_available_backends()) > 0
|
|
46
|
+
except Exception:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
async def send(
|
|
50
|
+
self,
|
|
51
|
+
message: str,
|
|
52
|
+
title: Optional[str] = None,
|
|
53
|
+
level: NotifyLevel = NotifyLevel.INFO,
|
|
54
|
+
**kwargs,
|
|
55
|
+
) -> NotifyResult:
|
|
56
|
+
try:
|
|
57
|
+
if not _AUDIO_AVAILABLE or _audio_speak is None:
|
|
58
|
+
raise ImportError(
|
|
59
|
+
"scitex_audio is not installed. "
|
|
60
|
+
"Install it with: pip install scitex-audio"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Prepend title if provided
|
|
64
|
+
full_message = f"{title}. {message}" if title else message
|
|
65
|
+
|
|
66
|
+
# Add urgency prefix for critical/error levels
|
|
67
|
+
if level == NotifyLevel.CRITICAL:
|
|
68
|
+
full_message = f"Critical alert! {full_message}"
|
|
69
|
+
elif level == NotifyLevel.ERROR:
|
|
70
|
+
full_message = f"Error. {full_message}"
|
|
71
|
+
|
|
72
|
+
# Run TTS in executor to not block
|
|
73
|
+
loop = asyncio.get_event_loop()
|
|
74
|
+
await loop.run_in_executor(
|
|
75
|
+
None,
|
|
76
|
+
lambda: _audio_speak(
|
|
77
|
+
full_message,
|
|
78
|
+
backend=kwargs.get("tts_backend", self.tts_backend),
|
|
79
|
+
speed=kwargs.get("speed", self.speed),
|
|
80
|
+
rate=kwargs.get("rate", self.rate),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return NotifyResult(
|
|
85
|
+
success=True,
|
|
86
|
+
backend=self.name,
|
|
87
|
+
message=message,
|
|
88
|
+
timestamp=datetime.now().isoformat(),
|
|
89
|
+
)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return NotifyResult(
|
|
92
|
+
success=False,
|
|
93
|
+
backend=self.name,
|
|
94
|
+
message=message,
|
|
95
|
+
timestamp=datetime.now().isoformat(),
|
|
96
|
+
error=str(e),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# EOF
|