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.
Files changed (30) hide show
  1. xtr_slack_notifier-5.0.0/LICENSE +22 -0
  2. xtr_slack_notifier-5.0.0/PKG-INFO +182 -0
  3. xtr_slack_notifier-5.0.0/README.md +159 -0
  4. xtr_slack_notifier-5.0.0/pyproject.toml +163 -0
  5. xtr_slack_notifier-5.0.0/pyproject.toml.orig +165 -0
  6. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/.agents/skills/xtr-slack-notifier/SKILL.md +107 -0
  7. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/__init__.py +23 -0
  8. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/__init__.py +33 -0
  9. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/_text_object.py +15 -0
  10. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/abstract_slack_block.py +25 -0
  11. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/abstract_slack_block_element.py +25 -0
  12. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_actions_block.py +53 -0
  13. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_block_element_interface.py +16 -0
  14. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_block_interface.py +16 -0
  15. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_button_block_element.py +42 -0
  16. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_context_block.py +55 -0
  17. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_divider_block.py +19 -0
  18. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_header_block.py +41 -0
  19. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_image_block.py +19 -0
  20. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_image_block_element.py +19 -0
  21. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_plain_text_input_block.py +49 -0
  22. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/block/slack_section_block.py +54 -0
  23. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/py.typed +0 -0
  24. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/recipe/__init__.py +15 -0
  25. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/recipe/manifest.toml +23 -0
  26. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/slack_options.py +162 -0
  27. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/slack_sent_message.py +44 -0
  28. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/slack_transport.py +161 -0
  29. xtr_slack_notifier-5.0.0/src/xtr_slack_notifier/slack_transport_factory.py +56 -0
  30. 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
+ ]