leap-feishu-webhook-notify 0.3.1__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.
- leap_feishu_webhook_notify-0.3.1/.gitattributes +5 -0
- leap_feishu_webhook_notify-0.3.1/.github/workflows/ci.yml +21 -0
- leap_feishu_webhook_notify-0.3.1/.github/workflows/publish.yml +22 -0
- leap_feishu_webhook_notify-0.3.1/.gitignore +11 -0
- leap_feishu_webhook_notify-0.3.1/.python-version +1 -0
- leap_feishu_webhook_notify-0.3.1/CHANGELOG.md +60 -0
- leap_feishu_webhook_notify-0.3.1/LICENSE +21 -0
- leap_feishu_webhook_notify-0.3.1/PKG-INFO +293 -0
- leap_feishu_webhook_notify-0.3.1/README.md +238 -0
- leap_feishu_webhook_notify-0.3.1/pyproject.toml +70 -0
- leap_feishu_webhook_notify-0.3.1/scripts/check.sh +8 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/__init__.py +40 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/_version.py +1 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/bot.py +394 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/context.py +64 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/manager.py +84 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/primitives.py +156 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/py.typed +0 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/schemas.py +66 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/splitter.py +81 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/templates/__init__.py +0 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/templates/alert.py +152 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/templates/lifecycle.py +127 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/templates/report.py +146 -0
- leap_feishu_webhook_notify-0.3.1/src/feishu_notify/transport.py +56 -0
- leap_feishu_webhook_notify-0.3.1/tests/__init__.py +0 -0
- leap_feishu_webhook_notify-0.3.1/tests/integration/__init__.py +0 -0
- leap_feishu_webhook_notify-0.3.1/tests/integration/test_real_webhook.py +185 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/__init__.py +0 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_bot.py +667 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_bot_templates.py +124 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_context.py +71 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_manager.py +103 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_primitives.py +123 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_public_api.py +53 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_schemas.py +56 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_signing.py +58 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_splitter.py +97 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_templates_alert.py +188 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_templates_lifecycle.py +89 -0
- leap_feishu_webhook_notify-0.3.1/tests/unit/test_templates_report.py +126 -0
- leap_feishu_webhook_notify-0.3.1/uv.lock +439 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.11"
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
- run: uv sync --extra dev
|
|
18
|
+
- run: uv run ruff check src tests
|
|
19
|
+
- run: uv run pyright src
|
|
20
|
+
- run: uv run pytest tests/unit/ --cov=feishu_notify --cov-report=term --cov-fail-under=85
|
|
21
|
+
- run: uv run python -m compileall -q src/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.11"
|
|
19
|
+
- name: Build
|
|
20
|
+
run: uv build
|
|
21
|
+
- name: Publish
|
|
22
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This project follows Keep a Changelog and Semantic Versioning.
|
|
4
|
+
|
|
5
|
+
## [0.3.1] - 2026-05-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Replaced CardKit v2 `note` elements with markdown-based footer text after the
|
|
10
|
+
Lark/Feishu card API stopped accepting the `note` tag.
|
|
11
|
+
- Replaced the deprecated `action` wrapper with top-level `button` elements.
|
|
12
|
+
Existing calls to `primitives.action_button(...)` keep the same Python API.
|
|
13
|
+
|
|
14
|
+
### Migration
|
|
15
|
+
|
|
16
|
+
- Upgrade from `0.3.0` to `0.3.1` with no application code changes.
|
|
17
|
+
|
|
18
|
+
## [0.3.0] - 2026-05-27
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Multi-target webhook support with `webhook_urls=[...]` and `secrets=[...]`.
|
|
23
|
+
- `FeishuBot.reset()` to restore the initial webhook(s) and secret(s).
|
|
24
|
+
- Stable public properties: `bot.enabled`, `bot.name`, and `bot.context`.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `bot.enabled` now returns `True` when any configured webhook URL is non-empty.
|
|
29
|
+
- `send()` and template send helpers return `list[bool]` in multi-target mode.
|
|
30
|
+
Single-webhook mode still returns `bool`.
|
|
31
|
+
|
|
32
|
+
### Backward Compatibility
|
|
33
|
+
|
|
34
|
+
- Existing single-webhook users can upgrade from `0.2.x` without code changes.
|
|
35
|
+
|
|
36
|
+
## [0.2.0] - 2026-05-21
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- `FeishuBot.reconfigure(...)` for runtime webhook and secret updates.
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
|
|
44
|
+
- Added dynamic configuration examples for in-memory, persisted, and multi-node
|
|
45
|
+
configuration patterns.
|
|
46
|
+
|
|
47
|
+
## [0.1.0] - 2026-05-21
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- `FeishuBot` with low-level `send(payload)` support.
|
|
52
|
+
- HMAC-SHA256 signing for Lark/Feishu custom bot webhooks.
|
|
53
|
+
- CardKit v2 primitives.
|
|
54
|
+
- Lifecycle templates: deploy, startup, shutdown, ping, and heartbeat.
|
|
55
|
+
- Alert templates: warning, error, and critical.
|
|
56
|
+
- Period report template support via the `PeriodSummary` protocol.
|
|
57
|
+
- `BotManager` for registering and broadcasting to multiple bots.
|
|
58
|
+
- Payload splitting for oversized interactive cards.
|
|
59
|
+
- Safe failure behavior: send methods log failures and return booleans instead
|
|
60
|
+
of raising.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Llugaes
|
|
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,293 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leap-feishu-webhook-notify
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Async Lark/Feishu custom bot webhook notifications with CardKit v2 templates
|
|
5
|
+
Project-URL: Homepage, https://github.com/Llugaes/leap-feishu-webhook-notify
|
|
6
|
+
Project-URL: Repository, https://github.com/Llugaes/leap-feishu-webhook-notify
|
|
7
|
+
Project-URL: Issues, https://github.com/Llugaes/leap-feishu-webhook-notify/issues
|
|
8
|
+
Author-email: Llugaes <249094896@qq.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Llugaes
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: cardkit,feishu,lark,notification,webhook
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Communications
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: >=3.11
|
|
45
|
+
Requires-Dist: httpx>=0.27
|
|
46
|
+
Requires-Dist: loguru>=0.7
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: pyright; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
52
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
53
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
54
|
+
Description-Content-Type: text/markdown
|
|
55
|
+
|
|
56
|
+
# leap-feishu-webhook-notify
|
|
57
|
+
|
|
58
|
+
Async Python notifications for Lark/Feishu custom bot webhooks.
|
|
59
|
+
|
|
60
|
+
The PyPI distribution is `leap-feishu-webhook-notify`. The import package remains
|
|
61
|
+
`feishu_notify`.
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- Async `httpx` transport with reusable connections.
|
|
66
|
+
- Lark/Feishu custom bot HMAC-SHA256 signing.
|
|
67
|
+
- CardKit v2 interactive cards.
|
|
68
|
+
- Built-in templates for lifecycle events, alerts, heartbeats, pings, and period reports.
|
|
69
|
+
- Payload splitting for large cards.
|
|
70
|
+
- Single-target and multi-target webhook fan-out.
|
|
71
|
+
- Runtime reconfiguration and reset support.
|
|
72
|
+
- Failure-safe sends: network and API failures are logged and returned as `False`, not raised.
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install leap-feishu-webhook-notify
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
With uv:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv add leap-feishu-webhook-notify
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Quick Start
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
import os
|
|
91
|
+
|
|
92
|
+
from feishu_notify import FeishuBot, ServiceContext
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def main() -> None:
|
|
96
|
+
bot = FeishuBot(
|
|
97
|
+
webhook_url=os.environ["FEISHU_WEBHOOK_URL"],
|
|
98
|
+
secret=os.environ.get("FEISHU_WEBHOOK_SECRET", ""),
|
|
99
|
+
context=ServiceContext(service="orders-api", env="prod"),
|
|
100
|
+
)
|
|
101
|
+
try:
|
|
102
|
+
ok = await bot.send_heartbeat(message="notification channel is ready")
|
|
103
|
+
print(ok)
|
|
104
|
+
finally:
|
|
105
|
+
await bot.aclose()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
asyncio.run(main())
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Alert Cards
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
await bot.send_warning(
|
|
115
|
+
title="Payment provider timeout",
|
|
116
|
+
message="The provider did not respond after 3 retries.",
|
|
117
|
+
error_message="TimeoutError: request timed out",
|
|
118
|
+
trace_id="trace-123",
|
|
119
|
+
details={"provider": "example-pay", "retry": 3},
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Set optional URLs on `ServiceContext` to render action buttons:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
context = ServiceContext(
|
|
127
|
+
service="orders-api",
|
|
128
|
+
env="prod",
|
|
129
|
+
host="worker-1",
|
|
130
|
+
log_url_template="https://logs.example.com/trace/{trace_id}",
|
|
131
|
+
commit_url_template="https://git.example.com/repo/commit/{commit}",
|
|
132
|
+
dashboard_url="https://metrics.example.com/d/orders",
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Multi-target Webhooks
|
|
137
|
+
|
|
138
|
+
Use `webhook_urls` when the same message should be sent to several groups.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
bot = FeishuBot(
|
|
142
|
+
webhook_urls=[
|
|
143
|
+
os.environ["FEISHU_PRIMARY_WEBHOOK"],
|
|
144
|
+
os.environ["FEISHU_ONCALL_WEBHOOK"],
|
|
145
|
+
],
|
|
146
|
+
secrets=[
|
|
147
|
+
os.environ.get("FEISHU_PRIMARY_SECRET", ""),
|
|
148
|
+
os.environ.get("FEISHU_ONCALL_SECRET", ""),
|
|
149
|
+
],
|
|
150
|
+
context=ServiceContext(service="orders-api", env="prod"),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
result = await bot.send_error(
|
|
154
|
+
title="Database unavailable",
|
|
155
|
+
message="All replicas failed the health check.",
|
|
156
|
+
error_message="ConnectionError: no healthy upstream",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
oks = result if isinstance(result, list) else [result]
|
|
160
|
+
failed_count = oks.count(False)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Return values:
|
|
164
|
+
|
|
165
|
+
| Mode | Return type |
|
|
166
|
+
|---|---|
|
|
167
|
+
| Single webhook | `bool` |
|
|
168
|
+
| Multiple webhooks | `list[bool]`, in the same order as `webhook_urls` |
|
|
169
|
+
|
|
170
|
+
## Runtime Reconfiguration
|
|
171
|
+
|
|
172
|
+
`reconfigure()` updates the active webhook(s) in memory. It does not persist
|
|
173
|
+
anything and does not read environment variables.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
bot.reconfigure(webhook_url=new_url, secret=new_secret)
|
|
177
|
+
|
|
178
|
+
bot.reconfigure(
|
|
179
|
+
webhook_urls=[primary_url, oncall_url],
|
|
180
|
+
secrets=[primary_secret, oncall_secret],
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`reset()` restores the webhook(s) and secret(s) passed to `__init__`.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
if override_url:
|
|
188
|
+
bot.reconfigure(webhook_url=override_url, secret=override_secret)
|
|
189
|
+
else:
|
|
190
|
+
bot.reset()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## BotManager
|
|
194
|
+
|
|
195
|
+
Use `BotManager` when different messages should go to different bots.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from feishu_notify import BotManager
|
|
199
|
+
|
|
200
|
+
manager = BotManager()
|
|
201
|
+
manager.register(ops_bot)
|
|
202
|
+
manager.register(alerts_bot)
|
|
203
|
+
|
|
204
|
+
await manager.broadcast_via("send_startup")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`BotManager.broadcast*()` folds a multi-target bot result with `all(...)`, so
|
|
208
|
+
each bot name maps to one success boolean.
|
|
209
|
+
|
|
210
|
+
## Period Reports
|
|
211
|
+
|
|
212
|
+
Any object implementing the `PeriodSummary` protocol can be sent as a report.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from dataclasses import dataclass, field
|
|
216
|
+
|
|
217
|
+
from feishu_notify import FailureItem, StatGroup
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@dataclass
|
|
221
|
+
class DailySummary:
|
|
222
|
+
period_label: str = "2026-06-09"
|
|
223
|
+
period_type: str = "daily"
|
|
224
|
+
total_counts: dict[str, int] = field(default_factory=lambda: {"ok": 120, "failed": 2})
|
|
225
|
+
groups: list[StatGroup] = field(default_factory=list)
|
|
226
|
+
failures: list[FailureItem] = field(default_factory=list)
|
|
227
|
+
prev_total_counts: dict[str, int] | None = None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
await bot.send_period_report(summary=DailySummary())
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Low-level Card API
|
|
234
|
+
|
|
235
|
+
Use `feishu_notify.primitives` to build custom CardKit v2 payloads.
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from feishu_notify import primitives
|
|
239
|
+
|
|
240
|
+
payload = primitives.card(
|
|
241
|
+
"Custom report",
|
|
242
|
+
"blue",
|
|
243
|
+
[
|
|
244
|
+
primitives.md("**Status**: OK"),
|
|
245
|
+
primitives.hr(),
|
|
246
|
+
primitives.action_button("Open dashboard", "https://metrics.example.com"),
|
|
247
|
+
],
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
await bot.send(payload)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Failure Behavior
|
|
254
|
+
|
|
255
|
+
All send methods are designed to be safe in application code:
|
|
256
|
+
|
|
257
|
+
| Failure | Behavior |
|
|
258
|
+
|---|---|
|
|
259
|
+
| Empty webhook URL | Return `False` |
|
|
260
|
+
| Network error or timeout | Log warning, invoke `on_failure`, return `False` |
|
|
261
|
+
| HTTP 4xx/5xx | Log status and body preview, return `False` |
|
|
262
|
+
| Lark/Feishu `code != 0` | Log code and message, return `False` |
|
|
263
|
+
| Invalid JSON response | Log warning, return `False` |
|
|
264
|
+
| Unexpected exception | Log exception, invoke `on_failure`, return `False` |
|
|
265
|
+
|
|
266
|
+
Avoid putting webhook URLs, secrets, tokens, passwords, or other sensitive data
|
|
267
|
+
inside card `message`, `details`, or logs.
|
|
268
|
+
|
|
269
|
+
## Development
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
uv sync --extra dev
|
|
273
|
+
uv run ruff check src tests
|
|
274
|
+
uv run pyright src
|
|
275
|
+
uv run pytest tests/unit/ --cov=feishu_notify --cov-report=term --cov-fail-under=85
|
|
276
|
+
uv run python -m compileall -q src/
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Real webhook smoke tests are skipped unless these environment variables are set:
|
|
280
|
+
|
|
281
|
+
- `FEISHU_TEST_WEBHOOK`
|
|
282
|
+
- `FEISHU_TEST_SECRET`
|
|
283
|
+
- `FEISHU_TEST_WEBHOOK_LIST`
|
|
284
|
+
|
|
285
|
+
Run them explicitly:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
uv run pytest tests/integration/ -v
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# leap-feishu-webhook-notify
|
|
2
|
+
|
|
3
|
+
Async Python notifications for Lark/Feishu custom bot webhooks.
|
|
4
|
+
|
|
5
|
+
The PyPI distribution is `leap-feishu-webhook-notify`. The import package remains
|
|
6
|
+
`feishu_notify`.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Async `httpx` transport with reusable connections.
|
|
11
|
+
- Lark/Feishu custom bot HMAC-SHA256 signing.
|
|
12
|
+
- CardKit v2 interactive cards.
|
|
13
|
+
- Built-in templates for lifecycle events, alerts, heartbeats, pings, and period reports.
|
|
14
|
+
- Payload splitting for large cards.
|
|
15
|
+
- Single-target and multi-target webhook fan-out.
|
|
16
|
+
- Runtime reconfiguration and reset support.
|
|
17
|
+
- Failure-safe sends: network and API failures are logged and returned as `False`, not raised.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install leap-feishu-webhook-notify
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
With uv:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv add leap-feishu-webhook-notify
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import asyncio
|
|
35
|
+
import os
|
|
36
|
+
|
|
37
|
+
from feishu_notify import FeishuBot, ServiceContext
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def main() -> None:
|
|
41
|
+
bot = FeishuBot(
|
|
42
|
+
webhook_url=os.environ["FEISHU_WEBHOOK_URL"],
|
|
43
|
+
secret=os.environ.get("FEISHU_WEBHOOK_SECRET", ""),
|
|
44
|
+
context=ServiceContext(service="orders-api", env="prod"),
|
|
45
|
+
)
|
|
46
|
+
try:
|
|
47
|
+
ok = await bot.send_heartbeat(message="notification channel is ready")
|
|
48
|
+
print(ok)
|
|
49
|
+
finally:
|
|
50
|
+
await bot.aclose()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
asyncio.run(main())
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Alert Cards
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
await bot.send_warning(
|
|
60
|
+
title="Payment provider timeout",
|
|
61
|
+
message="The provider did not respond after 3 retries.",
|
|
62
|
+
error_message="TimeoutError: request timed out",
|
|
63
|
+
trace_id="trace-123",
|
|
64
|
+
details={"provider": "example-pay", "retry": 3},
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Set optional URLs on `ServiceContext` to render action buttons:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
context = ServiceContext(
|
|
72
|
+
service="orders-api",
|
|
73
|
+
env="prod",
|
|
74
|
+
host="worker-1",
|
|
75
|
+
log_url_template="https://logs.example.com/trace/{trace_id}",
|
|
76
|
+
commit_url_template="https://git.example.com/repo/commit/{commit}",
|
|
77
|
+
dashboard_url="https://metrics.example.com/d/orders",
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Multi-target Webhooks
|
|
82
|
+
|
|
83
|
+
Use `webhook_urls` when the same message should be sent to several groups.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
bot = FeishuBot(
|
|
87
|
+
webhook_urls=[
|
|
88
|
+
os.environ["FEISHU_PRIMARY_WEBHOOK"],
|
|
89
|
+
os.environ["FEISHU_ONCALL_WEBHOOK"],
|
|
90
|
+
],
|
|
91
|
+
secrets=[
|
|
92
|
+
os.environ.get("FEISHU_PRIMARY_SECRET", ""),
|
|
93
|
+
os.environ.get("FEISHU_ONCALL_SECRET", ""),
|
|
94
|
+
],
|
|
95
|
+
context=ServiceContext(service="orders-api", env="prod"),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
result = await bot.send_error(
|
|
99
|
+
title="Database unavailable",
|
|
100
|
+
message="All replicas failed the health check.",
|
|
101
|
+
error_message="ConnectionError: no healthy upstream",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
oks = result if isinstance(result, list) else [result]
|
|
105
|
+
failed_count = oks.count(False)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Return values:
|
|
109
|
+
|
|
110
|
+
| Mode | Return type |
|
|
111
|
+
|---|---|
|
|
112
|
+
| Single webhook | `bool` |
|
|
113
|
+
| Multiple webhooks | `list[bool]`, in the same order as `webhook_urls` |
|
|
114
|
+
|
|
115
|
+
## Runtime Reconfiguration
|
|
116
|
+
|
|
117
|
+
`reconfigure()` updates the active webhook(s) in memory. It does not persist
|
|
118
|
+
anything and does not read environment variables.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
bot.reconfigure(webhook_url=new_url, secret=new_secret)
|
|
122
|
+
|
|
123
|
+
bot.reconfigure(
|
|
124
|
+
webhook_urls=[primary_url, oncall_url],
|
|
125
|
+
secrets=[primary_secret, oncall_secret],
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`reset()` restores the webhook(s) and secret(s) passed to `__init__`.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
if override_url:
|
|
133
|
+
bot.reconfigure(webhook_url=override_url, secret=override_secret)
|
|
134
|
+
else:
|
|
135
|
+
bot.reset()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## BotManager
|
|
139
|
+
|
|
140
|
+
Use `BotManager` when different messages should go to different bots.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from feishu_notify import BotManager
|
|
144
|
+
|
|
145
|
+
manager = BotManager()
|
|
146
|
+
manager.register(ops_bot)
|
|
147
|
+
manager.register(alerts_bot)
|
|
148
|
+
|
|
149
|
+
await manager.broadcast_via("send_startup")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`BotManager.broadcast*()` folds a multi-target bot result with `all(...)`, so
|
|
153
|
+
each bot name maps to one success boolean.
|
|
154
|
+
|
|
155
|
+
## Period Reports
|
|
156
|
+
|
|
157
|
+
Any object implementing the `PeriodSummary` protocol can be sent as a report.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from dataclasses import dataclass, field
|
|
161
|
+
|
|
162
|
+
from feishu_notify import FailureItem, StatGroup
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class DailySummary:
|
|
167
|
+
period_label: str = "2026-06-09"
|
|
168
|
+
period_type: str = "daily"
|
|
169
|
+
total_counts: dict[str, int] = field(default_factory=lambda: {"ok": 120, "failed": 2})
|
|
170
|
+
groups: list[StatGroup] = field(default_factory=list)
|
|
171
|
+
failures: list[FailureItem] = field(default_factory=list)
|
|
172
|
+
prev_total_counts: dict[str, int] | None = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
await bot.send_period_report(summary=DailySummary())
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Low-level Card API
|
|
179
|
+
|
|
180
|
+
Use `feishu_notify.primitives` to build custom CardKit v2 payloads.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from feishu_notify import primitives
|
|
184
|
+
|
|
185
|
+
payload = primitives.card(
|
|
186
|
+
"Custom report",
|
|
187
|
+
"blue",
|
|
188
|
+
[
|
|
189
|
+
primitives.md("**Status**: OK"),
|
|
190
|
+
primitives.hr(),
|
|
191
|
+
primitives.action_button("Open dashboard", "https://metrics.example.com"),
|
|
192
|
+
],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
await bot.send(payload)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Failure Behavior
|
|
199
|
+
|
|
200
|
+
All send methods are designed to be safe in application code:
|
|
201
|
+
|
|
202
|
+
| Failure | Behavior |
|
|
203
|
+
|---|---|
|
|
204
|
+
| Empty webhook URL | Return `False` |
|
|
205
|
+
| Network error or timeout | Log warning, invoke `on_failure`, return `False` |
|
|
206
|
+
| HTTP 4xx/5xx | Log status and body preview, return `False` |
|
|
207
|
+
| Lark/Feishu `code != 0` | Log code and message, return `False` |
|
|
208
|
+
| Invalid JSON response | Log warning, return `False` |
|
|
209
|
+
| Unexpected exception | Log exception, invoke `on_failure`, return `False` |
|
|
210
|
+
|
|
211
|
+
Avoid putting webhook URLs, secrets, tokens, passwords, or other sensitive data
|
|
212
|
+
inside card `message`, `details`, or logs.
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
uv sync --extra dev
|
|
218
|
+
uv run ruff check src tests
|
|
219
|
+
uv run pyright src
|
|
220
|
+
uv run pytest tests/unit/ --cov=feishu_notify --cov-report=term --cov-fail-under=85
|
|
221
|
+
uv run python -m compileall -q src/
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Real webhook smoke tests are skipped unless these environment variables are set:
|
|
225
|
+
|
|
226
|
+
- `FEISHU_TEST_WEBHOOK`
|
|
227
|
+
- `FEISHU_TEST_SECRET`
|
|
228
|
+
- `FEISHU_TEST_WEBHOOK_LIST`
|
|
229
|
+
|
|
230
|
+
Run them explicitly:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
uv run pytest tests/integration/ -v
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|