devalerts 0.2.1__tar.gz → 0.2.3__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devalerts
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Send unhandled Python exceptions straight to a Telegram chat. No backend, no account, no database — just your own bot token.
5
5
  Keywords: telegram,error-tracking,exception-monitoring,alerts,logging,monitoring,asgi
6
6
  Author: sslinNn
@@ -51,6 +51,8 @@ That's the whole setup. Two minutes with [@BotFather](https://t.me/BotFather)
51
51
  and every unhandled crash — including ones raised in threads — lands in your
52
52
  chat instead of a log file nobody's watching.
53
53
 
54
+ ![devalerts demo](https://raw.githubusercontent.com/sslinNn/devalerts/main/docs/demo.gif)
55
+
54
56
  ## Why devalerts
55
57
 
56
58
  - **Zero infrastructure.** No SaaS signup, no ingestion server, no API key
@@ -151,15 +153,27 @@ devalerts.init(bot_token="...", chat_id=123456789, tags={"env": "production"})
151
153
  🖥️ prod-web-2 (env=production)
152
154
  ```
153
155
 
154
- Add ad-hoc tags to a single call — they override `init()`'s tags on a key
155
- collision:
156
+ Add ad-hoc tags to a single call — they override `init()`'s tags (and each
157
+ other) on a key collision:
156
158
 
157
159
  ```python
158
160
  devalerts.report(extra={"request_id": "abc123"})
161
+ ```
162
+
163
+ Used as a decorator, `capture()` tags the alert with the wrapped function's
164
+ name as `job` automatically — no `extra` needed:
165
+
166
+ ```python
167
+ @devalerts.capture()
168
+ def nightly_sync(): ...
169
+ ```
159
170
 
160
- @devalerts.capture(extra={"job": "nightly-sync"})
161
- def run_job(): ...
162
171
  ```
172
+ 🔴 ValueError: boom
173
+ 🖥️ prod-web-2 (job=nightly_sync)
174
+ ```
175
+
176
+ Pass `extra={"job": "..."}` explicitly to override the auto-detected name.
163
177
 
164
178
  ## Manually reporting a caught exception
165
179
 
@@ -24,6 +24,8 @@ That's the whole setup. Two minutes with [@BotFather](https://t.me/BotFather)
24
24
  and every unhandled crash — including ones raised in threads — lands in your
25
25
  chat instead of a log file nobody's watching.
26
26
 
27
+ ![devalerts demo](https://raw.githubusercontent.com/sslinNn/devalerts/main/docs/demo.gif)
28
+
27
29
  ## Why devalerts
28
30
 
29
31
  - **Zero infrastructure.** No SaaS signup, no ingestion server, no API key
@@ -124,15 +126,27 @@ devalerts.init(bot_token="...", chat_id=123456789, tags={"env": "production"})
124
126
  🖥️ prod-web-2 (env=production)
125
127
  ```
126
128
 
127
- Add ad-hoc tags to a single call — they override `init()`'s tags on a key
128
- collision:
129
+ Add ad-hoc tags to a single call — they override `init()`'s tags (and each
130
+ other) on a key collision:
129
131
 
130
132
  ```python
131
133
  devalerts.report(extra={"request_id": "abc123"})
134
+ ```
135
+
136
+ Used as a decorator, `capture()` tags the alert with the wrapped function's
137
+ name as `job` automatically — no `extra` needed:
138
+
139
+ ```python
140
+ @devalerts.capture()
141
+ def nightly_sync(): ...
142
+ ```
132
143
 
133
- @devalerts.capture(extra={"job": "nightly-sync"})
134
- def run_job(): ...
135
144
  ```
145
+ 🔴 ValueError: boom
146
+ 🖥️ prod-web-2 (job=nightly_sync)
147
+ ```
148
+
149
+ Pass `extra={"job": "..."}` explicitly to override the auto-detected name.
136
150
 
137
151
  ## Manually reporting a caught exception
138
152
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "devalerts"
3
- version = "0.2.1"
3
+ version = "0.2.3"
4
4
  description = "Send unhandled Python exceptions straight to a Telegram chat. No backend, no account, no database — just your own bot token."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -130,17 +130,29 @@ def report(
130
130
  class capture(contextlib.ContextDecorator):
131
131
  """Context manager / decorator: report any exception raised inside the block or
132
132
  function, then re-raise it. Use ``@capture()`` on a function instead of wrapping
133
- its body in a manual ``try/except`` or ``with`` block."""
133
+ its body in a manual ``try/except`` or ``with`` block.
134
+
135
+ Used as a decorator, the wrapped function's name is tagged automatically as
136
+ ``job`` -- no need to pass ``extra={"job": ...}`` by hand. Used as a bare
137
+ ``with capture():`` block, there's no function to name, so only explicit
138
+ ``extra`` tags apply."""
134
139
 
135
140
  def __init__(self, *, extra: dict[str, str] | None = None) -> None:
136
141
  self._extra = extra
142
+ self._job_name: str | None = None
143
+
144
+ def __call__(self, func):
145
+ self._job_name = func.__qualname__
146
+ return super().__call__(func)
137
147
 
138
148
  def __enter__(self) -> "capture":
139
149
  return self
140
150
 
141
151
  def __exit__(self, exc_type, exc_value, tb) -> Literal[False]:
142
152
  if exc_type is not None:
143
- _send_exception(exc_type, exc_value, tb, extra=self._extra)
153
+ tags = {"job": self._job_name} if self._job_name else {}
154
+ tags.update(self._extra or {})
155
+ _send_exception(exc_type, exc_value, tb, extra=tags)
144
156
  return False
145
157
 
146
158
 
@@ -27,7 +27,7 @@ _LOCATION_WIDTH = 34
27
27
  def _supports_unicode() -> bool:
28
28
  encoding = getattr(sys.stdout, "encoding", None) or ""
29
29
  try:
30
- "─●…".encode(encoding)
30
+ "─●…×".encode(encoding)
31
31
  return True
32
32
  except (LookupError, UnicodeEncodeError, TypeError):
33
33
  return False
@@ -153,6 +153,7 @@ def _dashboard(as_json: bool = False) -> int:
153
153
  sep_char = "─" if unicode_ok else "-"
154
154
  dot_char = "●" if unicode_ok else "*"
155
155
  ellipsis = "…" if unicode_ok else "..."
156
+ times_char = "×" if unicode_ok else "x"
156
157
 
157
158
  style = _Style(_color_enabled())
158
159
  type_width = max(len("TYPE"), *(len(r[1]) for r in rows))
@@ -178,7 +179,9 @@ def _dashboard(as_json: bool = False) -> int:
178
179
  muted,
179
180
  backoff_multiplier,
180
181
  ) in rows:
181
- backoff_suffix = f" ×{backoff_multiplier}" if backoff_multiplier > 1 else ""
182
+ backoff_suffix = (
183
+ f" {times_char}{backoff_multiplier}" if backoff_multiplier > 1 else ""
184
+ )
182
185
  if muted:
183
186
  muted_count += 1
184
187
  status = style.dim(f"{dot_char} muted")
File without changes