xtr-telegram-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 (26) hide show
  1. xtr_telegram_notifier-5.0.0/LICENSE +22 -0
  2. xtr_telegram_notifier-5.0.0/PKG-INFO +190 -0
  3. xtr_telegram_notifier-5.0.0/README.md +167 -0
  4. xtr_telegram_notifier-5.0.0/pyproject.toml +162 -0
  5. xtr_telegram_notifier-5.0.0/pyproject.toml.orig +164 -0
  6. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/.agents/skills/xtr-telegram-notifier/SKILL.md +118 -0
  7. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/__init__.py +15 -0
  8. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/_markdown_v2.py +24 -0
  9. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/_payload.py +186 -0
  10. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/py.typed +0 -0
  11. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/recipe/__init__.py +15 -0
  12. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/recipe/manifest.toml +23 -0
  13. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/__init__.py +5 -0
  14. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/__init__.py +17 -0
  15. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/abstract_telegram_reply_markup.py +25 -0
  16. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/button/__init__.py +9 -0
  17. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/button/abstract_keyboard_button.py +23 -0
  18. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/button/inline_keyboard_button.py +64 -0
  19. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/button/keyboard_button.py +34 -0
  20. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/force_reply.py +26 -0
  21. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/inline_keyboard_markup.py +30 -0
  22. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/reply_keyboard_markup.py +45 -0
  23. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/reply_keyboard_remove.py +26 -0
  24. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/telegram_options.py +235 -0
  25. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/telegram_transport.py +196 -0
  26. xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/telegram_transport_factory.py +69 -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,190 @@
1
+ Metadata-Version: 2.4
2
+ Name: xtr-telegram-notifier
3
+ Version: 5.0.0
4
+ Summary: Telegram for xtr-notifier — chat messages posted, edited and answered through a bot, with keyboards, media and file uploads, configured by DSN.
5
+ Keywords: notifier,notification,telegram,chat,bot,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-telegram-notifier
27
+
28
+ **Telegram for xtr-notifier — chat messages posted, edited and answered through a bot, with keyboards, media and file uploads, 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 Telegram provider: installed beside it, a `telegram://` DSN is served with no
38
+ wiring, and a message can carry everything the bot API offers — a keyboard, a photo, a location,
39
+ an edit of a message already sent, the answer to a button's callback.
40
+
41
+ ## Install
42
+
43
+ ```sh
44
+ uv add xtr-telegram-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(
63
+ "telegram://123456:ABC-DEF@default?channel=-1001234567890"
64
+ )
65
+
66
+ await Chatter(transport).send(ChatMessage("Deploy *finished* in 42s."))
67
+ ```
68
+
69
+ ## The DSN
70
+
71
+ ```
72
+ telegram://BOT_ID:SECRET@default?channel=CHAT_ID
73
+ ```
74
+
75
+ - `BOT_ID:SECRET` — the bot's token, as the bot API hands it out; a DSN without it, or with half
76
+ of it, is refused with `Missing token.` or `Malformed token.`
77
+ - `default` — `api.telegram.org`; name another host (and port) to reach a local bot API server.
78
+ - `channel` — the chat a message naming none goes to. To find a chat's id, message the bot from
79
+ that chat and read `https://api.telegram.org/bot<token>/getUpdates`.
80
+ - `ssl=false` — reach the host over plain HTTP; `sslmode=disable` is the older spelling.
81
+
82
+ The transport's string form — what errors, events and `debug:config` show — is
83
+ `telegram://api.telegram.org?channel=CHAT_ID`: never the token.
84
+
85
+ ## Options
86
+
87
+ `TelegramOptions` builds what a message carries beyond its text, one call at a time;
88
+ `to_options()` renders it for the message:
89
+
90
+ ```python
91
+ from xtr_notifier.message import ChatMessage
92
+ from xtr_telegram_notifier import TelegramOptions
93
+ from xtr_telegram_notifier.reply.markup import InlineKeyboardMarkup
94
+ from xtr_telegram_notifier.reply.markup.button import InlineKeyboardButton
95
+
96
+ options = (
97
+ TelegramOptions()
98
+ .chat_id("-1001234567890")
99
+ .disable_notification(True)
100
+ .reply_markup(
101
+ InlineKeyboardMarkup().inline_keyboard(
102
+ [InlineKeyboardButton("Open").url("https://example.com/deploys/42")]
103
+ )
104
+ )
105
+ )
106
+ await chatter.send(ChatMessage("Deploy finished.", options.to_options()))
107
+ ```
108
+
109
+ What the message is follows from the option set, and only one of these may be:
110
+
111
+ | Option | Method | The text goes in |
112
+ |---|---|---|
113
+ | none | `sendMessage` | `text` |
114
+ | `photo` / `upload_photo` | `sendPhoto` | `caption` |
115
+ | `video`, `audio`, `document`, `animation` (and `upload_*`) | `sendVideo`, … | `caption` |
116
+ | `sticker` / `upload_sticker` | `sendSticker` | not sent |
117
+ | `location`, `venue`, `contact` | `sendLocation`, `sendVenue`, `sendContact` | not sent |
118
+ | `edit(message_id)` | `editMessageText`, or `editMessageMedia` with one media by URL or file id | `text`, or the media's caption |
119
+ | `edit_caption(message_id)` | `editMessageCaption` | `caption` |
120
+ | `answer_callback_query(id, show_alert, cache_time)` | `answerCallbackQuery` | `text` |
121
+
122
+ Setting two of them raises `MultipleExclusiveOptionsUsedError`; editing a message's media with a
123
+ file to upload raises `LogicError` — the bot API takes a URL or a file id there.
124
+
125
+ The text is sent as MarkdownV2 unless `parse_mode(...)` picks `TelegramOptions.PARSE_MODE_HTML`
126
+ or `PARSE_MODE_MARKDOWN`. It is escaped first: the characters the bot API rejects unescaped
127
+ (`.`, `!`, `#`, `+`, `-`, `=`, `|`, `{`, `}`, `>`) are escaped, while markup written on purpose —
128
+ `*bold*`, `_italic_`, `` `code` ``, `~strike~`, `||spoiler||`, `[link](https://example.com)`, block quotations,
129
+ custom emojis — and characters already escaped are kept.
130
+
131
+ Keyboards: `InlineKeyboardMarkup` with `InlineKeyboardButton`s (link, login URL, callback data,
132
+ inline query, payment), `ReplyKeyboardMarkup` with `KeyboardButton`s (text, contact, location,
133
+ poll), `ReplyKeyboardRemove` and `ForceReply`.
134
+
135
+ ## Uploads
136
+
137
+ `upload_photo(path)` and its siblings keep the file's **path**, so a message stays plain data and
138
+ can be queued on a message bus. The transport opens each file when it sends the message, sends
139
+ the fields as a form body — which the HTTP client sends as `multipart/form-data`, the file named
140
+ after its path — and closes the files once the request is made, whether it succeeded or not.
141
+ A field holding an object or a list, such as a reply markup, goes in that body as JSON text, the
142
+ way the bot API reads it. A message without an upload is sent as JSON.
143
+
144
+ ## Errors
145
+
146
+ | Error | When |
147
+ |---|---|
148
+ | `IncompleteDsnError` | The DSN carries no token (`Missing token.`) or half of one (`Malformed token.`) |
149
+ | `UnsupportedSchemeError` | The factory was handed a DSN that is not `telegram://` |
150
+ | `UnsupportedMessageTypeError` | The transport was handed a message that is not a `ChatMessage` |
151
+ | `MultipleExclusiveOptionsUsedError` | The options pick more than one kind of message |
152
+ | `LogicError` | An edit replaces a media with a file to upload |
153
+ | `TransportError` | `Unable to post the Telegram message: <description> (code <n>).` (or `edit`, `answer callback query`), or `Could not reach the remote Telegram server.` |
154
+
155
+ Every one of them is xtr-notifier's; `TransportError` is what failover moves on from.
156
+
157
+ ## Use in an application
158
+
159
+ Everything adding this package to an application takes — and, read backwards, what removing it
160
+ undoes. It ships no bundle: the notifier's bundle finds its factory by the DSN scheme.
161
+
162
+ - **Install** — `uv add xtr-telegram-notifier`, beside `xtr-notifier` set up as its
163
+ [*Use in an application*](../xtr-notifier/README.md#use-in-an-application) says.
164
+ - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Environment* step below: it writes
165
+ `TELEGRAM_DSN` in `.env`. It prints the step adding the transport to
166
+ `<app>/config/notifier.py`, which a recipe cannot make for you.
167
+ - **Configure** — add `telegram=env("TELEGRAM_DSN")` to `chatter_transports` in
168
+ `<app>/config/notifier.py`.
169
+ - **Environment** — `TELEGRAM_DSN`, `telegram://TOKEN@default?channel=CHAT_ID` with the bot's
170
+ token and the chat; read when a message is first sent.
171
+ - **Remove** — drop the `chatter_transports` entry and `TELEGRAM_DSN`, then
172
+ `uv remove xtr-telegram-notifier`.
173
+ - **Check** — `debug:config notifier` lists `telegram` under `chatter_transports`.
174
+
175
+ Everything else — the bundle, the channel policy, queuing through the message bus — is
176
+ xtr-notifier's.
177
+
178
+ ## Development
179
+
180
+ ```sh
181
+ uv run pytest
182
+ uv run ruff check
183
+ uv run ruff format --check
184
+ uv run basedpyright
185
+ uv run ty check
186
+ ```
187
+
188
+ ## License
189
+
190
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,167 @@
1
+ <div align="center">
2
+
3
+ # xtr-telegram-notifier
4
+
5
+ **Telegram for xtr-notifier — chat messages posted, edited and answered through a bot, with keyboards, media and file uploads, 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 Telegram provider: installed beside it, a `telegram://` DSN is served with no
15
+ wiring, and a message can carry everything the bot API offers — a keyboard, a photo, a location,
16
+ an edit of a message already sent, the answer to a button's callback.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ uv add xtr-telegram-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(
40
+ "telegram://123456:ABC-DEF@default?channel=-1001234567890"
41
+ )
42
+
43
+ await Chatter(transport).send(ChatMessage("Deploy *finished* in 42s."))
44
+ ```
45
+
46
+ ## The DSN
47
+
48
+ ```
49
+ telegram://BOT_ID:SECRET@default?channel=CHAT_ID
50
+ ```
51
+
52
+ - `BOT_ID:SECRET` — the bot's token, as the bot API hands it out; a DSN without it, or with half
53
+ of it, is refused with `Missing token.` or `Malformed token.`
54
+ - `default` — `api.telegram.org`; name another host (and port) to reach a local bot API server.
55
+ - `channel` — the chat a message naming none goes to. To find a chat's id, message the bot from
56
+ that chat and read `https://api.telegram.org/bot<token>/getUpdates`.
57
+ - `ssl=false` — reach the host over plain HTTP; `sslmode=disable` is the older spelling.
58
+
59
+ The transport's string form — what errors, events and `debug:config` show — is
60
+ `telegram://api.telegram.org?channel=CHAT_ID`: never the token.
61
+
62
+ ## Options
63
+
64
+ `TelegramOptions` builds what a message carries beyond its text, one call at a time;
65
+ `to_options()` renders it for the message:
66
+
67
+ ```python
68
+ from xtr_notifier.message import ChatMessage
69
+ from xtr_telegram_notifier import TelegramOptions
70
+ from xtr_telegram_notifier.reply.markup import InlineKeyboardMarkup
71
+ from xtr_telegram_notifier.reply.markup.button import InlineKeyboardButton
72
+
73
+ options = (
74
+ TelegramOptions()
75
+ .chat_id("-1001234567890")
76
+ .disable_notification(True)
77
+ .reply_markup(
78
+ InlineKeyboardMarkup().inline_keyboard(
79
+ [InlineKeyboardButton("Open").url("https://example.com/deploys/42")]
80
+ )
81
+ )
82
+ )
83
+ await chatter.send(ChatMessage("Deploy finished.", options.to_options()))
84
+ ```
85
+
86
+ What the message is follows from the option set, and only one of these may be:
87
+
88
+ | Option | Method | The text goes in |
89
+ |---|---|---|
90
+ | none | `sendMessage` | `text` |
91
+ | `photo` / `upload_photo` | `sendPhoto` | `caption` |
92
+ | `video`, `audio`, `document`, `animation` (and `upload_*`) | `sendVideo`, … | `caption` |
93
+ | `sticker` / `upload_sticker` | `sendSticker` | not sent |
94
+ | `location`, `venue`, `contact` | `sendLocation`, `sendVenue`, `sendContact` | not sent |
95
+ | `edit(message_id)` | `editMessageText`, or `editMessageMedia` with one media by URL or file id | `text`, or the media's caption |
96
+ | `edit_caption(message_id)` | `editMessageCaption` | `caption` |
97
+ | `answer_callback_query(id, show_alert, cache_time)` | `answerCallbackQuery` | `text` |
98
+
99
+ Setting two of them raises `MultipleExclusiveOptionsUsedError`; editing a message's media with a
100
+ file to upload raises `LogicError` — the bot API takes a URL or a file id there.
101
+
102
+ The text is sent as MarkdownV2 unless `parse_mode(...)` picks `TelegramOptions.PARSE_MODE_HTML`
103
+ or `PARSE_MODE_MARKDOWN`. It is escaped first: the characters the bot API rejects unescaped
104
+ (`.`, `!`, `#`, `+`, `-`, `=`, `|`, `{`, `}`, `>`) are escaped, while markup written on purpose —
105
+ `*bold*`, `_italic_`, `` `code` ``, `~strike~`, `||spoiler||`, `[link](https://example.com)`, block quotations,
106
+ custom emojis — and characters already escaped are kept.
107
+
108
+ Keyboards: `InlineKeyboardMarkup` with `InlineKeyboardButton`s (link, login URL, callback data,
109
+ inline query, payment), `ReplyKeyboardMarkup` with `KeyboardButton`s (text, contact, location,
110
+ poll), `ReplyKeyboardRemove` and `ForceReply`.
111
+
112
+ ## Uploads
113
+
114
+ `upload_photo(path)` and its siblings keep the file's **path**, so a message stays plain data and
115
+ can be queued on a message bus. The transport opens each file when it sends the message, sends
116
+ the fields as a form body — which the HTTP client sends as `multipart/form-data`, the file named
117
+ after its path — and closes the files once the request is made, whether it succeeded or not.
118
+ A field holding an object or a list, such as a reply markup, goes in that body as JSON text, the
119
+ way the bot API reads it. A message without an upload is sent as JSON.
120
+
121
+ ## Errors
122
+
123
+ | Error | When |
124
+ |---|---|
125
+ | `IncompleteDsnError` | The DSN carries no token (`Missing token.`) or half of one (`Malformed token.`) |
126
+ | `UnsupportedSchemeError` | The factory was handed a DSN that is not `telegram://` |
127
+ | `UnsupportedMessageTypeError` | The transport was handed a message that is not a `ChatMessage` |
128
+ | `MultipleExclusiveOptionsUsedError` | The options pick more than one kind of message |
129
+ | `LogicError` | An edit replaces a media with a file to upload |
130
+ | `TransportError` | `Unable to post the Telegram message: <description> (code <n>).` (or `edit`, `answer callback query`), or `Could not reach the remote Telegram server.` |
131
+
132
+ Every one of them is xtr-notifier's; `TransportError` is what failover moves on from.
133
+
134
+ ## Use in an application
135
+
136
+ Everything adding this package to an application takes — and, read backwards, what removing it
137
+ undoes. It ships no bundle: the notifier's bundle finds its factory by the DSN scheme.
138
+
139
+ - **Install** — `uv add xtr-telegram-notifier`, beside `xtr-notifier` set up as its
140
+ [*Use in an application*](../xtr-notifier/README.md#use-in-an-application) says.
141
+ - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Environment* step below: it writes
142
+ `TELEGRAM_DSN` in `.env`. It prints the step adding the transport to
143
+ `<app>/config/notifier.py`, which a recipe cannot make for you.
144
+ - **Configure** — add `telegram=env("TELEGRAM_DSN")` to `chatter_transports` in
145
+ `<app>/config/notifier.py`.
146
+ - **Environment** — `TELEGRAM_DSN`, `telegram://TOKEN@default?channel=CHAT_ID` with the bot's
147
+ token and the chat; read when a message is first sent.
148
+ - **Remove** — drop the `chatter_transports` entry and `TELEGRAM_DSN`, then
149
+ `uv remove xtr-telegram-notifier`.
150
+ - **Check** — `debug:config notifier` lists `telegram` under `chatter_transports`.
151
+
152
+ Everything else — the bundle, the channel policy, queuing through the message bus — is
153
+ xtr-notifier's.
154
+
155
+ ## Development
156
+
157
+ ```sh
158
+ uv run pytest
159
+ uv run ruff check
160
+ uv run ruff format --check
161
+ uv run basedpyright
162
+ uv run ty check
163
+ ```
164
+
165
+ ## License
166
+
167
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,162 @@
1
+ [project]
2
+ name = "xtr-telegram-notifier"
3
+ version = "5.0.0"
4
+ description = "Telegram for xtr-notifier — chat messages posted, edited and answered through a bot, with keyboards, media and file uploads, 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
+ "telegram",
13
+ "chat",
14
+ "bot",
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
+ telegram = "xtr_telegram_notifier:TelegramTransportFactory"
40
+
41
+ [project.entry-points.xtr_recipes]
42
+ telegram = "xtr_telegram_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
+ ]