devalerts 0.1.3__tar.gz → 0.1.5__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.
- devalerts-0.1.5/LICENSE +21 -0
- devalerts-0.1.5/PKG-INFO +226 -0
- devalerts-0.1.5/README.md +199 -0
- devalerts-0.1.5/pyproject.toml +61 -0
- devalerts-0.1.5/src/devalerts/__init__.py +162 -0
- devalerts-0.1.5/src/devalerts/_alert.py +44 -0
- devalerts-0.1.5/src/devalerts/_store.py +98 -0
- devalerts-0.1.5/src/devalerts/_telegram.py +25 -0
- devalerts-0.1.5/src/devalerts/cli.py +156 -0
- devalerts-0.1.3/PKG-INFO +0 -105
- devalerts-0.1.3/README.md +0 -96
- devalerts-0.1.3/pyproject.toml +0 -20
- devalerts-0.1.3/src/devalerts/__init__.py +0 -270
- devalerts-0.1.3/src/devalerts/cli.py +0 -60
- {devalerts-0.1.3 → devalerts-0.1.5}/src/devalerts/py.typed +0 -0
devalerts-0.1.5/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sslinNn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
devalerts-0.1.5/PKG-INFO
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devalerts
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Send unhandled Python exceptions straight to a Telegram chat. No backend, no account, no database — just your own bot token.
|
|
5
|
+
Keywords: telegram,error-tracking,exception-monitoring,alerts,logging,monitoring,asgi
|
|
6
|
+
Author: sslinNn
|
|
7
|
+
Author-email: sslinNn <morison1991@mail.ru>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Project-URL: Homepage, https://github.com/sslinNn/devalerts
|
|
24
|
+
Project-URL: Repository, https://github.com/sslinNn/devalerts
|
|
25
|
+
Project-URL: Bug Tracker, https://github.com/sslinNn/devalerts/issues
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# devalerts
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/devalerts/)
|
|
31
|
+
[](https://pypi.org/project/devalerts/)
|
|
32
|
+
[](https://pypi.org/project/devalerts/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
[](https://github.com/sslinNn/devalerts/actions/workflows/test.yml)
|
|
35
|
+
[](https://github.com/astral-sh/ruff)
|
|
36
|
+
[](https://mypy-lang.org/)
|
|
37
|
+
|
|
38
|
+
[Русская версия](README.ru.md)
|
|
39
|
+
|
|
40
|
+
Send unhandled Python exceptions straight to a Telegram chat — the moment
|
|
41
|
+
they happen, on your phone. No backend, no account, no database — just your
|
|
42
|
+
own bot token.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import devalerts
|
|
46
|
+
|
|
47
|
+
devalerts.init(bot_token="123456:ABC-DEF...", chat_id=123456789)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's the whole setup. Two minutes with [@BotFather](https://t.me/BotFather)
|
|
51
|
+
and every unhandled crash — including ones raised in threads — lands in your
|
|
52
|
+
chat instead of a log file nobody's watching.
|
|
53
|
+
|
|
54
|
+
## Why devalerts
|
|
55
|
+
|
|
56
|
+
- **Zero infrastructure.** No SaaS signup, no ingestion server, no API key
|
|
57
|
+
to manage beyond your own Telegram bot token. State lives in a local
|
|
58
|
+
SQLite file you already own.
|
|
59
|
+
- **One line to install, one line to wire up.** `init()` installs the hook
|
|
60
|
+
and gets out of the way.
|
|
61
|
+
- **Not spam.** Errors are grouped by fingerprint and rate-limited per
|
|
62
|
+
group, so a crash loop sends one message, not a thousand.
|
|
63
|
+
- **Framework-aware.** Ships an ASGI middleware for FastAPI/Starlette apps,
|
|
64
|
+
where the default excepthook would never even see a request error.
|
|
65
|
+
- **Small and typed.** No dependencies, ships `py.typed`, ~450 lines total —
|
|
66
|
+
short enough to read in one sitting before you trust it with your errors.
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
uv add devalerts
|
|
71
|
+
|
|
72
|
+
(or `pip install devalerts` if you're not using uv)
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
1. Create a bot with [@BotFather](https://t.me/BotFather) and get its token.
|
|
77
|
+
2. Message your bot once (or add it to a group) so it's allowed to message you back.
|
|
78
|
+
3. Get your chat id — message [@userinfobot](https://t.me/userinfobot), or call
|
|
79
|
+
`https://api.telegram.org/bot<TOKEN>/getUpdates` after step 2 and read `message.chat.id`.
|
|
80
|
+
4. In your app, as early as possible:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import devalerts
|
|
84
|
+
|
|
85
|
+
devalerts.init(bot_token="123456:ABC-DEF...", chat_id=123456789)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
That's it — any unhandled exception (including ones raised in threads) now
|
|
89
|
+
also lands in your Telegram chat.
|
|
90
|
+
|
|
91
|
+
## Grouping, rate limiting, and the dashboard
|
|
92
|
+
|
|
93
|
+
Exceptions are grouped by fingerprint (exception type + file + line where it
|
|
94
|
+
was raised) in a local SQLite file (`~/.devalerts/state.db`). Each group
|
|
95
|
+
sends at most one Telegram message per `rate_limit_seconds` (default 300);
|
|
96
|
+
repeats inside that window are counted but not sent, and the next message
|
|
97
|
+
for that group says how many were skipped. Old groups (untouched for 7 days)
|
|
98
|
+
are pruned automatically. Configure the window via `init()`:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
devalerts.init(bot_token="...", chat_id=123456789, rate_limit_seconds=60)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
See what's grouped and what's currently rate-limited:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
uv run devalerts dashboard
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
## Manually reporting a caught exception
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
try:
|
|
116
|
+
risky_call()
|
|
117
|
+
except Exception:
|
|
118
|
+
devalerts.report() # sends the currently-handled exception
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
or:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
with devalerts.capture():
|
|
125
|
+
risky_call() # reports on exception, then re-raises
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`capture` also works as a decorator, so you don't need to touch a function's
|
|
129
|
+
body at all:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
@devalerts.capture()
|
|
133
|
+
def risky_call():
|
|
134
|
+
...
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## FastAPI / Starlette / any ASGI app
|
|
138
|
+
|
|
139
|
+
`init()`'s excepthook won't see request errors — the framework already catches
|
|
140
|
+
them internally to return a 500 response, so nothing "unhandled" ever reaches
|
|
141
|
+
the process. Use the ASGI middleware instead:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
app.add_middleware(devalerts.ASGIMiddleware)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Only exceptions that actually escape as server errors get reported — routing
|
|
148
|
+
404s and raised `HTTPException`s are already turned into responses by the
|
|
149
|
+
framework before the middleware sees them.
|
|
150
|
+
|
|
151
|
+
## Why not Sentry?
|
|
152
|
+
|
|
153
|
+
If you already run Sentry/Rollbar/etc., keep using it — devalerts isn't a
|
|
154
|
+
replacement. It's for the side project, internal tool, or small service that
|
|
155
|
+
doesn't have (and doesn't want) that infrastructure: no account to create, no
|
|
156
|
+
SDK to configure, no server to trust — just a bot token you already control.
|
|
157
|
+
|
|
158
|
+
| | devalerts | Sentry-style tracker |
|
|
159
|
+
|--------------------------|---------------------|-----------------------|
|
|
160
|
+
| Setup | one bot token | account + project + SDK config |
|
|
161
|
+
| Backend | none — Telegram only | hosted or self-hosted service |
|
|
162
|
+
| Where alerts land | your Telegram chat | a web dashboard |
|
|
163
|
+
| Grouping / rate limiting | yes, local SQLite | yes, server-side |
|
|
164
|
+
| Search, trends, releases | no | yes |
|
|
165
|
+
|
|
166
|
+
## FAQ
|
|
167
|
+
|
|
168
|
+
**Works with FastAPI / Starlette?**
|
|
169
|
+
Yes — use [`ASGIMiddleware`](#fastapi--starlette--any-asgi-app), since the
|
|
170
|
+
default excepthook never sees request errors.
|
|
171
|
+
|
|
172
|
+
**Works with Docker?**
|
|
173
|
+
Yes, nothing container-specific. Just make sure `~/.devalerts/` (the dedup
|
|
174
|
+
state file) is either writable inside the container or a mounted volume if
|
|
175
|
+
you want dedup state to survive restarts — it self-recreates otherwise.
|
|
176
|
+
|
|
177
|
+
**Works with threads?**
|
|
178
|
+
Yes — `init()` installs both `sys.excepthook` and `threading.excepthook`.
|
|
179
|
+
|
|
180
|
+
**Works with Celery / background workers?**
|
|
181
|
+
Yes, `init()` in the worker process the same way as in a web process. For
|
|
182
|
+
tasks you catch yourself, use `report()` or `@devalerts.capture()`.
|
|
183
|
+
|
|
184
|
+
**Works on Windows / Linux / macOS?**
|
|
185
|
+
Yes — stdlib only (`urllib`, `sqlite3`, `threading`), no OS-specific code
|
|
186
|
+
paths.
|
|
187
|
+
|
|
188
|
+
## Privacy & Security
|
|
189
|
+
|
|
190
|
+
- The only network call devalerts makes is to `api.telegram.org` — no
|
|
191
|
+
telemetry, no analytics, nothing else phones home.
|
|
192
|
+
- No third-party server and no devalerts-run backend — messages go straight
|
|
193
|
+
from your process to your own Telegram bot.
|
|
194
|
+
- No accounts, no signup, no API key beyond the bot token you create and
|
|
195
|
+
control yourself.
|
|
196
|
+
- Basic secret redaction only (a few common token/key patterns) — do not
|
|
197
|
+
rely on this for sensitive production data; scrub what you can before it
|
|
198
|
+
ever reaches an exception message.
|
|
199
|
+
|
|
200
|
+
## What this does NOT do (by design)
|
|
201
|
+
|
|
202
|
+
- Grouping/rate limiting is local and in-process only (SQLite file, no
|
|
203
|
+
server) — the dashboard is a CLI table, not a web UI.
|
|
204
|
+
- No backend, no accounts — each user runs their own bot.
|
|
205
|
+
|
|
206
|
+
## Roadmap
|
|
207
|
+
|
|
208
|
+
- Web dashboard (hosted, optional — the local CLI dashboard stays either way)
|
|
209
|
+
- Slack delivery
|
|
210
|
+
- Discord delivery
|
|
211
|
+
- Email delivery
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
uv sync --group dev
|
|
216
|
+
uv run pre-commit install
|
|
217
|
+
|
|
218
|
+
`pre-commit` runs `ruff check`, `ruff format`, and `mypy` before each commit.
|
|
219
|
+
Run the full check manually with:
|
|
220
|
+
|
|
221
|
+
uv run pytest
|
|
222
|
+
uv run pre-commit run --all-files
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# devalerts
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/devalerts/)
|
|
4
|
+
[](https://pypi.org/project/devalerts/)
|
|
5
|
+
[](https://pypi.org/project/devalerts/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/sslinNn/devalerts/actions/workflows/test.yml)
|
|
8
|
+
[](https://github.com/astral-sh/ruff)
|
|
9
|
+
[](https://mypy-lang.org/)
|
|
10
|
+
|
|
11
|
+
[Русская версия](README.ru.md)
|
|
12
|
+
|
|
13
|
+
Send unhandled Python exceptions straight to a Telegram chat — the moment
|
|
14
|
+
they happen, on your phone. No backend, no account, no database — just your
|
|
15
|
+
own bot token.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import devalerts
|
|
19
|
+
|
|
20
|
+
devalerts.init(bot_token="123456:ABC-DEF...", chat_id=123456789)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That's the whole setup. Two minutes with [@BotFather](https://t.me/BotFather)
|
|
24
|
+
and every unhandled crash — including ones raised in threads — lands in your
|
|
25
|
+
chat instead of a log file nobody's watching.
|
|
26
|
+
|
|
27
|
+
## Why devalerts
|
|
28
|
+
|
|
29
|
+
- **Zero infrastructure.** No SaaS signup, no ingestion server, no API key
|
|
30
|
+
to manage beyond your own Telegram bot token. State lives in a local
|
|
31
|
+
SQLite file you already own.
|
|
32
|
+
- **One line to install, one line to wire up.** `init()` installs the hook
|
|
33
|
+
and gets out of the way.
|
|
34
|
+
- **Not spam.** Errors are grouped by fingerprint and rate-limited per
|
|
35
|
+
group, so a crash loop sends one message, not a thousand.
|
|
36
|
+
- **Framework-aware.** Ships an ASGI middleware for FastAPI/Starlette apps,
|
|
37
|
+
where the default excepthook would never even see a request error.
|
|
38
|
+
- **Small and typed.** No dependencies, ships `py.typed`, ~450 lines total —
|
|
39
|
+
short enough to read in one sitting before you trust it with your errors.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
uv add devalerts
|
|
44
|
+
|
|
45
|
+
(or `pip install devalerts` if you're not using uv)
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
1. Create a bot with [@BotFather](https://t.me/BotFather) and get its token.
|
|
50
|
+
2. Message your bot once (or add it to a group) so it's allowed to message you back.
|
|
51
|
+
3. Get your chat id — message [@userinfobot](https://t.me/userinfobot), or call
|
|
52
|
+
`https://api.telegram.org/bot<TOKEN>/getUpdates` after step 2 and read `message.chat.id`.
|
|
53
|
+
4. In your app, as early as possible:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import devalerts
|
|
57
|
+
|
|
58
|
+
devalerts.init(bot_token="123456:ABC-DEF...", chat_id=123456789)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That's it — any unhandled exception (including ones raised in threads) now
|
|
62
|
+
also lands in your Telegram chat.
|
|
63
|
+
|
|
64
|
+
## Grouping, rate limiting, and the dashboard
|
|
65
|
+
|
|
66
|
+
Exceptions are grouped by fingerprint (exception type + file + line where it
|
|
67
|
+
was raised) in a local SQLite file (`~/.devalerts/state.db`). Each group
|
|
68
|
+
sends at most one Telegram message per `rate_limit_seconds` (default 300);
|
|
69
|
+
repeats inside that window are counted but not sent, and the next message
|
|
70
|
+
for that group says how many were skipped. Old groups (untouched for 7 days)
|
|
71
|
+
are pruned automatically. Configure the window via `init()`:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
devalerts.init(bot_token="...", chat_id=123456789, rate_limit_seconds=60)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
See what's grouped and what's currently rate-limited:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
uv run devalerts dashboard
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+

|
|
84
|
+
|
|
85
|
+
## Manually reporting a caught exception
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
try:
|
|
89
|
+
risky_call()
|
|
90
|
+
except Exception:
|
|
91
|
+
devalerts.report() # sends the currently-handled exception
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
or:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
with devalerts.capture():
|
|
98
|
+
risky_call() # reports on exception, then re-raises
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`capture` also works as a decorator, so you don't need to touch a function's
|
|
102
|
+
body at all:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
@devalerts.capture()
|
|
106
|
+
def risky_call():
|
|
107
|
+
...
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## FastAPI / Starlette / any ASGI app
|
|
111
|
+
|
|
112
|
+
`init()`'s excepthook won't see request errors — the framework already catches
|
|
113
|
+
them internally to return a 500 response, so nothing "unhandled" ever reaches
|
|
114
|
+
the process. Use the ASGI middleware instead:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
app.add_middleware(devalerts.ASGIMiddleware)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Only exceptions that actually escape as server errors get reported — routing
|
|
121
|
+
404s and raised `HTTPException`s are already turned into responses by the
|
|
122
|
+
framework before the middleware sees them.
|
|
123
|
+
|
|
124
|
+
## Why not Sentry?
|
|
125
|
+
|
|
126
|
+
If you already run Sentry/Rollbar/etc., keep using it — devalerts isn't a
|
|
127
|
+
replacement. It's for the side project, internal tool, or small service that
|
|
128
|
+
doesn't have (and doesn't want) that infrastructure: no account to create, no
|
|
129
|
+
SDK to configure, no server to trust — just a bot token you already control.
|
|
130
|
+
|
|
131
|
+
| | devalerts | Sentry-style tracker |
|
|
132
|
+
|--------------------------|---------------------|-----------------------|
|
|
133
|
+
| Setup | one bot token | account + project + SDK config |
|
|
134
|
+
| Backend | none — Telegram only | hosted or self-hosted service |
|
|
135
|
+
| Where alerts land | your Telegram chat | a web dashboard |
|
|
136
|
+
| Grouping / rate limiting | yes, local SQLite | yes, server-side |
|
|
137
|
+
| Search, trends, releases | no | yes |
|
|
138
|
+
|
|
139
|
+
## FAQ
|
|
140
|
+
|
|
141
|
+
**Works with FastAPI / Starlette?**
|
|
142
|
+
Yes — use [`ASGIMiddleware`](#fastapi--starlette--any-asgi-app), since the
|
|
143
|
+
default excepthook never sees request errors.
|
|
144
|
+
|
|
145
|
+
**Works with Docker?**
|
|
146
|
+
Yes, nothing container-specific. Just make sure `~/.devalerts/` (the dedup
|
|
147
|
+
state file) is either writable inside the container or a mounted volume if
|
|
148
|
+
you want dedup state to survive restarts — it self-recreates otherwise.
|
|
149
|
+
|
|
150
|
+
**Works with threads?**
|
|
151
|
+
Yes — `init()` installs both `sys.excepthook` and `threading.excepthook`.
|
|
152
|
+
|
|
153
|
+
**Works with Celery / background workers?**
|
|
154
|
+
Yes, `init()` in the worker process the same way as in a web process. For
|
|
155
|
+
tasks you catch yourself, use `report()` or `@devalerts.capture()`.
|
|
156
|
+
|
|
157
|
+
**Works on Windows / Linux / macOS?**
|
|
158
|
+
Yes — stdlib only (`urllib`, `sqlite3`, `threading`), no OS-specific code
|
|
159
|
+
paths.
|
|
160
|
+
|
|
161
|
+
## Privacy & Security
|
|
162
|
+
|
|
163
|
+
- The only network call devalerts makes is to `api.telegram.org` — no
|
|
164
|
+
telemetry, no analytics, nothing else phones home.
|
|
165
|
+
- No third-party server and no devalerts-run backend — messages go straight
|
|
166
|
+
from your process to your own Telegram bot.
|
|
167
|
+
- No accounts, no signup, no API key beyond the bot token you create and
|
|
168
|
+
control yourself.
|
|
169
|
+
- Basic secret redaction only (a few common token/key patterns) — do not
|
|
170
|
+
rely on this for sensitive production data; scrub what you can before it
|
|
171
|
+
ever reaches an exception message.
|
|
172
|
+
|
|
173
|
+
## What this does NOT do (by design)
|
|
174
|
+
|
|
175
|
+
- Grouping/rate limiting is local and in-process only (SQLite file, no
|
|
176
|
+
server) — the dashboard is a CLI table, not a web UI.
|
|
177
|
+
- No backend, no accounts — each user runs their own bot.
|
|
178
|
+
|
|
179
|
+
## Roadmap
|
|
180
|
+
|
|
181
|
+
- Web dashboard (hosted, optional — the local CLI dashboard stays either way)
|
|
182
|
+
- Slack delivery
|
|
183
|
+
- Discord delivery
|
|
184
|
+
- Email delivery
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
uv sync --group dev
|
|
189
|
+
uv run pre-commit install
|
|
190
|
+
|
|
191
|
+
`pre-commit` runs `ruff check`, `ruff format`, and `mypy` before each commit.
|
|
192
|
+
Run the full check manually with:
|
|
193
|
+
|
|
194
|
+
uv run pytest
|
|
195
|
+
uv run pre-commit run --all-files
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "devalerts"
|
|
3
|
+
version = "0.1.5"
|
|
4
|
+
description = "Send unhandled Python exceptions straight to a Telegram chat. No backend, no account, no database — just your own bot token."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "sslinNn", email = "morison1991@mail.ru" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = []
|
|
13
|
+
keywords = [
|
|
14
|
+
"telegram",
|
|
15
|
+
"error-tracking",
|
|
16
|
+
"exception-monitoring",
|
|
17
|
+
"alerts",
|
|
18
|
+
"logging",
|
|
19
|
+
"monitoring",
|
|
20
|
+
"asgi",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
27
|
+
"Programming Language :: Python :: 3.9",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Topic :: Software Development :: Debuggers",
|
|
33
|
+
"Topic :: System :: Monitoring",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/sslinNn/devalerts"
|
|
39
|
+
Repository = "https://github.com/sslinNn/devalerts"
|
|
40
|
+
"Bug Tracker" = "https://github.com/sslinNn/devalerts/issues"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
devalerts = "devalerts.cli:main"
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["uv_build>=0.10.2,<0.12.0"]
|
|
47
|
+
build-backend = "uv_build"
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = [
|
|
51
|
+
"mypy>=1.19.1",
|
|
52
|
+
"pre-commit>=4.3.0",
|
|
53
|
+
"pytest>=8.4.2",
|
|
54
|
+
"ruff>=0.15.22",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
target-version = "py39"
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
extend-select = ["BLE"]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Send unhandled Python exceptions straight to a Telegram chat."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Callable, Literal, Optional, TypedDict
|
|
10
|
+
|
|
11
|
+
from ._alert import _format_alert, _redact
|
|
12
|
+
from ._store import _DEFAULT_RATE_LIMIT_SECONDS, _fingerprint, _should_send
|
|
13
|
+
from ._telegram import _send_telegram_message
|
|
14
|
+
|
|
15
|
+
__all__: list[str] = ["init", "report", "capture", "ASGIMiddleware"]
|
|
16
|
+
|
|
17
|
+
_ExceptHook = Callable[
|
|
18
|
+
[type[BaseException], BaseException, Optional[TracebackType]], object
|
|
19
|
+
]
|
|
20
|
+
_ThreadingExceptHook = Callable[[threading.ExceptHookArgs], object]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _State(TypedDict):
|
|
24
|
+
bot_token: str | None
|
|
25
|
+
chat_id: int | str | None
|
|
26
|
+
redact: bool
|
|
27
|
+
rate_limit_seconds: int
|
|
28
|
+
# Seeded with the real default hooks below (never None) -- _excepthook/
|
|
29
|
+
# _threading_excepthook can only ever fire after init() has replaced
|
|
30
|
+
# sys.excepthook/threading.excepthook, by which point these are always
|
|
31
|
+
# set, so keeping them non-Optional avoids an unreachable None check.
|
|
32
|
+
prev_excepthook: _ExceptHook
|
|
33
|
+
prev_threading_excepthook: _ThreadingExceptHook
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_state: _State = {
|
|
37
|
+
"bot_token": None,
|
|
38
|
+
"chat_id": None,
|
|
39
|
+
"redact": True,
|
|
40
|
+
"rate_limit_seconds": _DEFAULT_RATE_LIMIT_SECONDS,
|
|
41
|
+
"prev_excepthook": sys.excepthook,
|
|
42
|
+
"prev_threading_excepthook": threading.excepthook,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _send_exception(exc_type, exc_value, tb) -> None:
|
|
47
|
+
if _state["bot_token"] is None or _state["chat_id"] is None:
|
|
48
|
+
print("devalerts: init() was not called, dropping alert", file=sys.stderr)
|
|
49
|
+
return
|
|
50
|
+
fingerprint, location = _fingerprint(exc_type, tb)
|
|
51
|
+
send, skipped = _should_send(
|
|
52
|
+
fingerprint, exc_type.__name__, location, _state["rate_limit_seconds"]
|
|
53
|
+
)
|
|
54
|
+
if not send:
|
|
55
|
+
return
|
|
56
|
+
message = _format_alert(exc_type, exc_value, tb, skipped=skipped)
|
|
57
|
+
if _state["redact"]:
|
|
58
|
+
message = _redact(message)
|
|
59
|
+
_send_telegram_message(_state["bot_token"], _state["chat_id"], message)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _excepthook(exc_type, exc_value, tb) -> None:
|
|
63
|
+
if exc_type is not KeyboardInterrupt:
|
|
64
|
+
try:
|
|
65
|
+
_send_exception(exc_type, exc_value, tb)
|
|
66
|
+
except Exception as error: # noqa: BLE001 - crash handler must never raise
|
|
67
|
+
print(
|
|
68
|
+
f"devalerts: internal error while sending alert: {error}",
|
|
69
|
+
file=sys.stderr,
|
|
70
|
+
)
|
|
71
|
+
_state["prev_excepthook"](exc_type, exc_value, tb)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _threading_excepthook(args) -> None:
|
|
75
|
+
try:
|
|
76
|
+
_send_exception(args.exc_type, args.exc_value, args.exc_traceback)
|
|
77
|
+
except Exception as error: # noqa: BLE001
|
|
78
|
+
print(
|
|
79
|
+
f"devalerts: internal error while sending alert: {error}", file=sys.stderr
|
|
80
|
+
)
|
|
81
|
+
_state["prev_threading_excepthook"](args)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def init(
|
|
85
|
+
bot_token: str,
|
|
86
|
+
chat_id: int | str,
|
|
87
|
+
*,
|
|
88
|
+
redact: bool = True,
|
|
89
|
+
rate_limit_seconds: int = _DEFAULT_RATE_LIMIT_SECONDS,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Install a global exception hook that sends unhandled exceptions to Telegram."""
|
|
92
|
+
_state["bot_token"] = bot_token
|
|
93
|
+
_state["chat_id"] = chat_id
|
|
94
|
+
_state["redact"] = redact
|
|
95
|
+
_state["rate_limit_seconds"] = rate_limit_seconds
|
|
96
|
+
# ponytail: guard against calling init() twice capturing our own hook as
|
|
97
|
+
# "previous" -- that would make _excepthook chain to itself and recurse
|
|
98
|
+
# forever on the next crash, violating "must never raise".
|
|
99
|
+
if sys.excepthook is not _excepthook:
|
|
100
|
+
_state["prev_excepthook"] = sys.excepthook
|
|
101
|
+
if threading.excepthook is not _threading_excepthook:
|
|
102
|
+
_state["prev_threading_excepthook"] = threading.excepthook
|
|
103
|
+
sys.excepthook = _excepthook
|
|
104
|
+
threading.excepthook = _threading_excepthook
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def report(exc: BaseException | None = None) -> None:
|
|
108
|
+
"""Manually send a caught exception to Telegram."""
|
|
109
|
+
if exc is None:
|
|
110
|
+
exc_type, exc_value, tb = sys.exc_info()
|
|
111
|
+
if exc_type is None:
|
|
112
|
+
raise RuntimeError(
|
|
113
|
+
"report() requires an active exception or an exc argument"
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
exc_type, exc_value, tb = type(exc), exc, exc.__traceback__
|
|
117
|
+
_send_exception(exc_type, exc_value, tb)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class capture(contextlib.ContextDecorator):
|
|
121
|
+
"""Context manager / decorator: report any exception raised inside the block or
|
|
122
|
+
function, then re-raise it. Use ``@capture()`` on a function instead of wrapping
|
|
123
|
+
its body in a manual ``try/except`` or ``with`` block."""
|
|
124
|
+
|
|
125
|
+
def __enter__(self) -> "capture":
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
def __exit__(self, exc_type, exc_value, tb) -> Literal[False]:
|
|
129
|
+
if exc_type is not None:
|
|
130
|
+
_send_exception(exc_type, exc_value, tb)
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ASGIMiddleware:
|
|
135
|
+
"""ASGI middleware for FastAPI/Starlette (or any ASGI app): report any exception
|
|
136
|
+
that escapes a request, then re-raise it so the framework's own error handling
|
|
137
|
+
still runs unchanged.
|
|
138
|
+
|
|
139
|
+
Usage::
|
|
140
|
+
|
|
141
|
+
app.add_middleware(devalerts.ASGIMiddleware)
|
|
142
|
+
|
|
143
|
+
Only exceptions that actually reach here (unhandled server errors) get reported —
|
|
144
|
+
routing 404s and raised ``HTTPException``s are already turned into responses by
|
|
145
|
+
the framework before this middleware sees them, same as Sentry's ASGI integration.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, app) -> None:
|
|
149
|
+
self.app = app
|
|
150
|
+
|
|
151
|
+
async def __call__(self, scope, receive, send) -> None:
|
|
152
|
+
try:
|
|
153
|
+
await self.app(scope, receive, send)
|
|
154
|
+
except Exception:
|
|
155
|
+
exc_type, exc_value, tb = sys.exc_info()
|
|
156
|
+
# ponytail: fire-and-forget in a thread -- _send_exception is a
|
|
157
|
+
# blocking network call; awaiting it here would stall the event
|
|
158
|
+
# loop (and the error response) for up to _TIMEOUT_SECONDS.
|
|
159
|
+
threading.Thread(
|
|
160
|
+
target=_send_exception, args=(exc_type, exc_value, tb), daemon=True
|
|
161
|
+
).start()
|
|
162
|
+
raise
|