xtr-slack-notifier 5.0.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.
- xtr_slack_notifier-5.0.0/LICENSE +22 -0
- xtr_slack_notifier-5.0.0/PKG-INFO +182 -0
- xtr_slack_notifier-5.0.0/README.md +159 -0
- xtr_slack_notifier-5.0.0/pyproject.toml +163 -0
- xtr_slack_notifier-5.0.0/pyproject.toml.orig +165 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/.agents/skills/xtr-slack-notifier/SKILL.md +107 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/__init__.py +23 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/__init__.py +33 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/_text_object.py +15 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/abstract_slack_block.py +25 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/abstract_slack_block_element.py +25 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_actions_block.py +53 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_block_element_interface.py +16 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_block_interface.py +16 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_button_block_element.py +42 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_context_block.py +55 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_divider_block.py +19 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_header_block.py +41 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_image_block.py +19 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_image_block_element.py +19 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_plain_text_input_block.py +49 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_section_block.py +54 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/py.typed +0 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/recipe/__init__.py +15 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/recipe/manifest.toml +23 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/slack_options.py +162 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/slack_sent_message.py +44 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/slack_transport.py +161 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/slack_transport_factory.py +56 -0
- xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/update_message_slack_options.py +32 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xterr
|
|
4
|
+
Copyright (c) 2019-present Fabien Potencier
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xtr-slack-notifier
|
|
3
|
+
Version: 5.0.0
|
|
4
|
+
Summary: Slack for xtr-notifier — chat messages posted, scheduled and updated in a channel, with Block Kit blocks, configured by DSN.
|
|
5
|
+
Keywords: notifier,notification,slack,chat,block-kit,dsn,asyncio
|
|
6
|
+
Author: Xterr
|
|
7
|
+
Author-email: Xterr <me@xterr.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: typing-extensions>=4.4
|
|
19
|
+
Requires-Dist: xtr-notifier>=5.0,<6
|
|
20
|
+
Requires-Dist: xtr-http-client-contracts>=5.0,<6
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
|
|
26
|
+
# xtr-slack-notifier
|
|
27
|
+
|
|
28
|
+
**Slack for xtr-notifier — chat messages posted, scheduled and updated in a channel, with Block Kit blocks, configured by DSN.**
|
|
29
|
+
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Why?
|
|
35
|
+
|
|
36
|
+
[xtr-notifier](../xtr-notifier) sends chat messages through whatever provider a DSN names. This
|
|
37
|
+
library is the Slack provider: installed beside it, a `slack://` DSN is served with no wiring, a
|
|
38
|
+
notification is rendered as Block Kit sections, and a message can be laid out with blocks,
|
|
39
|
+
scheduled for later, or updated once posted.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
uv add xtr-slack-notifier
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
It needs an HTTP client implementing
|
|
48
|
+
[xtr-http-client-contracts](../xtr-http-client-contracts) — usually
|
|
49
|
+
[xtr-http-client](../xtr-http-client).
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from xtr_http_client import HttpClient
|
|
55
|
+
from xtr_notifier import Chatter, Transport
|
|
56
|
+
from xtr_notifier.message import ChatMessage
|
|
57
|
+
from xtr_notifier.transport import TransportFactoryDiscovery
|
|
58
|
+
|
|
59
|
+
discovery = TransportFactoryDiscovery(
|
|
60
|
+
TransportFactoryDiscovery.CHATTER_GROUP, http_client=HttpClient.create()
|
|
61
|
+
)
|
|
62
|
+
transport = Transport(discovery=discovery).from_string("slack://xoxb-1234@default?channel=ops")
|
|
63
|
+
|
|
64
|
+
sent = await Chatter(transport).send(ChatMessage("Deploy finished."))
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## The DSN
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
slack://TOKEN@default?channel=CHANNEL
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `TOKEN` — a bot (`xoxb-`), user (`xoxp-`) or app (`xoxa-2`) token; a DSN without one is refused
|
|
74
|
+
with `User is not set.`, and a token of another kind with
|
|
75
|
+
`A valid Slack token needs to start with "xoxb-", "xoxp-" or "xoxa-2". ...` when the transport
|
|
76
|
+
is built — on first use, since the notifier builds its transports lazily.
|
|
77
|
+
- `default` — `slack.com`; name another host (and port) for a test server.
|
|
78
|
+
- `channel` — the channel a message naming none is posted to.
|
|
79
|
+
- `ssl=false` — reach the host over plain HTTP.
|
|
80
|
+
|
|
81
|
+
The transport's string form — what errors, events and `debug:config` show — is
|
|
82
|
+
`slack://slack.com?channel=CHANNEL`: never the token. The token is sent as a bearer credential.
|
|
83
|
+
|
|
84
|
+
## Options and blocks
|
|
85
|
+
|
|
86
|
+
`SlackOptions` builds what a message carries beyond its text; `to_options()` renders it for the
|
|
87
|
+
message:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from xtr_notifier.message import ChatMessage
|
|
91
|
+
from xtr_slack_notifier import SlackOptions
|
|
92
|
+
from xtr_slack_notifier.block import SlackActionsBlock, SlackHeaderBlock, SlackSectionBlock
|
|
93
|
+
|
|
94
|
+
options = (
|
|
95
|
+
SlackOptions()
|
|
96
|
+
.recipient("C0123456789")
|
|
97
|
+
.block(SlackHeaderBlock("Deploy 42"))
|
|
98
|
+
.block(SlackSectionBlock().text("*Finished* in 42s.").field("*Env*").field("production"))
|
|
99
|
+
.block(SlackActionsBlock().button("Open", "https://example.com/deploys/42", "primary"))
|
|
100
|
+
)
|
|
101
|
+
await chatter.send(ChatMessage("Deploy 42 finished.", options.to_options()))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The setters are `recipient`, `as_user`, `icon_emoji`, `icon_url`, `link_names`, `mrkdwn`, `parse`,
|
|
105
|
+
`unfurl_links`, `unfurl_media`, `username`, `thread_ts` (reply in a thread), `post_at` (schedule
|
|
106
|
+
through `chat.scheduleMessage`) and `block`. A message takes up to 50 blocks.
|
|
107
|
+
|
|
108
|
+
The blocks, under `xtr_slack_notifier.block`: `SlackSectionBlock` (text, up to 10 fields, an
|
|
109
|
+
accessory), `SlackHeaderBlock`, `SlackContextBlock` (up to 10 texts and images),
|
|
110
|
+
`SlackActionsBlock` (up to 25 buttons), `SlackDividerBlock`, `SlackImageBlock` and
|
|
111
|
+
`SlackPlainTextInputBlock`; the elements `SlackButtonBlockElement` and `SlackImageBlockElement`.
|
|
112
|
+
Anything else implements `SlackBlockInterface`.
|
|
113
|
+
|
|
114
|
+
A message made from a notification and carrying no options is rendered with
|
|
115
|
+
`SlackOptions.from_notification(summary)`: the emoji as the icon, a section for the subject, one
|
|
116
|
+
for the content, and the exception's traceback after a divider.
|
|
117
|
+
|
|
118
|
+
Fields left empty — `None`, `""`, an empty list or mapping — are not sent; `False` and `0` are.
|
|
119
|
+
|
|
120
|
+
## Updating a message
|
|
121
|
+
|
|
122
|
+
The transport returns a `SlackSentMessage`, carrying the channel (`channel_id`) and the message's
|
|
123
|
+
`ts` (`message_id`). `get_update_message(subject, options)` makes the message replacing it, sent
|
|
124
|
+
through `chat.update`:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
sent = await chatter.send(ChatMessage("Deploying..."))
|
|
128
|
+
await chatter.send(sent.get_update_message("Deployed."))
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Its options are `UpdateMessageSlackOptions(channel_id, ts, options)`, which render with the kind
|
|
132
|
+
`slack.update` — the transport accepts both kinds, and tells them apart by it after the message
|
|
133
|
+
crossed a message bus.
|
|
134
|
+
|
|
135
|
+
## Errors
|
|
136
|
+
|
|
137
|
+
| Error | When |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `IncompleteDsnError` | The DSN carries no token (`User is not set.`) |
|
|
140
|
+
| `InvalidArgumentError` | The token does not start with `xoxb-`, `xoxp-` or `xoxa-2` |
|
|
141
|
+
| `UnsupportedSchemeError` | The factory was handed a DSN that is not `slack://` |
|
|
142
|
+
| `UnsupportedMessageTypeError` | The transport was handed a message that is not a `ChatMessage` |
|
|
143
|
+
| `LogicError` | More than 50 blocks, 25 buttons, or 10 fields or context elements |
|
|
144
|
+
| `LengthError` | A header over 150 characters, an input label or placeholder over 150, an id over 255 |
|
|
145
|
+
| `TransportError` | `Unable to post the Slack message: "<error>" (<errors>).` when Slack answers `ok: false`, `Unable to post the Slack message: "<body>".` on another status, or `Could not reach the remote Slack server.` |
|
|
146
|
+
|
|
147
|
+
Every one of them is xtr-notifier's; `TransportError` is what failover moves on from.
|
|
148
|
+
|
|
149
|
+
## Use in an application
|
|
150
|
+
|
|
151
|
+
Everything adding this package to an application takes — and, read backwards, what removing it
|
|
152
|
+
undoes. It ships no bundle: the notifier's bundle finds its factory by the DSN scheme.
|
|
153
|
+
|
|
154
|
+
- **Install** — `uv add xtr-slack-notifier`, beside `xtr-notifier` set up as its
|
|
155
|
+
[*Use in an application*](../xtr-notifier/README.md#use-in-an-application) says.
|
|
156
|
+
- **Recipe** — `uv run xtr-recipes recipes:sync` does the *Environment* step below: it writes
|
|
157
|
+
`SLACK_DSN` in `.env`. It prints the step adding the transport to
|
|
158
|
+
`<app>/config/notifier.py`, which a recipe cannot make for you.
|
|
159
|
+
- **Configure** — add `slack=env("SLACK_DSN")` to `chatter_transports` in
|
|
160
|
+
`<app>/config/notifier.py`.
|
|
161
|
+
- **Environment** — `SLACK_DSN`, `slack://TOKEN@default?channel=CHANNEL` with the app's token and
|
|
162
|
+
the channel; read when a message is first sent.
|
|
163
|
+
- **Remove** — drop the `chatter_transports` entry and `SLACK_DSN`, then
|
|
164
|
+
`uv remove xtr-slack-notifier`.
|
|
165
|
+
- **Check** — `debug:config notifier` lists `slack` under `chatter_transports`.
|
|
166
|
+
|
|
167
|
+
Everything else — the bundle, the channel policy, queuing through the message bus — is
|
|
168
|
+
xtr-notifier's.
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
uv run pytest
|
|
174
|
+
uv run ruff check
|
|
175
|
+
uv run ruff format --check
|
|
176
|
+
uv run basedpyright
|
|
177
|
+
uv run ty check
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# xtr-slack-notifier
|
|
4
|
+
|
|
5
|
+
**Slack for xtr-notifier — chat messages posted, scheduled and updated in a channel, with Block Kit blocks, configured by DSN.**
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why?
|
|
12
|
+
|
|
13
|
+
[xtr-notifier](../xtr-notifier) sends chat messages through whatever provider a DSN names. This
|
|
14
|
+
library is the Slack provider: installed beside it, a `slack://` DSN is served with no wiring, a
|
|
15
|
+
notification is rendered as Block Kit sections, and a message can be laid out with blocks,
|
|
16
|
+
scheduled for later, or updated once posted.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
uv add xtr-slack-notifier
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
It needs an HTTP client implementing
|
|
25
|
+
[xtr-http-client-contracts](../xtr-http-client-contracts) — usually
|
|
26
|
+
[xtr-http-client](../xtr-http-client).
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from xtr_http_client import HttpClient
|
|
32
|
+
from xtr_notifier import Chatter, Transport
|
|
33
|
+
from xtr_notifier.message import ChatMessage
|
|
34
|
+
from xtr_notifier.transport import TransportFactoryDiscovery
|
|
35
|
+
|
|
36
|
+
discovery = TransportFactoryDiscovery(
|
|
37
|
+
TransportFactoryDiscovery.CHATTER_GROUP, http_client=HttpClient.create()
|
|
38
|
+
)
|
|
39
|
+
transport = Transport(discovery=discovery).from_string("slack://xoxb-1234@default?channel=ops")
|
|
40
|
+
|
|
41
|
+
sent = await Chatter(transport).send(ChatMessage("Deploy finished."))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## The DSN
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
slack://TOKEN@default?channel=CHANNEL
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `TOKEN` — a bot (`xoxb-`), user (`xoxp-`) or app (`xoxa-2`) token; a DSN without one is refused
|
|
51
|
+
with `User is not set.`, and a token of another kind with
|
|
52
|
+
`A valid Slack token needs to start with "xoxb-", "xoxp-" or "xoxa-2". ...` when the transport
|
|
53
|
+
is built — on first use, since the notifier builds its transports lazily.
|
|
54
|
+
- `default` — `slack.com`; name another host (and port) for a test server.
|
|
55
|
+
- `channel` — the channel a message naming none is posted to.
|
|
56
|
+
- `ssl=false` — reach the host over plain HTTP.
|
|
57
|
+
|
|
58
|
+
The transport's string form — what errors, events and `debug:config` show — is
|
|
59
|
+
`slack://slack.com?channel=CHANNEL`: never the token. The token is sent as a bearer credential.
|
|
60
|
+
|
|
61
|
+
## Options and blocks
|
|
62
|
+
|
|
63
|
+
`SlackOptions` builds what a message carries beyond its text; `to_options()` renders it for the
|
|
64
|
+
message:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from xtr_notifier.message import ChatMessage
|
|
68
|
+
from xtr_slack_notifier import SlackOptions
|
|
69
|
+
from xtr_slack_notifier.block import SlackActionsBlock, SlackHeaderBlock, SlackSectionBlock
|
|
70
|
+
|
|
71
|
+
options = (
|
|
72
|
+
SlackOptions()
|
|
73
|
+
.recipient("C0123456789")
|
|
74
|
+
.block(SlackHeaderBlock("Deploy 42"))
|
|
75
|
+
.block(SlackSectionBlock().text("*Finished* in 42s.").field("*Env*").field("production"))
|
|
76
|
+
.block(SlackActionsBlock().button("Open", "https://example.com/deploys/42", "primary"))
|
|
77
|
+
)
|
|
78
|
+
await chatter.send(ChatMessage("Deploy 42 finished.", options.to_options()))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The setters are `recipient`, `as_user`, `icon_emoji`, `icon_url`, `link_names`, `mrkdwn`, `parse`,
|
|
82
|
+
`unfurl_links`, `unfurl_media`, `username`, `thread_ts` (reply in a thread), `post_at` (schedule
|
|
83
|
+
through `chat.scheduleMessage`) and `block`. A message takes up to 50 blocks.
|
|
84
|
+
|
|
85
|
+
The blocks, under `xtr_slack_notifier.block`: `SlackSectionBlock` (text, up to 10 fields, an
|
|
86
|
+
accessory), `SlackHeaderBlock`, `SlackContextBlock` (up to 10 texts and images),
|
|
87
|
+
`SlackActionsBlock` (up to 25 buttons), `SlackDividerBlock`, `SlackImageBlock` and
|
|
88
|
+
`SlackPlainTextInputBlock`; the elements `SlackButtonBlockElement` and `SlackImageBlockElement`.
|
|
89
|
+
Anything else implements `SlackBlockInterface`.
|
|
90
|
+
|
|
91
|
+
A message made from a notification and carrying no options is rendered with
|
|
92
|
+
`SlackOptions.from_notification(summary)`: the emoji as the icon, a section for the subject, one
|
|
93
|
+
for the content, and the exception's traceback after a divider.
|
|
94
|
+
|
|
95
|
+
Fields left empty — `None`, `""`, an empty list or mapping — are not sent; `False` and `0` are.
|
|
96
|
+
|
|
97
|
+
## Updating a message
|
|
98
|
+
|
|
99
|
+
The transport returns a `SlackSentMessage`, carrying the channel (`channel_id`) and the message's
|
|
100
|
+
`ts` (`message_id`). `get_update_message(subject, options)` makes the message replacing it, sent
|
|
101
|
+
through `chat.update`:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
sent = await chatter.send(ChatMessage("Deploying..."))
|
|
105
|
+
await chatter.send(sent.get_update_message("Deployed."))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Its options are `UpdateMessageSlackOptions(channel_id, ts, options)`, which render with the kind
|
|
109
|
+
`slack.update` — the transport accepts both kinds, and tells them apart by it after the message
|
|
110
|
+
crossed a message bus.
|
|
111
|
+
|
|
112
|
+
## Errors
|
|
113
|
+
|
|
114
|
+
| Error | When |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `IncompleteDsnError` | The DSN carries no token (`User is not set.`) |
|
|
117
|
+
| `InvalidArgumentError` | The token does not start with `xoxb-`, `xoxp-` or `xoxa-2` |
|
|
118
|
+
| `UnsupportedSchemeError` | The factory was handed a DSN that is not `slack://` |
|
|
119
|
+
| `UnsupportedMessageTypeError` | The transport was handed a message that is not a `ChatMessage` |
|
|
120
|
+
| `LogicError` | More than 50 blocks, 25 buttons, or 10 fields or context elements |
|
|
121
|
+
| `LengthError` | A header over 150 characters, an input label or placeholder over 150, an id over 255 |
|
|
122
|
+
| `TransportError` | `Unable to post the Slack message: "<error>" (<errors>).` when Slack answers `ok: false`, `Unable to post the Slack message: "<body>".` on another status, or `Could not reach the remote Slack server.` |
|
|
123
|
+
|
|
124
|
+
Every one of them is xtr-notifier's; `TransportError` is what failover moves on from.
|
|
125
|
+
|
|
126
|
+
## Use in an application
|
|
127
|
+
|
|
128
|
+
Everything adding this package to an application takes — and, read backwards, what removing it
|
|
129
|
+
undoes. It ships no bundle: the notifier's bundle finds its factory by the DSN scheme.
|
|
130
|
+
|
|
131
|
+
- **Install** — `uv add xtr-slack-notifier`, beside `xtr-notifier` set up as its
|
|
132
|
+
[*Use in an application*](../xtr-notifier/README.md#use-in-an-application) says.
|
|
133
|
+
- **Recipe** — `uv run xtr-recipes recipes:sync` does the *Environment* step below: it writes
|
|
134
|
+
`SLACK_DSN` in `.env`. It prints the step adding the transport to
|
|
135
|
+
`<app>/config/notifier.py`, which a recipe cannot make for you.
|
|
136
|
+
- **Configure** — add `slack=env("SLACK_DSN")` to `chatter_transports` in
|
|
137
|
+
`<app>/config/notifier.py`.
|
|
138
|
+
- **Environment** — `SLACK_DSN`, `slack://TOKEN@default?channel=CHANNEL` with the app's token and
|
|
139
|
+
the channel; read when a message is first sent.
|
|
140
|
+
- **Remove** — drop the `chatter_transports` entry and `SLACK_DSN`, then
|
|
141
|
+
`uv remove xtr-slack-notifier`.
|
|
142
|
+
- **Check** — `debug:config notifier` lists `slack` under `chatter_transports`.
|
|
143
|
+
|
|
144
|
+
Everything else — the bundle, the channel policy, queuing through the message bus — is
|
|
145
|
+
xtr-notifier's.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
uv run pytest
|
|
151
|
+
uv run ruff check
|
|
152
|
+
uv run ruff format --check
|
|
153
|
+
uv run basedpyright
|
|
154
|
+
uv run ty check
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xtr-slack-notifier"
|
|
3
|
+
version = "5.0.0"
|
|
4
|
+
description = "Slack for xtr-notifier — chat messages posted, scheduled and updated in a channel, with Block Kit blocks, configured by DSN."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"notifier",
|
|
11
|
+
"notification",
|
|
12
|
+
"slack",
|
|
13
|
+
"chat",
|
|
14
|
+
"block-kit",
|
|
15
|
+
"dsn",
|
|
16
|
+
"asyncio",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Framework :: AsyncIO",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"typing-extensions>=4.4",
|
|
30
|
+
"xtr-notifier>=5.0,<6",
|
|
31
|
+
"xtr-http-client-contracts>=5.0,<6",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[[project.authors]]
|
|
35
|
+
name = "Xterr"
|
|
36
|
+
email = "me@xterr.dev"
|
|
37
|
+
|
|
38
|
+
[project.entry-points."xtr_notifier.chatter_transport_factories"]
|
|
39
|
+
slack = "xtr_slack_notifier:SlackTransportFactory"
|
|
40
|
+
|
|
41
|
+
[project.entry-points.xtr_recipes]
|
|
42
|
+
slack = "xtr_slack_notifier.recipe"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"basedpyright>=1.21",
|
|
47
|
+
"ruff>=0.8",
|
|
48
|
+
"pytest>=8",
|
|
49
|
+
"pytest-cov>=5",
|
|
50
|
+
"anyio>=4.0",
|
|
51
|
+
"ty>=0.0.83",
|
|
52
|
+
"xtr-http-client>=5.0,<6",
|
|
53
|
+
"xtr-recipes>=5.0,<6",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[build-system]
|
|
57
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
58
|
+
build-backend = "uv_build"
|
|
59
|
+
|
|
60
|
+
[tool.basedpyright]
|
|
61
|
+
typeCheckingMode = "all"
|
|
62
|
+
pythonVersion = "3.11"
|
|
63
|
+
pythonPlatform = "All"
|
|
64
|
+
include = [
|
|
65
|
+
"src",
|
|
66
|
+
"tests",
|
|
67
|
+
]
|
|
68
|
+
exclude = [
|
|
69
|
+
"**/__pycache__",
|
|
70
|
+
"**/.venv",
|
|
71
|
+
"**/build",
|
|
72
|
+
"**/dist",
|
|
73
|
+
".tmp",
|
|
74
|
+
]
|
|
75
|
+
reportUnusedCallResult = "warning"
|
|
76
|
+
reportUnnecessaryTypeIgnoreComment = "error"
|
|
77
|
+
reportUnusedVariable = "error"
|
|
78
|
+
reportMissingParameterType = "error"
|
|
79
|
+
reportPrivateUsage = "error"
|
|
80
|
+
reportImplicitStringConcatenation = "none"
|
|
81
|
+
|
|
82
|
+
[[tool.basedpyright.executionEnvironments]]
|
|
83
|
+
root = "tests"
|
|
84
|
+
extraPaths = ["."]
|
|
85
|
+
reportPrivateUsage = "none"
|
|
86
|
+
|
|
87
|
+
[tool.ruff]
|
|
88
|
+
target-version = "py311"
|
|
89
|
+
line-length = 100
|
|
90
|
+
src = [
|
|
91
|
+
"src",
|
|
92
|
+
"tests",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[tool.ruff.lint]
|
|
96
|
+
select = ["ALL"]
|
|
97
|
+
ignore = [
|
|
98
|
+
"COM812",
|
|
99
|
+
"ISC001",
|
|
100
|
+
"D203",
|
|
101
|
+
"D213",
|
|
102
|
+
"CPY001",
|
|
103
|
+
"FBT001",
|
|
104
|
+
"FBT002",
|
|
105
|
+
"TD002",
|
|
106
|
+
"TD003",
|
|
107
|
+
"FIX002",
|
|
108
|
+
"TRY003",
|
|
109
|
+
"EM101",
|
|
110
|
+
"EM102",
|
|
111
|
+
]
|
|
112
|
+
fixable = ["ALL"]
|
|
113
|
+
unfixable = []
|
|
114
|
+
|
|
115
|
+
[tool.ruff.lint.per-file-ignores]
|
|
116
|
+
"tests/**/*.py" = [
|
|
117
|
+
"S101",
|
|
118
|
+
"ARG",
|
|
119
|
+
"PLR2004",
|
|
120
|
+
"SLF001",
|
|
121
|
+
"D",
|
|
122
|
+
"FBT003",
|
|
123
|
+
"S105",
|
|
124
|
+
"S106",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint.pydocstyle]
|
|
128
|
+
convention = "google"
|
|
129
|
+
|
|
130
|
+
[tool.ruff.format]
|
|
131
|
+
quote-style = "double"
|
|
132
|
+
indent-style = "space"
|
|
133
|
+
docstring-code-format = true
|
|
134
|
+
docstring-code-line-length = "dynamic"
|
|
135
|
+
|
|
136
|
+
[tool.ty.src]
|
|
137
|
+
include = [
|
|
138
|
+
"src",
|
|
139
|
+
"tests",
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
[tool.pytest.ini_options]
|
|
143
|
+
minversion = "8.0"
|
|
144
|
+
testpaths = ["tests"]
|
|
145
|
+
addopts = [
|
|
146
|
+
"-ra",
|
|
147
|
+
"--strict-config",
|
|
148
|
+
"--strict-markers",
|
|
149
|
+
]
|
|
150
|
+
filterwarnings = ["error"]
|
|
151
|
+
|
|
152
|
+
[tool.coverage.run]
|
|
153
|
+
source = ["src"]
|
|
154
|
+
branch = true
|
|
155
|
+
|
|
156
|
+
[tool.coverage.report]
|
|
157
|
+
fail_under = 95
|
|
158
|
+
exclude_lines = [
|
|
159
|
+
"pragma: no cover",
|
|
160
|
+
"if TYPE_CHECKING:",
|
|
161
|
+
"raise NotImplementedError",
|
|
162
|
+
'^\s*\.\.\.$',
|
|
163
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xtr-slack-notifier"
|
|
3
|
+
version = "5.0.0"
|
|
4
|
+
description = "Slack for xtr-notifier — chat messages posted, scheduled and updated in a channel, with Block Kit blocks, configured by DSN."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Xterr", email = "me@xterr.dev" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
keywords = ["notifier", "notification", "slack", "chat", "block-kit", "dsn", "asyncio"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Framework :: AsyncIO",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"typing-extensions>=4.4",
|
|
25
|
+
"xtr-notifier>=5.0,<6",
|
|
26
|
+
# The Web API is reached through the application's client.
|
|
27
|
+
"xtr-http-client-contracts>=5.0,<6",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Found by the chatter when a DSN's scheme is slack, with no wiring.
|
|
31
|
+
[project.entry-points."xtr_notifier.chatter_transport_factories"]
|
|
32
|
+
slack = "xtr_slack_notifier:SlackTransportFactory"
|
|
33
|
+
|
|
34
|
+
# Advertises the recipe — what *Use in an application* takes — for recipes:sync.
|
|
35
|
+
[project.entry-points."xtr_recipes"]
|
|
36
|
+
slack = "xtr_slack_notifier.recipe"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"basedpyright>=1.21",
|
|
41
|
+
"ruff>=0.8",
|
|
42
|
+
"pytest>=8",
|
|
43
|
+
"pytest-cov>=5",
|
|
44
|
+
"anyio>=4.0",
|
|
45
|
+
"ty>=0.0.83",
|
|
46
|
+
# MockHttpClient stands in for the Slack Web API in transport tests.
|
|
47
|
+
"xtr-http-client>=5.0,<6",
|
|
48
|
+
# Read the shipped recipe with the parser recipes:sync uses.
|
|
49
|
+
"xtr-recipes>=5.0,<6",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[build-system]
|
|
53
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
54
|
+
build-backend = "uv_build"
|
|
55
|
+
|
|
56
|
+
# ─────────────────────────────────────────────────────────────────
|
|
57
|
+
# basedpyright — typeCheckingMode = "all" sets every report flag to error
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────
|
|
59
|
+
[tool.basedpyright]
|
|
60
|
+
typeCheckingMode = "all"
|
|
61
|
+
pythonVersion = "3.11"
|
|
62
|
+
pythonPlatform = "All"
|
|
63
|
+
include = ["src", "tests"]
|
|
64
|
+
exclude = ["**/__pycache__", "**/.venv", "**/build", "**/dist", ".tmp"]
|
|
65
|
+
|
|
66
|
+
reportUnusedCallResult = "warning"
|
|
67
|
+
reportUnnecessaryTypeIgnoreComment = "error"
|
|
68
|
+
reportUnusedVariable = "error"
|
|
69
|
+
reportMissingParameterType = "error"
|
|
70
|
+
reportPrivateUsage = "error"
|
|
71
|
+
|
|
72
|
+
# An error message long enough to be useful has to wrap, and a 100-column
|
|
73
|
+
# limit puts the break inside the string.
|
|
74
|
+
reportImplicitStringConcatenation = "none"
|
|
75
|
+
|
|
76
|
+
# Tests check state a class keeps to itself, for the same reason ruff's
|
|
77
|
+
# SLF001 is off for tests.
|
|
78
|
+
[[tool.basedpyright.executionEnvironments]]
|
|
79
|
+
root = "tests"
|
|
80
|
+
extraPaths = ["."]
|
|
81
|
+
reportPrivateUsage = "none"
|
|
82
|
+
|
|
83
|
+
# ─────────────────────────────────────────────────────────────────
|
|
84
|
+
# ruff — select = ["ALL"], minimal justified ignores
|
|
85
|
+
# ─────────────────────────────────────────────────────────────────
|
|
86
|
+
[tool.ruff]
|
|
87
|
+
target-version = "py311"
|
|
88
|
+
line-length = 100
|
|
89
|
+
src = ["src", "tests"]
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint]
|
|
92
|
+
select = ["ALL"]
|
|
93
|
+
ignore = [
|
|
94
|
+
# Formatter conflicts (ruff documents these)
|
|
95
|
+
"COM812",
|
|
96
|
+
"ISC001",
|
|
97
|
+
# Mutually-exclusive docstring conventions; keep the modern one
|
|
98
|
+
"D203",
|
|
99
|
+
"D213",
|
|
100
|
+
# Project-specific noise
|
|
101
|
+
"CPY001",
|
|
102
|
+
"FBT001",
|
|
103
|
+
"FBT002",
|
|
104
|
+
"TD002",
|
|
105
|
+
"TD003",
|
|
106
|
+
"FIX002",
|
|
107
|
+
# Errors compose their own messages from typed fields; the string at a
|
|
108
|
+
# raise site is a `reason` data field.
|
|
109
|
+
"TRY003",
|
|
110
|
+
"EM101",
|
|
111
|
+
"EM102",
|
|
112
|
+
]
|
|
113
|
+
fixable = ["ALL"]
|
|
114
|
+
unfixable = []
|
|
115
|
+
|
|
116
|
+
[tool.ruff.lint.per-file-ignores]
|
|
117
|
+
"tests/**/*.py" = [
|
|
118
|
+
"S101", # assert is the point of pytest
|
|
119
|
+
"ARG", # fixtures look unused
|
|
120
|
+
"PLR2004", # magic numbers in test data
|
|
121
|
+
"SLF001", # tests reach into privates
|
|
122
|
+
"D", # test names are the docs
|
|
123
|
+
"FBT003", # a flag passed to a builder is a value
|
|
124
|
+
"S105", # fake secret literals are test data
|
|
125
|
+
"S106", # fake secret literals are test data
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
[tool.ruff.lint.pydocstyle]
|
|
129
|
+
convention = "google"
|
|
130
|
+
|
|
131
|
+
[tool.ruff.format]
|
|
132
|
+
quote-style = "double"
|
|
133
|
+
indent-style = "space"
|
|
134
|
+
docstring-code-format = true
|
|
135
|
+
docstring-code-line-length = "dynamic"
|
|
136
|
+
|
|
137
|
+
# ─────────────────────────────────────────────────────────────────
|
|
138
|
+
# ty — a second opinion on the source
|
|
139
|
+
# ─────────────────────────────────────────────────────────────────
|
|
140
|
+
[tool.ty.src]
|
|
141
|
+
include = ["src", "tests"]
|
|
142
|
+
|
|
143
|
+
# ─────────────────────────────────────────────────────────────────
|
|
144
|
+
# pytest
|
|
145
|
+
# ─────────────────────────────────────────────────────────────────
|
|
146
|
+
[tool.pytest.ini_options]
|
|
147
|
+
minversion = "8.0"
|
|
148
|
+
testpaths = ["tests"]
|
|
149
|
+
addopts = ["-ra", "--strict-config", "--strict-markers"]
|
|
150
|
+
filterwarnings = ["error"]
|
|
151
|
+
|
|
152
|
+
[tool.coverage.run]
|
|
153
|
+
source = ["src"]
|
|
154
|
+
branch = true
|
|
155
|
+
|
|
156
|
+
[tool.coverage.report]
|
|
157
|
+
# CI runs the tests with --cov; a change taking coverage below this fails there.
|
|
158
|
+
fail_under = 95
|
|
159
|
+
exclude_lines = [
|
|
160
|
+
"pragma: no cover",
|
|
161
|
+
"if TYPE_CHECKING:",
|
|
162
|
+
"raise NotImplementedError",
|
|
163
|
+
# A Protocol body states a contract; there is nothing there to run.
|
|
164
|
+
"^\\s*\\.\\.\\.$",
|
|
165
|
+
]
|