VersaLog 2.6.0__tar.gz → 3.0.0__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.
Files changed (24) hide show
  1. {versalog-2.6.0 → versalog-3.0.0}/PKG-INFO +10 -2
  2. {versalog-2.6.0 → versalog-3.0.0}/VersaLog/__init__.py +3 -3
  3. {versalog-2.6.0 → versalog-3.0.0}/VersaLog/core.py +29 -0
  4. versalog-3.0.0/VersaLog/integrations/django.py +19 -0
  5. versalog-3.0.0/VersaLog/integrations/webhook/__init__.py +3 -0
  6. versalog-3.0.0/VersaLog/integrations/webhook/formatters.py +17 -0
  7. versalog-3.0.0/VersaLog/integrations/webhook/handler.py +43 -0
  8. versalog-3.0.0/VersaLog/integrations/webhook/sender.py +16 -0
  9. {versalog-2.6.0 → versalog-3.0.0}/VersaLog.egg-info/PKG-INFO +10 -2
  10. {versalog-2.6.0 → versalog-3.0.0}/VersaLog.egg-info/SOURCES.txt +5 -1
  11. {versalog-2.6.0 → versalog-3.0.0}/setup.py +1 -1
  12. versalog-2.6.0/VersaLog/integrations/django.py +0 -5
  13. {versalog-2.6.0 → versalog-3.0.0}/LICENSE +0 -0
  14. {versalog-2.6.0 → versalog-3.0.0}/README.md +0 -0
  15. {versalog-2.6.0 → versalog-3.0.0}/VersaLog/handler.py +0 -0
  16. {versalog-2.6.0 → versalog-3.0.0}/VersaLog/integrations/__init__.py +0 -0
  17. {versalog-2.6.0 → versalog-3.0.0}/VersaLog/integrations/auto.py +0 -0
  18. {versalog-2.6.0 → versalog-3.0.0}/VersaLog/integrations/fastapi.py +0 -0
  19. {versalog-2.6.0 → versalog-3.0.0}/VersaLog/integrations/flask.py +0 -0
  20. {versalog-2.6.0 → versalog-3.0.0}/VersaLog/integrations/generic.py +0 -0
  21. {versalog-2.6.0 → versalog-3.0.0}/VersaLog.egg-info/dependency_links.txt +0 -0
  22. {versalog-2.6.0 → versalog-3.0.0}/VersaLog.egg-info/requires.txt +0 -0
  23. {versalog-2.6.0 → versalog-3.0.0}/VersaLog.egg-info/top_level.txt +0 -0
  24. {versalog-2.6.0 → versalog-3.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: VersaLog
3
- Version: 2.6.0
3
+ Version: 3.0.0
4
4
  Summary: Versatile logging library.
5
5
  Home-page:
6
6
  Author:
@@ -18,6 +18,14 @@ Requires-Python: >=3.8, <3.14
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: plyer
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: license
25
+ Dynamic: license-file
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
21
29
 
22
30
  ## What is VersaLog.py?
23
31
 
@@ -4,10 +4,10 @@ from .integrations.auto import auto_setup
4
4
  from .integrations.fastapi import setup_fastapi_logging
5
5
  from .integrations.flask import setup_flask_logging
6
6
  from .integrations.django import setup_django_logging
7
+ from .integrations.webhook import WebhookHandler
7
8
 
8
9
  __copyright__ = "Copyright 2025 by Kayu0514"
9
- __version__ = "2.6.0"
10
+ __version__ = "3.0.0"
10
11
  __author__ = "Kayu0514"
11
12
  __url__ = "https://github.com/kayu0514"
12
-
13
- __all__ = ["VersaLog", "VersaLogHandler", "auto_setup", "setup_fastapi_logging","setup_flask_logging", "setup_django_logging"]
13
+ __all__ = ["VersaLog", "VersaLogHandler", "WebhookHandler", "auto_setup", "setup_fastapi_logging","setup_flask_logging", "setup_django_logging"]
@@ -2,6 +2,8 @@ from typing import Optional, Union, List
2
2
  from plyer import notification
3
3
  from contextlib import contextmanager
4
4
 
5
+ from .integrations.webhook import WebhookHandler
6
+
5
7
  import datetime
6
8
  import inspect
7
9
  import os
@@ -73,6 +75,7 @@ class VersaLog:
73
75
  self.all_save = all_save
74
76
  self.save_levels = save_levels
75
77
  self.silent = silent
78
+ self._webhook_handlers = []
76
79
 
77
80
  self.log_queue = queue.Queue()
78
81
  self.worker_thread = threading.Thread(target=self._worker, daemon=True)
@@ -218,6 +221,32 @@ class VersaLog:
218
221
 
219
222
  self._save_log(plain, types)
220
223
 
224
+ def add_webhook(self, url: str, level: str = "ERROR", *, format: str = "generic", timeout: float = 3.0, headers: Optional[dict] = None,):
225
+ """Register a webhook destination for logs at or above ``level``.
226
+
227
+ ``format`` can be ``"generic"``, ``"discord"``, or ``"slack"``.
228
+ The default ``"ERROR"`` sends ERROR and CRITICAL logs.
229
+ """
230
+
231
+ handler = WebhookHandler(url=url, level=level, format=format, timeout=timeout, headers=headers)
232
+ self._webhook_handlers.append(handler)
233
+ return handler
234
+
235
+ def remove_webhook(self, handler) -> bool:
236
+ try:
237
+ self._webhook_handlers.remove(handler)
238
+ return True
239
+ except ValueError:
240
+ return False
241
+
242
+ def _dispatch_webhooks(self, message: str, level: str) -> None:
243
+ for handler in tuple(self._webhook_handlers):
244
+ if handler.should_handle(level):
245
+ try:
246
+ handler.emit(message, level)
247
+ except Exception:
248
+ pass
249
+
221
250
  def info(self, msg: str, tag: Optional[Union[str, List[str]]] = None) -> None:
222
251
  self._Log(msg, "INFO", tag)
223
252
 
@@ -0,0 +1,19 @@
1
+ import logging
2
+ from ..handler import VersaLogHandle
3
+
4
+
5
+ def setup_django_logging(versalog):
6
+ handler = VersaLogHandler(versalog)
7
+
8
+ root = logging.getLogger()
9
+ for h in root.handlers[:]:
10
+ root.removeHandler(h)
11
+ root.setLevel(logging.NOTSET)
12
+ root.addHandler(handler)
13
+
14
+ for name in ["django", "django.request", "django.server", "django.db.backends"]:
15
+ logger = logging.getLogger(name)
16
+ for h in logger.handlers[:]:
17
+ logger.removeHandler(h)
18
+ logger.propagate = True
19
+ logger.setLevel(logging.NOTSET)
@@ -0,0 +1,3 @@
1
+ from .handler import WebhookHandler
2
+
3
+ __all__ = ["WebhookHandler"]
@@ -0,0 +1,17 @@
1
+ import datetime
2
+
3
+
4
+ def format_payload(level, message, format="generic"):
5
+ timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
6
+ if format == "generic":
7
+ return {
8
+ "source": "VersaLog",
9
+ "level": level,
10
+ "message": message,
11
+ "timestamp": timestamp,
12
+ }
13
+ if format == "discord":
14
+ return {"content": f"[{level}] {message}"}
15
+ if format == "slack":
16
+ return {"text": f"[{level}] {message}"}
17
+ raise ValueError("format must be 'generic', 'discord', or 'slack'")
@@ -0,0 +1,43 @@
1
+ from .formatters import format_payload
2
+ from .sender import send_json
3
+
4
+
5
+ LEVELS = {
6
+ "DEBUG": 10,
7
+ "INFO": 20,
8
+ "WARNING": 30,
9
+ "ERROR": 40,
10
+ "CRITICAL": 50,
11
+ }
12
+
13
+
14
+ class WebhookHandler:
15
+ def __init__(self, url, level="ERROR", format="generic", timeout=3.0, headers=None):
16
+ if not isinstance(url, str) or not url.strip():
17
+ raise ValueError("url must be a non-empty string")
18
+ normalized_level = str(level).upper()
19
+ if normalized_level not in LEVELS:
20
+ raise ValueError(
21
+ "level must be one of: " + ", ".join(LEVELS.keys())
22
+ )
23
+ if format not in ("generic", "discord", "slack"):
24
+ raise ValueError("format must be 'generic', 'discord', or 'slack'")
25
+ if timeout <= 0:
26
+ raise ValueError("timeout must be greater than 0")
27
+
28
+ self.url = url
29
+ self.level = normalized_level
30
+ self.format = format
31
+ self.timeout = timeout
32
+ self.headers = dict(headers or {})
33
+ self.last_success = None
34
+
35
+ def should_handle(self, level):
36
+ return LEVELS.get(str(level).upper(), 0) >= LEVELS[self.level]
37
+
38
+ def emit(self, message, level):
39
+ payload = format_payload(str(level).upper(), str(message), self.format)
40
+ self.last_success = send_json(
41
+ self.url, payload, timeout=self.timeout, headers=self.headers
42
+ )
43
+ return self.last_success
@@ -0,0 +1,16 @@
1
+ import json
2
+ from urllib.error import HTTPError, URLError
3
+ from urllib.request import Request, urlopen
4
+
5
+
6
+ def send_json(url, payload, timeout=3.0, headers=None):
7
+ request_headers = {"Content-Type": "application/json; charset=utf-8"}
8
+ if headers:
9
+ request_headers.update(headers)
10
+ data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
11
+ request = Request(url, data=data, headers=request_headers, method="POST")
12
+ try:
13
+ with urlopen(request, timeout=timeout) as response:
14
+ return 200 <= response.status < 300
15
+ except (HTTPError, URLError, TimeoutError, OSError, ValueError):
16
+ return False
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: VersaLog
3
- Version: 2.6.0
3
+ Version: 3.0.0
4
4
  Summary: Versatile logging library.
5
5
  Home-page:
6
6
  Author:
@@ -18,6 +18,14 @@ Requires-Python: >=3.8, <3.14
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: plyer
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: license
25
+ Dynamic: license-file
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
21
29
 
22
30
  ## What is VersaLog.py?
23
31
 
@@ -14,4 +14,8 @@ VersaLog/integrations/auto.py
14
14
  VersaLog/integrations/django.py
15
15
  VersaLog/integrations/fastapi.py
16
16
  VersaLog/integrations/flask.py
17
- VersaLog/integrations/generic.py
17
+ VersaLog/integrations/generic.py
18
+ VersaLog/integrations/webhook/__init__.py
19
+ VersaLog/integrations/webhook/formatters.py
20
+ VersaLog/integrations/webhook/handler.py
21
+ VersaLog/integrations/webhook/sender.py
@@ -5,7 +5,7 @@ with open("README.md", encoding="utf-8") as f:
5
5
 
6
6
  setup(
7
7
  name="VersaLog",
8
- version="2.6.0",
8
+ version="3.0.0",
9
9
  description="Versatile logging library.",
10
10
  long_description=long_description,
11
11
  long_description_content_type="text/markdown",
@@ -1,5 +0,0 @@
1
- from .generic import setup_logging
2
-
3
-
4
- def setup_django_logging(versalog):
5
- setup_logging(versalog)
File without changes
File without changes
File without changes
File without changes