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.
Files changed (65) hide show
  1. pynotifyhub-0.1.0/.claude/settings.local.json +37 -0
  2. pynotifyhub-0.1.0/.env.example +35 -0
  3. pynotifyhub-0.1.0/.github/workflows/ci.yml +33 -0
  4. pynotifyhub-0.1.0/.gitignore +33 -0
  5. pynotifyhub-0.1.0/CHANGELOG.md +27 -0
  6. pynotifyhub-0.1.0/LICENSE +21 -0
  7. pynotifyhub-0.1.0/PKG-INFO +223 -0
  8. pynotifyhub-0.1.0/README.md +190 -0
  9. pynotifyhub-0.1.0/docs/usage.md +444 -0
  10. pynotifyhub-0.1.0/examples/async_app.py +38 -0
  11. pynotifyhub-0.1.0/examples/notify-hub.example.toml +96 -0
  12. pynotifyhub-0.1.0/examples/smoke_test.py +127 -0
  13. pynotifyhub-0.1.0/examples/sync_app.py +54 -0
  14. pynotifyhub-0.1.0/pyproject.toml +84 -0
  15. pynotifyhub-0.1.0/src/notify_hub/__init__.py +75 -0
  16. pynotifyhub-0.1.0/src/notify_hub/_util.py +79 -0
  17. pynotifyhub-0.1.0/src/notify_hub/capabilities.py +32 -0
  18. pynotifyhub-0.1.0/src/notify_hub/channels/__init__.py +19 -0
  19. pynotifyhub-0.1.0/src/notify_hub/channels/base.py +93 -0
  20. pynotifyhub-0.1.0/src/notify_hub/channels/dingtalk.py +82 -0
  21. pynotifyhub-0.1.0/src/notify_hub/channels/discord.py +51 -0
  22. pynotifyhub-0.1.0/src/notify_hub/channels/lark.py +166 -0
  23. pynotifyhub-0.1.0/src/notify_hub/channels/phone/__init__.py +7 -0
  24. pynotifyhub-0.1.0/src/notify_hub/channels/phone/base.py +34 -0
  25. pynotifyhub-0.1.0/src/notify_hub/channels/phone/channel.py +66 -0
  26. pynotifyhub-0.1.0/src/notify_hub/channels/phone/twilio.py +65 -0
  27. pynotifyhub-0.1.0/src/notify_hub/channels/slack.py +110 -0
  28. pynotifyhub-0.1.0/src/notify_hub/channels/telegram.py +125 -0
  29. pynotifyhub-0.1.0/src/notify_hub/channels/wecom.py +106 -0
  30. pynotifyhub-0.1.0/src/notify_hub/config.py +233 -0
  31. pynotifyhub-0.1.0/src/notify_hub/exceptions.py +40 -0
  32. pynotifyhub-0.1.0/src/notify_hub/guard.py +58 -0
  33. pynotifyhub-0.1.0/src/notify_hub/hub.py +345 -0
  34. pynotifyhub-0.1.0/src/notify_hub/levels.py +75 -0
  35. pynotifyhub-0.1.0/src/notify_hub/logging.py +124 -0
  36. pynotifyhub-0.1.0/src/notify_hub/message.py +112 -0
  37. pynotifyhub-0.1.0/src/notify_hub/py.typed +0 -0
  38. pynotifyhub-0.1.0/src/notify_hub/registry.py +65 -0
  39. pynotifyhub-0.1.0/src/notify_hub/results.py +75 -0
  40. pynotifyhub-0.1.0/src/notify_hub/router.py +62 -0
  41. pynotifyhub-0.1.0/src/notify_hub/runtime.py +70 -0
  42. pynotifyhub-0.1.0/tests/__init__.py +0 -0
  43. pynotifyhub-0.1.0/tests/channels/__init__.py +0 -0
  44. pynotifyhub-0.1.0/tests/channels/test_dingtalk.py +94 -0
  45. pynotifyhub-0.1.0/tests/channels/test_discord.py +74 -0
  46. pynotifyhub-0.1.0/tests/channels/test_lark.py +190 -0
  47. pynotifyhub-0.1.0/tests/channels/test_phone_twilio.py +100 -0
  48. pynotifyhub-0.1.0/tests/channels/test_slack.py +128 -0
  49. pynotifyhub-0.1.0/tests/channels/test_telegram.py +111 -0
  50. pynotifyhub-0.1.0/tests/channels/test_wecom.py +113 -0
  51. pynotifyhub-0.1.0/tests/conftest.py +49 -0
  52. pynotifyhub-0.1.0/tests/test_config.py +230 -0
  53. pynotifyhub-0.1.0/tests/test_degrade.py +56 -0
  54. pynotifyhub-0.1.0/tests/test_guard.py +53 -0
  55. pynotifyhub-0.1.0/tests/test_hub.py +251 -0
  56. pynotifyhub-0.1.0/tests/test_levels.py +45 -0
  57. pynotifyhub-0.1.0/tests/test_lifecycle.py +54 -0
  58. pynotifyhub-0.1.0/tests/test_logging.py +91 -0
  59. pynotifyhub-0.1.0/tests/test_message.py +64 -0
  60. pynotifyhub-0.1.0/tests/test_public_api.py +18 -0
  61. pynotifyhub-0.1.0/tests/test_redact.py +35 -0
  62. pynotifyhub-0.1.0/tests/test_registry.py +48 -0
  63. pynotifyhub-0.1.0/tests/test_router.py +61 -0
  64. pynotifyhub-0.1.0/tests/test_runtime.py +65 -0
  65. 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