pynotifyhub 0.1.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.
- pynotifyhub-0.1.0/.claude/settings.local.json +37 -0
- pynotifyhub-0.1.0/.env.example +35 -0
- pynotifyhub-0.1.0/.github/workflows/ci.yml +33 -0
- pynotifyhub-0.1.0/.gitignore +33 -0
- pynotifyhub-0.1.0/CHANGELOG.md +27 -0
- pynotifyhub-0.1.0/LICENSE +21 -0
- pynotifyhub-0.1.0/PKG-INFO +223 -0
- pynotifyhub-0.1.0/README.md +190 -0
- pynotifyhub-0.1.0/docs/usage.md +444 -0
- pynotifyhub-0.1.0/examples/async_app.py +38 -0
- pynotifyhub-0.1.0/examples/notify-hub.example.toml +96 -0
- pynotifyhub-0.1.0/examples/smoke_test.py +127 -0
- pynotifyhub-0.1.0/examples/sync_app.py +54 -0
- pynotifyhub-0.1.0/pyproject.toml +84 -0
- pynotifyhub-0.1.0/src/notify_hub/__init__.py +75 -0
- pynotifyhub-0.1.0/src/notify_hub/_util.py +79 -0
- pynotifyhub-0.1.0/src/notify_hub/capabilities.py +32 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/__init__.py +19 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/base.py +93 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/dingtalk.py +82 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/discord.py +51 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/lark.py +166 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/phone/__init__.py +7 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/phone/base.py +34 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/phone/channel.py +66 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/phone/twilio.py +65 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/slack.py +110 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/telegram.py +125 -0
- pynotifyhub-0.1.0/src/notify_hub/channels/wecom.py +106 -0
- pynotifyhub-0.1.0/src/notify_hub/config.py +233 -0
- pynotifyhub-0.1.0/src/notify_hub/exceptions.py +40 -0
- pynotifyhub-0.1.0/src/notify_hub/guard.py +58 -0
- pynotifyhub-0.1.0/src/notify_hub/hub.py +345 -0
- pynotifyhub-0.1.0/src/notify_hub/levels.py +75 -0
- pynotifyhub-0.1.0/src/notify_hub/logging.py +124 -0
- pynotifyhub-0.1.0/src/notify_hub/message.py +112 -0
- pynotifyhub-0.1.0/src/notify_hub/py.typed +0 -0
- pynotifyhub-0.1.0/src/notify_hub/registry.py +65 -0
- pynotifyhub-0.1.0/src/notify_hub/results.py +75 -0
- pynotifyhub-0.1.0/src/notify_hub/router.py +62 -0
- pynotifyhub-0.1.0/src/notify_hub/runtime.py +70 -0
- pynotifyhub-0.1.0/tests/__init__.py +0 -0
- pynotifyhub-0.1.0/tests/channels/__init__.py +0 -0
- pynotifyhub-0.1.0/tests/channels/test_dingtalk.py +94 -0
- pynotifyhub-0.1.0/tests/channels/test_discord.py +74 -0
- pynotifyhub-0.1.0/tests/channels/test_lark.py +190 -0
- pynotifyhub-0.1.0/tests/channels/test_phone_twilio.py +100 -0
- pynotifyhub-0.1.0/tests/channels/test_slack.py +128 -0
- pynotifyhub-0.1.0/tests/channels/test_telegram.py +111 -0
- pynotifyhub-0.1.0/tests/channels/test_wecom.py +113 -0
- pynotifyhub-0.1.0/tests/conftest.py +49 -0
- pynotifyhub-0.1.0/tests/test_config.py +230 -0
- pynotifyhub-0.1.0/tests/test_degrade.py +56 -0
- pynotifyhub-0.1.0/tests/test_guard.py +53 -0
- pynotifyhub-0.1.0/tests/test_hub.py +251 -0
- pynotifyhub-0.1.0/tests/test_levels.py +45 -0
- pynotifyhub-0.1.0/tests/test_lifecycle.py +54 -0
- pynotifyhub-0.1.0/tests/test_logging.py +91 -0
- pynotifyhub-0.1.0/tests/test_message.py +64 -0
- pynotifyhub-0.1.0/tests/test_public_api.py +18 -0
- pynotifyhub-0.1.0/tests/test_redact.py +35 -0
- pynotifyhub-0.1.0/tests/test_registry.py +48 -0
- pynotifyhub-0.1.0/tests/test_router.py +61 -0
- pynotifyhub-0.1.0/tests/test_runtime.py +65 -0
- pynotifyhub-0.1.0/tests/test_util.py +46 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(git -C /home/ubuntu/workspace/tmp/btcdom_arb status -sb)",
|
|
5
|
+
"Bash(git -C /home/ubuntu/workspace/tmp/btcdom_arb log --oneline -1)",
|
|
6
|
+
"Bash(pip3 install *)",
|
|
7
|
+
"Bash(python3 -m pytest -q)",
|
|
8
|
+
"Bash(NH_DISCORD_URL=\"https://discord.com/api/webhooks/1490400357577392239/eI3NisN0oG5G5QNPVj9kxOQp114vtPv17BKyUDJ8BVFZGyyNhtYHwwaUUJMC0EVLPuoV\" NH_SLACK_URL=\"https://hooks.slack.com/services/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\" NH_LARK_URL=\"https://open.larksuite.com/open-apis/bot/v2/hook/de57d6a6-d8bb-4335-a700-bfd674682175\" python3 /tmp/nh_smoke.py)",
|
|
9
|
+
"Bash(NH_LARK_URL=\"https://open.larksuite.com/open-apis/bot/v2/hook/de57d6a6-d8bb-4335-a700-bfd674682175\" python3 *)",
|
|
10
|
+
"Bash(DISCORD_WEBHOOK_URL=\"https://discord.com/api/webhooks/1490400357577392239/eI3NisN0oG5G5QNPVj9kxOQp114vtPv17BKyUDJ8BVFZGyyNhtYHwwaUUJMC0EVLPuoV\" SLACK_WEBHOOK_URL=\"https://hooks.slack.com/services/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\" LARK_WEBHOOK_URL=\"https://open.larksuite.com/open-apis/bot/v2/hook/de57d6a6-d8bb-4335-a700-bfd674682175\" python3 app.py)",
|
|
11
|
+
"Bash(python3 -)",
|
|
12
|
+
"Bash(git add *)",
|
|
13
|
+
"Bash(git commit -q -m 'Add Chinese usage guide with 30-second quickstart in docs/ *)",
|
|
14
|
+
"Bash(git push *)",
|
|
15
|
+
"Bash(gh auth *)",
|
|
16
|
+
"Bash(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -T git@github.com)",
|
|
17
|
+
"Bash(ssh -o BatchMode=yes -i ~/.ssh/id_ed25519_allen -T git@github.com)",
|
|
18
|
+
"Bash(git config *)",
|
|
19
|
+
"Bash(git remote *)",
|
|
20
|
+
"Bash(sed -i 's|^DISCORD_WEBHOOK_URL=$|DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1490400357577392239/eI3NisN0oG5G5QNPVj9kxOQp114vtPv17BKyUDJ8BVFZGyyNhtYHwwaUUJMC0EVLPuoV|; s|^SLACK_WEBHOOK_URL=$|SLACK_WEBHOOK_URL=https://hooks.slack.com/services/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|; s|^LARK_WEBHOOK_URL=.*实际值.*|LARK_WEBHOOK_URL=|; s|^LARK_WEBHOOK_URL=$|LARK_WEBHOOK_URL=https://open.larksuite.com/open-apis/bot/v2/hook/de57d6a6-d8bb-4335-a700-bfd674682175|' .env)",
|
|
21
|
+
"Bash(git check-ignore *)",
|
|
22
|
+
"Bash(set -o pipefail)",
|
|
23
|
+
"Bash(python3 -m ruff format .)",
|
|
24
|
+
"Bash(python3 -m ruff check --fix .)",
|
|
25
|
+
"Bash(python3 -m mypy)",
|
|
26
|
+
"Bash(python3 -m ruff check .)",
|
|
27
|
+
"Bash(git commit -q -m 'Fix pre-release review findings: client leak, close race, secret redaction *)",
|
|
28
|
+
"Bash(sed -n '210,225p' docs/usage.md)",
|
|
29
|
+
"Bash(sed -n '314,320p' docs/usage.md)",
|
|
30
|
+
"Bash(git commit -q -m 'Add Lark image support via app credentials; accept WeCom webhook URL forms *)",
|
|
31
|
+
"Bash(python3 -m build)",
|
|
32
|
+
"Bash(python3 -m twine check dist/*)",
|
|
33
|
+
"Bash(python3 -m twine upload dist/*)",
|
|
34
|
+
"Bash(python3 -m twine upload --verbose dist/notify_hub-0.1.0-py3-none-any.whl)"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# notify-hub 本地验证用的凭据模板。
|
|
2
|
+
# 用法:cp .env.example .env,然后在 .env 里填真实值。
|
|
3
|
+
# .env 已在 .gitignore 中,永远不会被提交。
|
|
4
|
+
# 不用的渠道留空即可。
|
|
5
|
+
|
|
6
|
+
# ── Discord ───────────────────────────────────────────────
|
|
7
|
+
DISCORD_WEBHOOK_URL=
|
|
8
|
+
|
|
9
|
+
# ── Slack ─────────────────────────────────────────────────
|
|
10
|
+
SLACK_WEBHOOK_URL=
|
|
11
|
+
SLACK_BOT_TOKEN= # xoxb- 开头;配了才能测本地图片/文件上传
|
|
12
|
+
SLACK_FILE_CHANNEL= # 文件上传目标频道 ID,如 C0123456789
|
|
13
|
+
|
|
14
|
+
# ── Telegram ──────────────────────────────────────────────
|
|
15
|
+
TELEGRAM_BOT_TOKEN=
|
|
16
|
+
TELEGRAM_CHAT_ID=
|
|
17
|
+
|
|
18
|
+
# ── 企业微信 WeCom ────────────────────────────────────────
|
|
19
|
+
WECOM_ROBOT_KEY= # webhook 地址里 key= 后面那串
|
|
20
|
+
|
|
21
|
+
# ── 钉钉 DingTalk ─────────────────────────────────────────
|
|
22
|
+
DINGTALK_TOKEN= # webhook 地址里 access_token= 后面那串
|
|
23
|
+
DINGTALK_SECRET= # 安全设置「加签」的 SEC 开头密钥
|
|
24
|
+
|
|
25
|
+
# ── 飞书 / Lark ───────────────────────────────────────────
|
|
26
|
+
LARK_WEBHOOK_URL= # 完整 webhook URL
|
|
27
|
+
LARK_SECRET= # 可选:签名校验密钥
|
|
28
|
+
LARK_APP_ID= # 自建应用凭据;配了才能发图片
|
|
29
|
+
LARK_APP_SECRET=
|
|
30
|
+
|
|
31
|
+
# ── 电话 Twilio ───────────────────────────────────────────
|
|
32
|
+
TWILIO_ACCOUNT_SID=
|
|
33
|
+
TWILIO_AUTH_TOKEN=
|
|
34
|
+
TWILIO_FROM_NUMBER= # 你的 Twilio 号码,如 +15550001111
|
|
35
|
+
TWILIO_TO_NUMBER= # 接听测试电话的手机号
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- run: pip install -e ".[dev]"
|
|
17
|
+
- run: ruff check .
|
|
18
|
+
- run: ruff format --check .
|
|
19
|
+
- run: mypy
|
|
20
|
+
|
|
21
|
+
test:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
strategy:
|
|
24
|
+
fail-fast: false
|
|
25
|
+
matrix:
|
|
26
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python-version }}
|
|
32
|
+
- run: pip install -e ".[dev]"
|
|
33
|
+
- run: pytest -q
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# Tooling caches
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
htmlcov/
|
|
17
|
+
|
|
18
|
+
# Editors
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
|
|
23
|
+
# notify-hub runtime artifacts
|
|
24
|
+
notify-hub.log
|
|
25
|
+
*.jsonl
|
|
26
|
+
|
|
27
|
+
# Secrets — NEVER commit
|
|
28
|
+
.env
|
|
29
|
+
.env.local
|
|
30
|
+
*.secret
|
|
31
|
+
|
|
32
|
+
# OS
|
|
33
|
+
.DS_Store
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-06-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release: unified notification hub with level-based routing.
|
|
15
|
+
- Channels: Slack, Discord, Telegram, WeCom (企业微信), DingTalk (钉钉),
|
|
16
|
+
Lark/Feishu (飞书), and voice calls via Twilio.
|
|
17
|
+
- Async non-blocking `notify()` that works in both sync and asyncio host apps.
|
|
18
|
+
- Attachment support (image/file from path, bytes, or URL) with per-channel
|
|
19
|
+
capability declarations and graceful degradation.
|
|
20
|
+
- Structured notification log (text / JSON Lines, file or stdlib logger).
|
|
21
|
+
- Anti-storm protection: fingerprint dedup window and per-channel rate limiting.
|
|
22
|
+
- TOML / dict configuration with `${ENV_VAR}` secret expansion.
|
|
23
|
+
- Lark image sending via optional `app_id`/`app_secret` (tenant token with
|
|
24
|
+
caching, image upload, image_key message).
|
|
25
|
+
- WeCom accepts a bare key, the full webhook URL, or any `key=` fragment.
|
|
26
|
+
- Credential redaction: tokens embedded in URLs are masked before error
|
|
27
|
+
strings reach results and the audit log.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Allen-zjx
|
|
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.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pynotifyhub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified multi-channel notification library: Slack, Discord, Telegram, WeCom, DingTalk, Lark/Feishu, and voice calls with level-based routing.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Allen-zjx/notify-hub
|
|
6
|
+
Project-URL: Repository, https://github.com/Allen-zjx/notify-hub
|
|
7
|
+
Project-URL: Changelog, https://github.com/Allen-zjx/notify-hub/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Allen-zjx
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: alert,dingtalk,discord,feishu,lark,notification,slack,telegram,twilio,wecom
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Communications
|
|
21
|
+
Classifier: Topic :: System :: Monitoring
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# notify-hub
|
|
35
|
+
|
|
36
|
+
Unified multi-channel notification library for Python.
|
|
37
|
+
|
|
38
|
+
Send alerts to **Slack, Discord, Telegram, WeCom (企业微信), DingTalk (钉钉),
|
|
39
|
+
Lark/Feishu (飞书), and phone voice calls (Twilio)** through one API, with
|
|
40
|
+
level-based routing configured per application — so you never write
|
|
41
|
+
notification glue code again.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from notify_hub import Hub
|
|
45
|
+
|
|
46
|
+
hub = Hub.from_config("notify-hub.toml")
|
|
47
|
+
hub.notify("database is down", level="CRITICAL") # returns in microseconds
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
What that one call does is decided entirely by config: which channels fire
|
|
51
|
+
for which level, how attachments degrade per channel, retries, dedup,
|
|
52
|
+
rate limits, and where the audit log goes.
|
|
53
|
+
|
|
54
|
+
> 📖 **中文使用说明(30 秒跑通 + 全渠道接入指南):[docs/usage.md](docs/usage.md)**
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- **7 channels, one interface** — Slack, Discord, Telegram, WeCom, DingTalk,
|
|
59
|
+
Lark/Feishu, phone voice calls (Twilio, pluggable for other providers).
|
|
60
|
+
- **Level-based routing** — builtin `DEBUG..CRITICAL` plus your own custom
|
|
61
|
+
levels (`TRADE_HALT = 45`); each app maps levels to channels in config,
|
|
62
|
+
matching rules are unioned.
|
|
63
|
+
- **Never blocks your app** — `notify()` returns immediately in both sync
|
|
64
|
+
and asyncio hosts; fan-out runs concurrently on a lazily-started
|
|
65
|
+
background event loop. Await the handle (async) or `wait()` on it (sync)
|
|
66
|
+
only when you need the outcome.
|
|
67
|
+
- **Failure isolation & retries** — one channel failing never affects the
|
|
68
|
+
others and never raises into your code; transient errors (429/5xx,
|
|
69
|
+
network) retry with exponential backoff + jitter.
|
|
70
|
+
- **Attachments with graceful degradation** — images/files from a path,
|
|
71
|
+
bytes, or URL; channels declare capabilities and unsupported content is
|
|
72
|
+
dropped / linked / failed per your policy (phone calls read text via TTS).
|
|
73
|
+
- **Anti-storm guards** — fingerprint dedup window (default 60 s) and
|
|
74
|
+
per-channel token-bucket rate limits, so a crash loop can't ring your
|
|
75
|
+
phone 1000 times.
|
|
76
|
+
- **Audit log** — every send attempt recorded (per-channel status, attempts,
|
|
77
|
+
latency, errors) as JSON Lines or text, to a file and/or your own
|
|
78
|
+
`logging.Logger`.
|
|
79
|
+
- **A well-behaved library** — stdlib dataclasses, one hard dependency
|
|
80
|
+
(`httpx`), zero threads until first use, nothing written or configured
|
|
81
|
+
unless you ask.
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install pynotifyhub
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Requires Python 3.10+. The distribution is named `pynotifyhub` (PyPI rejects
|
|
90
|
+
`notify-hub` as too similar to an existing project); the import stays
|
|
91
|
+
`notify_hub`.
|
|
92
|
+
|
|
93
|
+
## Quickstart
|
|
94
|
+
|
|
95
|
+
### 1. Configure once (TOML or a dict)
|
|
96
|
+
|
|
97
|
+
```toml
|
|
98
|
+
# notify-hub.toml
|
|
99
|
+
[hub]
|
|
100
|
+
default_channels = ["slack-ops"]
|
|
101
|
+
|
|
102
|
+
[channels.slack-ops]
|
|
103
|
+
type = "slack"
|
|
104
|
+
webhook_url = "${SLACK_WEBHOOK_URL}" # expanded from the environment
|
|
105
|
+
|
|
106
|
+
[channels.oncall-phone]
|
|
107
|
+
type = "phone"
|
|
108
|
+
provider = "twilio"
|
|
109
|
+
account_sid = "${TWILIO_ACCOUNT_SID}"
|
|
110
|
+
auth_token = "${TWILIO_AUTH_TOKEN}"
|
|
111
|
+
from_number = "+15550001111"
|
|
112
|
+
to_numbers = ["+15552223333"]
|
|
113
|
+
rate_limit = { per_minute = 2, burst = 1 }
|
|
114
|
+
|
|
115
|
+
[[routes]]
|
|
116
|
+
min_level = "WARNING"
|
|
117
|
+
channels = ["slack-ops"]
|
|
118
|
+
|
|
119
|
+
[[routes]]
|
|
120
|
+
levels = ["CRITICAL"]
|
|
121
|
+
channels = ["oncall-phone"] # CRITICAL also matches the rule above
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
See [`examples/notify-hub.example.toml`](examples/notify-hub.example.toml)
|
|
125
|
+
for every channel and option.
|
|
126
|
+
|
|
127
|
+
### 2. Send
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from notify_hub import Attachment, Hub
|
|
131
|
+
|
|
132
|
+
with Hub.from_config("notify-hub.toml") as hub:
|
|
133
|
+
hub.warning("disk at 85%") # fire-and-forget
|
|
134
|
+
hub.critical("db down") # slack + phone call
|
|
135
|
+
|
|
136
|
+
handle = hub.notify(
|
|
137
|
+
"deploy finished",
|
|
138
|
+
level="INFO",
|
|
139
|
+
title="Release v2.1",
|
|
140
|
+
attachments=[Attachment.image("https://example.com/chart.png")],
|
|
141
|
+
)
|
|
142
|
+
result = handle.wait(timeout=30) # only if you need it
|
|
143
|
+
print(result.ok)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Async hosts use the same hub:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
async with Hub.from_config("notify-hub.toml") as hub:
|
|
150
|
+
hub.error("worker crashed") # still instant
|
|
151
|
+
result = await hub.notify_async("done") # or await the outcome
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Custom levels:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
hub.levels.register("TRADE_HALT", 45) # or [levels.custom] in TOML
|
|
158
|
+
hub.notify("strategy halted", level="TRADE_HALT")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 3. Observe
|
|
162
|
+
|
|
163
|
+
With `[log]` enabled, every attempt is recorded:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{"ts": "2026-06-11T12:00:00+00:00", "level": "CRITICAL", "fingerprint": "9f3a...",
|
|
167
|
+
"preview": "db down", "suppressed_by_dedup": false,
|
|
168
|
+
"channels": [{"id": "slack-ops", "ok": true, "attempts": 1, "latency_ms": 212.4,
|
|
169
|
+
"error": null, "degraded": [], "skipped_reason": null}]}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Channel capability matrix
|
|
173
|
+
|
|
174
|
+
| Channel | Text | Markdown | Image | File | Notes |
|
|
175
|
+
|---|:---:|:---:|:---:|:---:|---|
|
|
176
|
+
| Slack | ✅ | ✅ (mrkdwn) | ✅* | ✅* | *URL images free via blocks; uploads need `bot_token` + `file_channel` |
|
|
177
|
+
| Discord | ✅ | ✅ | ✅ | ✅ | multipart, ≤10 files |
|
|
178
|
+
| Telegram | ✅ | ✅ (HTML) | ✅ | ✅ | single image + short text sent as captioned photo |
|
|
179
|
+
| WeCom 企业微信 | ✅ | ✅ | ✅ | ✅ | image ≤2 MB jpg/png; file via `upload_media` |
|
|
180
|
+
| DingTalk 钉钉 | ✅ | ✅ | — | — | HMAC signing or keyword prefix |
|
|
181
|
+
| Lark/Feishu 飞书 | ✅ | ✅ (post) | ✅* | — | *images need `app_id`+`app_secret` (custom app); both feishu.cn and larksuite.com |
|
|
182
|
+
| Phone (Twilio) | ✅ (TTS) | — | — | — | text stripped of markdown/URLs, read aloud |
|
|
183
|
+
|
|
184
|
+
Unsupported content degrades per `[degrade]` policy — by default it is
|
|
185
|
+
dropped quietly and noted in the result, never failing the notification.
|
|
186
|
+
|
|
187
|
+
## Writing your own channel
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from notify_hub import Capability, Channel, Message, register_channel
|
|
191
|
+
|
|
192
|
+
@register_channel
|
|
193
|
+
class MyChannel(Channel):
|
|
194
|
+
name = "mychannel"
|
|
195
|
+
capabilities = Capability.TEXT
|
|
196
|
+
|
|
197
|
+
async def send(self, message: Message) -> None:
|
|
198
|
+
... # raise ChannelSendError on failure; hub handles retry/logging
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Or publish it as an entry point in the `notify_hub.channels` group.
|
|
202
|
+
Voice providers (e.g. Vonage) subclass `VoiceProvider` the same way.
|
|
203
|
+
|
|
204
|
+
## Design notes
|
|
205
|
+
|
|
206
|
+
- The TOML schema and routing semantics are language-neutral by design —
|
|
207
|
+
they are the contract for planned Go and Rust ports.
|
|
208
|
+
- v2 reserves `escalation` (ack + auto-escalate chains) and `digest`
|
|
209
|
+
(aggregation windows) fields in routing rules; they parse today and warn,
|
|
210
|
+
but do nothing yet.
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pip install -e ".[dev]"
|
|
216
|
+
pytest # tests
|
|
217
|
+
ruff check . && ruff format --check .
|
|
218
|
+
mypy
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# notify-hub
|
|
2
|
+
|
|
3
|
+
Unified multi-channel notification library for Python.
|
|
4
|
+
|
|
5
|
+
Send alerts to **Slack, Discord, Telegram, WeCom (企业微信), DingTalk (钉钉),
|
|
6
|
+
Lark/Feishu (飞书), and phone voice calls (Twilio)** through one API, with
|
|
7
|
+
level-based routing configured per application — so you never write
|
|
8
|
+
notification glue code again.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from notify_hub import Hub
|
|
12
|
+
|
|
13
|
+
hub = Hub.from_config("notify-hub.toml")
|
|
14
|
+
hub.notify("database is down", level="CRITICAL") # returns in microseconds
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
What that one call does is decided entirely by config: which channels fire
|
|
18
|
+
for which level, how attachments degrade per channel, retries, dedup,
|
|
19
|
+
rate limits, and where the audit log goes.
|
|
20
|
+
|
|
21
|
+
> 📖 **中文使用说明(30 秒跑通 + 全渠道接入指南):[docs/usage.md](docs/usage.md)**
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **7 channels, one interface** — Slack, Discord, Telegram, WeCom, DingTalk,
|
|
26
|
+
Lark/Feishu, phone voice calls (Twilio, pluggable for other providers).
|
|
27
|
+
- **Level-based routing** — builtin `DEBUG..CRITICAL` plus your own custom
|
|
28
|
+
levels (`TRADE_HALT = 45`); each app maps levels to channels in config,
|
|
29
|
+
matching rules are unioned.
|
|
30
|
+
- **Never blocks your app** — `notify()` returns immediately in both sync
|
|
31
|
+
and asyncio hosts; fan-out runs concurrently on a lazily-started
|
|
32
|
+
background event loop. Await the handle (async) or `wait()` on it (sync)
|
|
33
|
+
only when you need the outcome.
|
|
34
|
+
- **Failure isolation & retries** — one channel failing never affects the
|
|
35
|
+
others and never raises into your code; transient errors (429/5xx,
|
|
36
|
+
network) retry with exponential backoff + jitter.
|
|
37
|
+
- **Attachments with graceful degradation** — images/files from a path,
|
|
38
|
+
bytes, or URL; channels declare capabilities and unsupported content is
|
|
39
|
+
dropped / linked / failed per your policy (phone calls read text via TTS).
|
|
40
|
+
- **Anti-storm guards** — fingerprint dedup window (default 60 s) and
|
|
41
|
+
per-channel token-bucket rate limits, so a crash loop can't ring your
|
|
42
|
+
phone 1000 times.
|
|
43
|
+
- **Audit log** — every send attempt recorded (per-channel status, attempts,
|
|
44
|
+
latency, errors) as JSON Lines or text, to a file and/or your own
|
|
45
|
+
`logging.Logger`.
|
|
46
|
+
- **A well-behaved library** — stdlib dataclasses, one hard dependency
|
|
47
|
+
(`httpx`), zero threads until first use, nothing written or configured
|
|
48
|
+
unless you ask.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install pynotifyhub
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires Python 3.10+. The distribution is named `pynotifyhub` (PyPI rejects
|
|
57
|
+
`notify-hub` as too similar to an existing project); the import stays
|
|
58
|
+
`notify_hub`.
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
### 1. Configure once (TOML or a dict)
|
|
63
|
+
|
|
64
|
+
```toml
|
|
65
|
+
# notify-hub.toml
|
|
66
|
+
[hub]
|
|
67
|
+
default_channels = ["slack-ops"]
|
|
68
|
+
|
|
69
|
+
[channels.slack-ops]
|
|
70
|
+
type = "slack"
|
|
71
|
+
webhook_url = "${SLACK_WEBHOOK_URL}" # expanded from the environment
|
|
72
|
+
|
|
73
|
+
[channels.oncall-phone]
|
|
74
|
+
type = "phone"
|
|
75
|
+
provider = "twilio"
|
|
76
|
+
account_sid = "${TWILIO_ACCOUNT_SID}"
|
|
77
|
+
auth_token = "${TWILIO_AUTH_TOKEN}"
|
|
78
|
+
from_number = "+15550001111"
|
|
79
|
+
to_numbers = ["+15552223333"]
|
|
80
|
+
rate_limit = { per_minute = 2, burst = 1 }
|
|
81
|
+
|
|
82
|
+
[[routes]]
|
|
83
|
+
min_level = "WARNING"
|
|
84
|
+
channels = ["slack-ops"]
|
|
85
|
+
|
|
86
|
+
[[routes]]
|
|
87
|
+
levels = ["CRITICAL"]
|
|
88
|
+
channels = ["oncall-phone"] # CRITICAL also matches the rule above
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
See [`examples/notify-hub.example.toml`](examples/notify-hub.example.toml)
|
|
92
|
+
for every channel and option.
|
|
93
|
+
|
|
94
|
+
### 2. Send
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from notify_hub import Attachment, Hub
|
|
98
|
+
|
|
99
|
+
with Hub.from_config("notify-hub.toml") as hub:
|
|
100
|
+
hub.warning("disk at 85%") # fire-and-forget
|
|
101
|
+
hub.critical("db down") # slack + phone call
|
|
102
|
+
|
|
103
|
+
handle = hub.notify(
|
|
104
|
+
"deploy finished",
|
|
105
|
+
level="INFO",
|
|
106
|
+
title="Release v2.1",
|
|
107
|
+
attachments=[Attachment.image("https://example.com/chart.png")],
|
|
108
|
+
)
|
|
109
|
+
result = handle.wait(timeout=30) # only if you need it
|
|
110
|
+
print(result.ok)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Async hosts use the same hub:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
async with Hub.from_config("notify-hub.toml") as hub:
|
|
117
|
+
hub.error("worker crashed") # still instant
|
|
118
|
+
result = await hub.notify_async("done") # or await the outcome
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Custom levels:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
hub.levels.register("TRADE_HALT", 45) # or [levels.custom] in TOML
|
|
125
|
+
hub.notify("strategy halted", level="TRADE_HALT")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 3. Observe
|
|
129
|
+
|
|
130
|
+
With `[log]` enabled, every attempt is recorded:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{"ts": "2026-06-11T12:00:00+00:00", "level": "CRITICAL", "fingerprint": "9f3a...",
|
|
134
|
+
"preview": "db down", "suppressed_by_dedup": false,
|
|
135
|
+
"channels": [{"id": "slack-ops", "ok": true, "attempts": 1, "latency_ms": 212.4,
|
|
136
|
+
"error": null, "degraded": [], "skipped_reason": null}]}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Channel capability matrix
|
|
140
|
+
|
|
141
|
+
| Channel | Text | Markdown | Image | File | Notes |
|
|
142
|
+
|---|:---:|:---:|:---:|:---:|---|
|
|
143
|
+
| Slack | ✅ | ✅ (mrkdwn) | ✅* | ✅* | *URL images free via blocks; uploads need `bot_token` + `file_channel` |
|
|
144
|
+
| Discord | ✅ | ✅ | ✅ | ✅ | multipart, ≤10 files |
|
|
145
|
+
| Telegram | ✅ | ✅ (HTML) | ✅ | ✅ | single image + short text sent as captioned photo |
|
|
146
|
+
| WeCom 企业微信 | ✅ | ✅ | ✅ | ✅ | image ≤2 MB jpg/png; file via `upload_media` |
|
|
147
|
+
| DingTalk 钉钉 | ✅ | ✅ | — | — | HMAC signing or keyword prefix |
|
|
148
|
+
| Lark/Feishu 飞书 | ✅ | ✅ (post) | ✅* | — | *images need `app_id`+`app_secret` (custom app); both feishu.cn and larksuite.com |
|
|
149
|
+
| Phone (Twilio) | ✅ (TTS) | — | — | — | text stripped of markdown/URLs, read aloud |
|
|
150
|
+
|
|
151
|
+
Unsupported content degrades per `[degrade]` policy — by default it is
|
|
152
|
+
dropped quietly and noted in the result, never failing the notification.
|
|
153
|
+
|
|
154
|
+
## Writing your own channel
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from notify_hub import Capability, Channel, Message, register_channel
|
|
158
|
+
|
|
159
|
+
@register_channel
|
|
160
|
+
class MyChannel(Channel):
|
|
161
|
+
name = "mychannel"
|
|
162
|
+
capabilities = Capability.TEXT
|
|
163
|
+
|
|
164
|
+
async def send(self, message: Message) -> None:
|
|
165
|
+
... # raise ChannelSendError on failure; hub handles retry/logging
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Or publish it as an entry point in the `notify_hub.channels` group.
|
|
169
|
+
Voice providers (e.g. Vonage) subclass `VoiceProvider` the same way.
|
|
170
|
+
|
|
171
|
+
## Design notes
|
|
172
|
+
|
|
173
|
+
- The TOML schema and routing semantics are language-neutral by design —
|
|
174
|
+
they are the contract for planned Go and Rust ports.
|
|
175
|
+
- v2 reserves `escalation` (ack + auto-escalate chains) and `digest`
|
|
176
|
+
(aggregation windows) fields in routing rules; they parse today and warn,
|
|
177
|
+
but do nothing yet.
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pip install -e ".[dev]"
|
|
183
|
+
pytest # tests
|
|
184
|
+
ruff check . && ruff format --check .
|
|
185
|
+
mypy
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|