xtr-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_notifier-5.0.0/LICENSE +22 -0
- xtr_notifier-5.0.0/PKG-INFO +300 -0
- xtr_notifier-5.0.0/README.md +270 -0
- xtr_notifier-5.0.0/pyproject.toml +186 -0
- xtr_notifier-5.0.0/pyproject.toml.orig +197 -0
- xtr_notifier-5.0.0/src/xtr_notifier/.agents/skills/xtr-notifier/SKILL.md +143 -0
- xtr_notifier-5.0.0/src/xtr_notifier/.agents/skills/xtr-notifier/references/bridges.md +200 -0
- xtr_notifier-5.0.0/src/xtr_notifier/__init__.py +28 -0
- xtr_notifier-5.0.0/src/xtr_notifier/_message_sender.py +64 -0
- xtr_notifier-5.0.0/src/xtr_notifier/admin_recipients_provider_interface.py +26 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/__init__.py +21 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/_chat_message_handler.py +34 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/_failed_messages.py +15 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/_message_bus.py +39 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/_peer_factories.py +46 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/_peer_wiring.py +102 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/_peers.py +52 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/_services.py +221 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/_text_message_handler.py +35 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/admin_recipient_config.py +35 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/notifier_bundle.py +233 -0
- xtr_notifier-5.0.0/src/xtr_notifier/bundle/notifier_config.py +74 -0
- xtr_notifier-5.0.0/src/xtr_notifier/channel/__init__.py +21 -0
- xtr_notifier-5.0.0/src/xtr_notifier/channel/abstract_channel.py +59 -0
- xtr_notifier-5.0.0/src/xtr_notifier/channel/channel_interface.py +41 -0
- xtr_notifier-5.0.0/src/xtr_notifier/channel/channel_policy.py +51 -0
- xtr_notifier-5.0.0/src/xtr_notifier/channel/channel_policy_interface.py +19 -0
- xtr_notifier-5.0.0/src/xtr_notifier/channel/chat_channel.py +51 -0
- xtr_notifier-5.0.0/src/xtr_notifier/channel/push_channel.py +51 -0
- xtr_notifier-5.0.0/src/xtr_notifier/channel/sms_channel.py +58 -0
- xtr_notifier-5.0.0/src/xtr_notifier/chatter.py +28 -0
- xtr_notifier-5.0.0/src/xtr_notifier/chatter_interface.py +28 -0
- xtr_notifier-5.0.0/src/xtr_notifier/event/__init__.py +10 -0
- xtr_notifier-5.0.0/src/xtr_notifier/event/failed_message_event.py +39 -0
- xtr_notifier-5.0.0/src/xtr_notifier/event/message_event.py +39 -0
- xtr_notifier-5.0.0/src/xtr_notifier/event/notification_events.py +49 -0
- xtr_notifier-5.0.0/src/xtr_notifier/event/sent_message_event.py +28 -0
- xtr_notifier-5.0.0/src/xtr_notifier/event_listener/__init__.py +11 -0
- xtr_notifier-5.0.0/src/xtr_notifier/event_listener/notification_logger_listener.py +60 -0
- xtr_notifier-5.0.0/src/xtr_notifier/event_listener/send_failed_message_to_notifier_listener.py +61 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/__init__.py +31 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/incomplete_dsn_error.py +31 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/invalid_argument_error.py +15 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/length_error.py +15 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/logic_error.py +16 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/missing_required_option_error.py +27 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/multiple_exclusive_options_used_error.py +42 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/notifier_error.py +9 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/notifier_runtime_error.py +15 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/transport_error.py +43 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/unsupported_message_type_error.py +37 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/unsupported_options_error.py +37 -0
- xtr_notifier-5.0.0/src/xtr_notifier/exception/unsupported_scheme_error.py +77 -0
- xtr_notifier-5.0.0/src/xtr_notifier/message/__init__.py +23 -0
- xtr_notifier-5.0.0/src/xtr_notifier/message/chat_message.py +62 -0
- xtr_notifier-5.0.0/src/xtr_notifier/message/message_interface.py +35 -0
- xtr_notifier-5.0.0/src/xtr_notifier/message/message_options.py +45 -0
- xtr_notifier-5.0.0/src/xtr_notifier/message/message_options_interface.py +30 -0
- xtr_notifier-5.0.0/src/xtr_notifier/message/null_message.py +59 -0
- xtr_notifier-5.0.0/src/xtr_notifier/message/push_message.py +69 -0
- xtr_notifier-5.0.0/src/xtr_notifier/message/sent_message.py +76 -0
- xtr_notifier-5.0.0/src/xtr_notifier/message/sms_message.py +95 -0
- xtr_notifier-5.0.0/src/xtr_notifier/messenger/__init__.py +36 -0
- xtr_notifier-5.0.0/src/xtr_notifier/messenger/message_handler.py +38 -0
- xtr_notifier-5.0.0/src/xtr_notifier/notification/__init__.py +16 -0
- xtr_notifier-5.0.0/src/xtr_notifier/notification/chat_notification_interface.py +33 -0
- xtr_notifier-5.0.0/src/xtr_notifier/notification/importance.py +20 -0
- xtr_notifier-5.0.0/src/xtr_notifier/notification/notification.py +171 -0
- xtr_notifier-5.0.0/src/xtr_notifier/notification/notification_summary.py +42 -0
- xtr_notifier-5.0.0/src/xtr_notifier/notification/push_notification_interface.py +33 -0
- xtr_notifier-5.0.0/src/xtr_notifier/notification/sms_notification_interface.py +33 -0
- xtr_notifier-5.0.0/src/xtr_notifier/notifier.py +137 -0
- xtr_notifier-5.0.0/src/xtr_notifier/notifier_interface.py +24 -0
- xtr_notifier-5.0.0/src/xtr_notifier/py.typed +0 -0
- xtr_notifier-5.0.0/src/xtr_notifier/recipe/__init__.py +15 -0
- xtr_notifier-5.0.0/src/xtr_notifier/recipe/files/config/notifier.py.tmpl +29 -0
- xtr_notifier-5.0.0/src/xtr_notifier/recipe/manifest.toml +31 -0
- xtr_notifier-5.0.0/src/xtr_notifier/recipient/__init__.py +17 -0
- xtr_notifier-5.0.0/src/xtr_notifier/recipient/email_recipient_interface.py +24 -0
- xtr_notifier-5.0.0/src/xtr_notifier/recipient/no_recipient.py +20 -0
- xtr_notifier-5.0.0/src/xtr_notifier/recipient/recipient.py +66 -0
- xtr_notifier-5.0.0/src/xtr_notifier/recipient/recipient_interface.py +18 -0
- xtr_notifier-5.0.0/src/xtr_notifier/recipient/sms_recipient_interface.py +19 -0
- xtr_notifier-5.0.0/src/xtr_notifier/testing/__init__.py +52 -0
- xtr_notifier-5.0.0/src/xtr_notifier/testing/_factory_assertions.py +86 -0
- xtr_notifier-5.0.0/src/xtr_notifier/testing/_failures.py +21 -0
- xtr_notifier-5.0.0/src/xtr_notifier/testing/_notification_assertions.py +120 -0
- xtr_notifier-5.0.0/src/xtr_notifier/testing/_transport_assertions.py +98 -0
- xtr_notifier-5.0.0/src/xtr_notifier/testing/factory_cases.py +56 -0
- xtr_notifier-5.0.0/src/xtr_notifier/testing/transport_cases.py +48 -0
- xtr_notifier-5.0.0/src/xtr_notifier/texter.py +28 -0
- xtr_notifier-5.0.0/src/xtr_notifier/texter_interface.py +28 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/__init__.py +31 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/abstract_transport.py +123 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/abstract_transport_factory.py +93 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/dsn.py +187 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/failover_transport.py +66 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/null_transport.py +56 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/null_transport_factory.py +66 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/round_robin_transport.py +135 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/transport.py +106 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/transport_factory_discovery.py +154 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/transport_factory_interface.py +32 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/transport_interface.py +33 -0
- xtr_notifier-5.0.0/src/xtr_notifier/transport/transports.py +71 -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,300 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xtr-notifier
|
|
3
|
+
Version: 5.0.0
|
|
4
|
+
Summary: Send notifications to chat, SMS and push channels — one notification, the channels its importance or its recipient call for, any provider configured by DSN.
|
|
5
|
+
Keywords: notifier,notification,chat,sms,push,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-clock>=5.0,<6
|
|
20
|
+
Requires-Dist: xtr-event-dispatcher-contracts>=5.0,<6
|
|
21
|
+
Requires-Dist: xtr-http-client-contracts>=5.0,<6
|
|
22
|
+
Requires-Dist: xtr-logging-contracts>=5.0,<6
|
|
23
|
+
Requires-Dist: xtr-service-contracts>=5.0,<6
|
|
24
|
+
Requires-Dist: xtr-dependency-injection>=5.0,<6 ; extra == 'di'
|
|
25
|
+
Requires-Dist: xtr-messenger>=5.0,<6 ; extra == 'messenger'
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Provides-Extra: di
|
|
28
|
+
Provides-Extra: messenger
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
<div align="center">
|
|
32
|
+
|
|
33
|
+
# xtr-notifier
|
|
34
|
+
|
|
35
|
+
**Send notifications to chat, SMS and push channels — one notification, the channels its importance or its recipient call for, any provider configured by DSN.**
|
|
36
|
+
|
|
37
|
+
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
|
|
38
|
+
<img alt="asyncio" src="https://img.shields.io/badge/asyncio-native-1f6feb">
|
|
39
|
+
<img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
|
|
40
|
+
<img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
|
|
41
|
+
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Why?
|
|
47
|
+
|
|
48
|
+
"The disk is almost full" has to reach someone: a chat room during the day, a phone at night,
|
|
49
|
+
both when it is urgent. Code that says *what happened* should not also decide *which provider,
|
|
50
|
+
which channel, which token*. Here a notification carries a subject, a body and an importance;
|
|
51
|
+
the notifier picks the channels — from the notification, or from a policy keyed by its
|
|
52
|
+
importance — and each channel hands a message to a transport chosen by a DSN
|
|
53
|
+
(`slack://TOKEN@default?channel=ops`). Changing provider is changing a string.
|
|
54
|
+
|
|
55
|
+
- **Channels** — `chat`, `sms` and `push`, each built from the notification or rendered by it.
|
|
56
|
+
- **Transports by DSN** — one string per provider; `a || b` falls over, `a && b` takes turns.
|
|
57
|
+
- **Providers are libraries** — each installed beside this one and found by its DSN scheme.
|
|
58
|
+
- **Queueable** — messages are frozen values, so they cross a message bus and are sent by a
|
|
59
|
+
worker.
|
|
60
|
+
- **Testable** — a logger listener records every message; `xtr_notifier.testing` asserts on it,
|
|
61
|
+
and holds a provider library's transport and factory to what every provider owes.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
uv add xtr-notifier
|
|
67
|
+
uv add "xtr-notifier[messenger]" # queue messages through xtr-messenger
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A provider's transport reaches it through an HTTP client: install
|
|
71
|
+
[xtr-http-client](../xtr-http-client), or bring any
|
|
72
|
+
[xtr-http-client-contracts](../xtr-http-client-contracts) implementation.
|
|
73
|
+
|
|
74
|
+
## Quick start
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from xtr_notifier import Notifier, Transport
|
|
78
|
+
from xtr_notifier.channel import ChatChannel, ChannelPolicy, SmsChannel
|
|
79
|
+
from xtr_notifier.notification import Importance, Notification
|
|
80
|
+
from xtr_notifier.recipient import Recipient
|
|
81
|
+
from xtr_notifier.transport import TransportFactoryDiscovery
|
|
82
|
+
|
|
83
|
+
chat = Transport(
|
|
84
|
+
discovery=TransportFactoryDiscovery(TransportFactoryDiscovery.CHATTER_GROUP, http_client=client)
|
|
85
|
+
).from_strings({"ops": "null://null"})
|
|
86
|
+
texts = Transport(
|
|
87
|
+
discovery=TransportFactoryDiscovery(TransportFactoryDiscovery.TEXTER_GROUP, http_client=client)
|
|
88
|
+
).from_strings({"sms": "null://null"})
|
|
89
|
+
|
|
90
|
+
notifier = Notifier(
|
|
91
|
+
{"chat": ChatChannel(chat), "sms": SmsChannel(texts)},
|
|
92
|
+
ChannelPolicy({"urgent": ["sms", "chat"], "high": ["chat"]}),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
await notifier.send(
|
|
96
|
+
Notification("Disk almost full", content="98% used on db-1", importance=Importance.URGENT),
|
|
97
|
+
Recipient(phone="+3312345678"),
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`null://null` sends nowhere, and needs no HTTP client: swap it for a provider's DSN once its
|
|
102
|
+
library is installed.
|
|
103
|
+
|
|
104
|
+
## Notifications
|
|
105
|
+
|
|
106
|
+
`Notification(subject, *, content="", importance=Importance.HIGH, emoji="", channels=(),
|
|
107
|
+
exception=None)` is built in one call and read through properties.
|
|
108
|
+
|
|
109
|
+
- **Channels** — `channels=["chat/slack", "sms"]` names them; the part after `/` is the
|
|
110
|
+
transport. Left empty, the notifier asks its `ChannelPolicy` for the importance's channels.
|
|
111
|
+
- **Importance** — `Importance.URGENT`, `HIGH`, `MEDIUM`, `LOW`, a `StrEnum` whose values key
|
|
112
|
+
the policy. `Notification.importance_from_level("critical")` maps a log level: critical and
|
|
113
|
+
above is urgent, error high, the rest low.
|
|
114
|
+
- **Exceptions** — `Notification.from_exception(error, channels=...)` names the subject after
|
|
115
|
+
the error and keeps its traceback; the emoji follows the importance.
|
|
116
|
+
- **Per recipient** — override `get_channels(recipient)`, or implement
|
|
117
|
+
`as_chat_message(recipient, transport_name)`, `as_sms_message(...)` or
|
|
118
|
+
`as_push_message(...)` to render a channel's message yourself (`ChatNotificationInterface`
|
|
119
|
+
and friends, in `xtr_notifier.notification.<kind>_notification_interface`).
|
|
120
|
+
|
|
121
|
+
Recipients are anything with an `email` or a `phone`; `Recipient(email, phone)` is the plain one,
|
|
122
|
+
and `NoRecipient()` — what `send` uses with no recipient — is a chat room or a topic.
|
|
123
|
+
|
|
124
|
+
## Messages, the chatter and the texter
|
|
125
|
+
|
|
126
|
+
A channel turns a notification into a message: `ChatMessage(subject)`,
|
|
127
|
+
`SmsMessage(phone, subject, from_="")`, `PushMessage(subject, content)`. Messages are frozen;
|
|
128
|
+
`with_transport(...)`, `with_options(...)` and friends return copies. Provider options — blocks,
|
|
129
|
+
buttons, a parse mode — are a frozen `MessageOptions(kind, recipient_id, values)` that a
|
|
130
|
+
provider's builder renders, so a message survives a trip through a bus.
|
|
131
|
+
|
|
132
|
+
When the code has a message rather than a notification, send it directly:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from xtr_notifier import Chatter, Texter
|
|
136
|
+
|
|
137
|
+
await Chatter(chat).send(ChatMessage("Deployed 4.1.0", transport="ops"))
|
|
138
|
+
await Texter(texts).send(SmsMessage("+3312345678", "Your code is 4812"))
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Given a message bus, the chatter, the texter and the channels queue instead of sending, and
|
|
142
|
+
return `None`; `xtr_notifier.messenger.MessageHandler(transport)` sends what the bus hands back.
|
|
143
|
+
|
|
144
|
+
## Transports and DSNs
|
|
145
|
+
|
|
146
|
+
`Transport(factories, discovery)` builds transports from DSNs: one DSN gives one transport,
|
|
147
|
+
`a || b` a `FailoverTransport` (sticks to the first that works), `a && b` a
|
|
148
|
+
`RoundRobinTransport` (takes turns). A transport that raises `TransportError` is skipped for 60
|
|
149
|
+
seconds. `from_strings({...})` gives `Transports`, which sends a message through the transport
|
|
150
|
+
it names, or the first that supports it.
|
|
151
|
+
|
|
152
|
+
A DSN is `scheme://user:password@host:port/path?option=value`; `host` is `default` for the
|
|
153
|
+
provider's own API host. A factory serves a scheme: the ones given first, then those installed
|
|
154
|
+
libraries advertise in the group the builder reads — the chatter's
|
|
155
|
+
(`xtr_notifier.chatter_transport_factories`) or the texter's
|
|
156
|
+
(`xtr_notifier.texter_transport_factories`). A scheme nothing serves raises
|
|
157
|
+
`UnsupportedSchemeError`, naming the library to install when it is a known one.
|
|
158
|
+
|
|
159
|
+
## Providers
|
|
160
|
+
|
|
161
|
+
Each provider is a library of its own, installed beside this one; its factory is found by its
|
|
162
|
+
DSN scheme with no wiring. This package ships one transport, `null`, in both groups.
|
|
163
|
+
|
|
164
|
+
| Library | Scheme | Kind | DSN |
|
|
165
|
+
|---|---|---|---|
|
|
166
|
+
| xtr-notifier | `null` | chat, SMS, push | `null://null` |
|
|
167
|
+
| xtr-telegram-notifier | `telegram` | chat | `telegram://BOT_ID:TOKEN@default?channel=CHAT_ID` |
|
|
168
|
+
| xtr-slack-notifier | `slack` | chat | `slack://TOKEN@default?channel=CHANNEL` |
|
|
169
|
+
|
|
170
|
+
Writing one is a package with an `AbstractTransport`, an `AbstractTransportFactory` and an entry
|
|
171
|
+
point; the agent skill's `references/bridges.md` walks through it.
|
|
172
|
+
|
|
173
|
+
## Testing
|
|
174
|
+
|
|
175
|
+
Record what was sent with a `NotificationLoggerListener` (the bundle registers one when the
|
|
176
|
+
event dispatcher is active) and assert on it:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from xtr_notifier.testing import assert_notification_count, assert_notification_subject_contains
|
|
180
|
+
|
|
181
|
+
assert_notification_count(listener.events, 1, "ops")
|
|
182
|
+
assert_notification_subject_contains(listener.events.get_messages()[0], "Disk")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A provider library checks its transport and factory with `TransportCases` /
|
|
186
|
+
`assert_transport_cases` and `FactoryCases` / `assert_factory_cases`, which report every case
|
|
187
|
+
that fails at once. None of it imports a test framework.
|
|
188
|
+
|
|
189
|
+
## Use in an application
|
|
190
|
+
|
|
191
|
+
Everything adding this package to an application on
|
|
192
|
+
[xtr-dependency-injection](../xtr-dependency-injection) takes — and, read backwards, what
|
|
193
|
+
removing it undoes.
|
|
194
|
+
|
|
195
|
+
- **Install** — `uv add "xtr-notifier[messenger]"`, or `uv add xtr-notifier` to send in-process
|
|
196
|
+
only. Each provider is a library of its own (see [Providers](#providers)). It works with
|
|
197
|
+
[xtr-http-client](../xtr-http-client), which providers are reached through,
|
|
198
|
+
[xtr-event-dispatcher](../xtr-event-dispatcher), which announces every message and records
|
|
199
|
+
them for tests, and [xtr-messenger](../xtr-messenger), which queues them — each used when
|
|
200
|
+
installed.
|
|
201
|
+
- **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate*, *Configure* and
|
|
202
|
+
*Environment* steps below: it lists `NotifierBundle`, writes a starting
|
|
203
|
+
`<app>/config/notifier.py` with one chat and one SMS-and-push transport, and
|
|
204
|
+
`NOTIFIER_CHATTER_DSN` and `NOTIFIER_TEXTER_DSN`, both `null://null`, in `.env`. There is no
|
|
205
|
+
ignore line to write. It prints the steps to install a provider and to route the messages to
|
|
206
|
+
a worker, which a recipe cannot make for you.
|
|
207
|
+
- **Activate** — `NotifierBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`, imported
|
|
208
|
+
from `xtr_notifier.bundle`.
|
|
209
|
+
- **Brings along** — the HTTP client, messenger and event dispatcher bundles, each when its
|
|
210
|
+
package is installed.
|
|
211
|
+
- **Configure** — the zero-config notifier has no channel and refuses every notification with
|
|
212
|
+
`The "chat" channel does not exist.`; transports, the channel policy and the administrators go
|
|
213
|
+
in `<app>/config/notifier.py`, a `@configure` function returning `NotifierConfig` — see
|
|
214
|
+
[Kernel / bundle](#kernel--bundle). To queue messages, route `ChatMessage`, `SmsMessage` and
|
|
215
|
+
`PushMessage` in `<app>/config/messenger.py`.
|
|
216
|
+
- **Environment** — `NOTIFIER_CHATTER_DSN` and `NOTIFIER_TEXTER_DSN`, the DSNs the written
|
|
217
|
+
configuration reads when a message is first sent. A provider's DSN carries its token.
|
|
218
|
+
- **Ignore** — nothing.
|
|
219
|
+
- **Remove** — drop the `BUNDLES` entry, delete `<app>/config/notifier.py` and the two variables,
|
|
220
|
+
then `uv remove xtr-notifier`.
|
|
221
|
+
- **Check** — `debug:bundles` shows `notifier` as `listed` and `active`; `debug:config notifier`
|
|
222
|
+
shows the transports, the channel policy and the administrators.
|
|
223
|
+
|
|
224
|
+
## Kernel / bundle
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# app/config/notifier.py
|
|
228
|
+
from xtr_dependency_injection import configure, env
|
|
229
|
+
from xtr_notifier.bundle import AdminRecipientConfig, NotifierConfig
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@configure
|
|
233
|
+
def notifier() -> NotifierConfig:
|
|
234
|
+
return NotifierConfig(
|
|
235
|
+
chatter_transports={"slack": env("SLACK_DSN"), "ops": "telegram://... || slack://..."},
|
|
236
|
+
texter_transports={"sms": env("SMS_DSN")},
|
|
237
|
+
channel_policy={"urgent": ["sms", "chat/slack"], "high": ["chat/slack"]},
|
|
238
|
+
admin_recipients=[AdminRecipientConfig("ops@example.com", "+3312345678")],
|
|
239
|
+
notification_on_failed_messages=True,
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
| Field | Default | What it does |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| `chatter_transports` | `{}` | Chat transports by name; any brings the `chat` channel and `ChatterInterface` |
|
|
246
|
+
| `texter_transports` | `{}` | SMS and push transports; any brings the `sms` and `push` channels and `TexterInterface` |
|
|
247
|
+
| `channel_policy` | `{}` | Channels by importance value, for a notification naming none |
|
|
248
|
+
| `admin_recipients` | `()` | Who the application's own failures are reported to (`email` required) |
|
|
249
|
+
| `notification_on_failed_messages` | `False` | Report a message the bus gave up on to the administrators |
|
|
250
|
+
| `http_client` | `None` | Qualifier of the HTTP client providers are reached through |
|
|
251
|
+
|
|
252
|
+
What the bundle registers:
|
|
253
|
+
|
|
254
|
+
- `NotifierInterface` / `AdminRecipientsProviderInterface` — always.
|
|
255
|
+
- A `Transport` builder and `Transports` per kind, under the qualifiers `chatter` and `texter`,
|
|
256
|
+
plus the chatter, the texter and the channels — only for a kind with transports. Nothing reads
|
|
257
|
+
a DSN before a message is sent, so a DSN given through `env()` is checked where it is used.
|
|
258
|
+
- With the messenger bundle: a message whose type the bus routes (by class, by its wire name, or
|
|
259
|
+
`*`) is queued, and a handler sends it through the same transports; an unrouted one is sent at
|
|
260
|
+
once. The texter queues only when both SMS and push messages are routed.
|
|
261
|
+
- With the event dispatcher bundle: a `NotificationLoggerListener`, and every message announced.
|
|
262
|
+
- `notification_on_failed_messages=True` without the messenger or the event dispatcher bundle
|
|
263
|
+
fails the build with `LogicError`.
|
|
264
|
+
|
|
265
|
+
Extension points, for an application or another bundle:
|
|
266
|
+
|
|
267
|
+
- Any class deriving from `TransportFactoryInterface` the application registers serves both
|
|
268
|
+
kinds; tag one `CHATTER_TRANSPORT_FACTORY_TAG` or `TEXTER_TRANSPORT_FACTORY_TAG` to serve one.
|
|
269
|
+
A factory keeping `AbstractTransportFactory`'s constructor is handed the HTTP client and the
|
|
270
|
+
event dispatcher; one with a constructor of its own is built by the container.
|
|
271
|
+
- Tag a `ChannelInterface` service `CHANNEL_TAG` with `channel="<name>"` to add a channel.
|
|
272
|
+
|
|
273
|
+
## Errors
|
|
274
|
+
|
|
275
|
+
Every error derives from `NotifierError`.
|
|
276
|
+
|
|
277
|
+
| Error | Raised when |
|
|
278
|
+
|---|---|
|
|
279
|
+
| `InvalidArgumentError` | A DSN cannot be read, an option is not a boolean, a transport name is unknown |
|
|
280
|
+
| `IncompleteDsnError` | A DSN lacks a part its provider needs — a token, a user |
|
|
281
|
+
| `MissingRequiredOptionError` | A DSN lacks a required option |
|
|
282
|
+
| `UnsupportedSchemeError` | No factory serves a DSN's scheme |
|
|
283
|
+
| `LogicError` | A channel does not exist or cannot reach the recipient; no transport supports a message |
|
|
284
|
+
| `UnsupportedMessageTypeError` | A transport is handed a message of another kind |
|
|
285
|
+
| `UnsupportedOptionsError` | A transport is handed another provider's options |
|
|
286
|
+
| `MultipleExclusiveOptionsUsedError` | Options a provider takes only one of were set together |
|
|
287
|
+
| `LengthError` | A value is longer than the provider accepts |
|
|
288
|
+
| `TransportError` | A provider could not be reached or refused a message (carries the response, when one came) |
|
|
289
|
+
| `NotifierRuntimeError` | Every transport of a failover or round robin failed |
|
|
290
|
+
|
|
291
|
+
## Development
|
|
292
|
+
|
|
293
|
+
```sh
|
|
294
|
+
cd packages/xtr-notifier
|
|
295
|
+
uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# xtr-notifier
|
|
4
|
+
|
|
5
|
+
**Send notifications to chat, SMS and push channels — one notification, the channels its importance or its recipient call for, any provider configured by DSN.**
|
|
6
|
+
|
|
7
|
+
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
|
|
8
|
+
<img alt="asyncio" src="https://img.shields.io/badge/asyncio-native-1f6feb">
|
|
9
|
+
<img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
|
|
10
|
+
<img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why?
|
|
17
|
+
|
|
18
|
+
"The disk is almost full" has to reach someone: a chat room during the day, a phone at night,
|
|
19
|
+
both when it is urgent. Code that says *what happened* should not also decide *which provider,
|
|
20
|
+
which channel, which token*. Here a notification carries a subject, a body and an importance;
|
|
21
|
+
the notifier picks the channels — from the notification, or from a policy keyed by its
|
|
22
|
+
importance — and each channel hands a message to a transport chosen by a DSN
|
|
23
|
+
(`slack://TOKEN@default?channel=ops`). Changing provider is changing a string.
|
|
24
|
+
|
|
25
|
+
- **Channels** — `chat`, `sms` and `push`, each built from the notification or rendered by it.
|
|
26
|
+
- **Transports by DSN** — one string per provider; `a || b` falls over, `a && b` takes turns.
|
|
27
|
+
- **Providers are libraries** — each installed beside this one and found by its DSN scheme.
|
|
28
|
+
- **Queueable** — messages are frozen values, so they cross a message bus and are sent by a
|
|
29
|
+
worker.
|
|
30
|
+
- **Testable** — a logger listener records every message; `xtr_notifier.testing` asserts on it,
|
|
31
|
+
and holds a provider library's transport and factory to what every provider owes.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
uv add xtr-notifier
|
|
37
|
+
uv add "xtr-notifier[messenger]" # queue messages through xtr-messenger
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A provider's transport reaches it through an HTTP client: install
|
|
41
|
+
[xtr-http-client](../xtr-http-client), or bring any
|
|
42
|
+
[xtr-http-client-contracts](../xtr-http-client-contracts) implementation.
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from xtr_notifier import Notifier, Transport
|
|
48
|
+
from xtr_notifier.channel import ChatChannel, ChannelPolicy, SmsChannel
|
|
49
|
+
from xtr_notifier.notification import Importance, Notification
|
|
50
|
+
from xtr_notifier.recipient import Recipient
|
|
51
|
+
from xtr_notifier.transport import TransportFactoryDiscovery
|
|
52
|
+
|
|
53
|
+
chat = Transport(
|
|
54
|
+
discovery=TransportFactoryDiscovery(TransportFactoryDiscovery.CHATTER_GROUP, http_client=client)
|
|
55
|
+
).from_strings({"ops": "null://null"})
|
|
56
|
+
texts = Transport(
|
|
57
|
+
discovery=TransportFactoryDiscovery(TransportFactoryDiscovery.TEXTER_GROUP, http_client=client)
|
|
58
|
+
).from_strings({"sms": "null://null"})
|
|
59
|
+
|
|
60
|
+
notifier = Notifier(
|
|
61
|
+
{"chat": ChatChannel(chat), "sms": SmsChannel(texts)},
|
|
62
|
+
ChannelPolicy({"urgent": ["sms", "chat"], "high": ["chat"]}),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
await notifier.send(
|
|
66
|
+
Notification("Disk almost full", content="98% used on db-1", importance=Importance.URGENT),
|
|
67
|
+
Recipient(phone="+3312345678"),
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`null://null` sends nowhere, and needs no HTTP client: swap it for a provider's DSN once its
|
|
72
|
+
library is installed.
|
|
73
|
+
|
|
74
|
+
## Notifications
|
|
75
|
+
|
|
76
|
+
`Notification(subject, *, content="", importance=Importance.HIGH, emoji="", channels=(),
|
|
77
|
+
exception=None)` is built in one call and read through properties.
|
|
78
|
+
|
|
79
|
+
- **Channels** — `channels=["chat/slack", "sms"]` names them; the part after `/` is the
|
|
80
|
+
transport. Left empty, the notifier asks its `ChannelPolicy` for the importance's channels.
|
|
81
|
+
- **Importance** — `Importance.URGENT`, `HIGH`, `MEDIUM`, `LOW`, a `StrEnum` whose values key
|
|
82
|
+
the policy. `Notification.importance_from_level("critical")` maps a log level: critical and
|
|
83
|
+
above is urgent, error high, the rest low.
|
|
84
|
+
- **Exceptions** — `Notification.from_exception(error, channels=...)` names the subject after
|
|
85
|
+
the error and keeps its traceback; the emoji follows the importance.
|
|
86
|
+
- **Per recipient** — override `get_channels(recipient)`, or implement
|
|
87
|
+
`as_chat_message(recipient, transport_name)`, `as_sms_message(...)` or
|
|
88
|
+
`as_push_message(...)` to render a channel's message yourself (`ChatNotificationInterface`
|
|
89
|
+
and friends, in `xtr_notifier.notification.<kind>_notification_interface`).
|
|
90
|
+
|
|
91
|
+
Recipients are anything with an `email` or a `phone`; `Recipient(email, phone)` is the plain one,
|
|
92
|
+
and `NoRecipient()` — what `send` uses with no recipient — is a chat room or a topic.
|
|
93
|
+
|
|
94
|
+
## Messages, the chatter and the texter
|
|
95
|
+
|
|
96
|
+
A channel turns a notification into a message: `ChatMessage(subject)`,
|
|
97
|
+
`SmsMessage(phone, subject, from_="")`, `PushMessage(subject, content)`. Messages are frozen;
|
|
98
|
+
`with_transport(...)`, `with_options(...)` and friends return copies. Provider options — blocks,
|
|
99
|
+
buttons, a parse mode — are a frozen `MessageOptions(kind, recipient_id, values)` that a
|
|
100
|
+
provider's builder renders, so a message survives a trip through a bus.
|
|
101
|
+
|
|
102
|
+
When the code has a message rather than a notification, send it directly:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from xtr_notifier import Chatter, Texter
|
|
106
|
+
|
|
107
|
+
await Chatter(chat).send(ChatMessage("Deployed 4.1.0", transport="ops"))
|
|
108
|
+
await Texter(texts).send(SmsMessage("+3312345678", "Your code is 4812"))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Given a message bus, the chatter, the texter and the channels queue instead of sending, and
|
|
112
|
+
return `None`; `xtr_notifier.messenger.MessageHandler(transport)` sends what the bus hands back.
|
|
113
|
+
|
|
114
|
+
## Transports and DSNs
|
|
115
|
+
|
|
116
|
+
`Transport(factories, discovery)` builds transports from DSNs: one DSN gives one transport,
|
|
117
|
+
`a || b` a `FailoverTransport` (sticks to the first that works), `a && b` a
|
|
118
|
+
`RoundRobinTransport` (takes turns). A transport that raises `TransportError` is skipped for 60
|
|
119
|
+
seconds. `from_strings({...})` gives `Transports`, which sends a message through the transport
|
|
120
|
+
it names, or the first that supports it.
|
|
121
|
+
|
|
122
|
+
A DSN is `scheme://user:password@host:port/path?option=value`; `host` is `default` for the
|
|
123
|
+
provider's own API host. A factory serves a scheme: the ones given first, then those installed
|
|
124
|
+
libraries advertise in the group the builder reads — the chatter's
|
|
125
|
+
(`xtr_notifier.chatter_transport_factories`) or the texter's
|
|
126
|
+
(`xtr_notifier.texter_transport_factories`). A scheme nothing serves raises
|
|
127
|
+
`UnsupportedSchemeError`, naming the library to install when it is a known one.
|
|
128
|
+
|
|
129
|
+
## Providers
|
|
130
|
+
|
|
131
|
+
Each provider is a library of its own, installed beside this one; its factory is found by its
|
|
132
|
+
DSN scheme with no wiring. This package ships one transport, `null`, in both groups.
|
|
133
|
+
|
|
134
|
+
| Library | Scheme | Kind | DSN |
|
|
135
|
+
|---|---|---|---|
|
|
136
|
+
| xtr-notifier | `null` | chat, SMS, push | `null://null` |
|
|
137
|
+
| xtr-telegram-notifier | `telegram` | chat | `telegram://BOT_ID:TOKEN@default?channel=CHAT_ID` |
|
|
138
|
+
| xtr-slack-notifier | `slack` | chat | `slack://TOKEN@default?channel=CHANNEL` |
|
|
139
|
+
|
|
140
|
+
Writing one is a package with an `AbstractTransport`, an `AbstractTransportFactory` and an entry
|
|
141
|
+
point; the agent skill's `references/bridges.md` walks through it.
|
|
142
|
+
|
|
143
|
+
## Testing
|
|
144
|
+
|
|
145
|
+
Record what was sent with a `NotificationLoggerListener` (the bundle registers one when the
|
|
146
|
+
event dispatcher is active) and assert on it:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from xtr_notifier.testing import assert_notification_count, assert_notification_subject_contains
|
|
150
|
+
|
|
151
|
+
assert_notification_count(listener.events, 1, "ops")
|
|
152
|
+
assert_notification_subject_contains(listener.events.get_messages()[0], "Disk")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A provider library checks its transport and factory with `TransportCases` /
|
|
156
|
+
`assert_transport_cases` and `FactoryCases` / `assert_factory_cases`, which report every case
|
|
157
|
+
that fails at once. None of it imports a test framework.
|
|
158
|
+
|
|
159
|
+
## Use in an application
|
|
160
|
+
|
|
161
|
+
Everything adding this package to an application on
|
|
162
|
+
[xtr-dependency-injection](../xtr-dependency-injection) takes — and, read backwards, what
|
|
163
|
+
removing it undoes.
|
|
164
|
+
|
|
165
|
+
- **Install** — `uv add "xtr-notifier[messenger]"`, or `uv add xtr-notifier` to send in-process
|
|
166
|
+
only. Each provider is a library of its own (see [Providers](#providers)). It works with
|
|
167
|
+
[xtr-http-client](../xtr-http-client), which providers are reached through,
|
|
168
|
+
[xtr-event-dispatcher](../xtr-event-dispatcher), which announces every message and records
|
|
169
|
+
them for tests, and [xtr-messenger](../xtr-messenger), which queues them — each used when
|
|
170
|
+
installed.
|
|
171
|
+
- **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate*, *Configure* and
|
|
172
|
+
*Environment* steps below: it lists `NotifierBundle`, writes a starting
|
|
173
|
+
`<app>/config/notifier.py` with one chat and one SMS-and-push transport, and
|
|
174
|
+
`NOTIFIER_CHATTER_DSN` and `NOTIFIER_TEXTER_DSN`, both `null://null`, in `.env`. There is no
|
|
175
|
+
ignore line to write. It prints the steps to install a provider and to route the messages to
|
|
176
|
+
a worker, which a recipe cannot make for you.
|
|
177
|
+
- **Activate** — `NotifierBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`, imported
|
|
178
|
+
from `xtr_notifier.bundle`.
|
|
179
|
+
- **Brings along** — the HTTP client, messenger and event dispatcher bundles, each when its
|
|
180
|
+
package is installed.
|
|
181
|
+
- **Configure** — the zero-config notifier has no channel and refuses every notification with
|
|
182
|
+
`The "chat" channel does not exist.`; transports, the channel policy and the administrators go
|
|
183
|
+
in `<app>/config/notifier.py`, a `@configure` function returning `NotifierConfig` — see
|
|
184
|
+
[Kernel / bundle](#kernel--bundle). To queue messages, route `ChatMessage`, `SmsMessage` and
|
|
185
|
+
`PushMessage` in `<app>/config/messenger.py`.
|
|
186
|
+
- **Environment** — `NOTIFIER_CHATTER_DSN` and `NOTIFIER_TEXTER_DSN`, the DSNs the written
|
|
187
|
+
configuration reads when a message is first sent. A provider's DSN carries its token.
|
|
188
|
+
- **Ignore** — nothing.
|
|
189
|
+
- **Remove** — drop the `BUNDLES` entry, delete `<app>/config/notifier.py` and the two variables,
|
|
190
|
+
then `uv remove xtr-notifier`.
|
|
191
|
+
- **Check** — `debug:bundles` shows `notifier` as `listed` and `active`; `debug:config notifier`
|
|
192
|
+
shows the transports, the channel policy and the administrators.
|
|
193
|
+
|
|
194
|
+
## Kernel / bundle
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
# app/config/notifier.py
|
|
198
|
+
from xtr_dependency_injection import configure, env
|
|
199
|
+
from xtr_notifier.bundle import AdminRecipientConfig, NotifierConfig
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@configure
|
|
203
|
+
def notifier() -> NotifierConfig:
|
|
204
|
+
return NotifierConfig(
|
|
205
|
+
chatter_transports={"slack": env("SLACK_DSN"), "ops": "telegram://... || slack://..."},
|
|
206
|
+
texter_transports={"sms": env("SMS_DSN")},
|
|
207
|
+
channel_policy={"urgent": ["sms", "chat/slack"], "high": ["chat/slack"]},
|
|
208
|
+
admin_recipients=[AdminRecipientConfig("ops@example.com", "+3312345678")],
|
|
209
|
+
notification_on_failed_messages=True,
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
| Field | Default | What it does |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| `chatter_transports` | `{}` | Chat transports by name; any brings the `chat` channel and `ChatterInterface` |
|
|
216
|
+
| `texter_transports` | `{}` | SMS and push transports; any brings the `sms` and `push` channels and `TexterInterface` |
|
|
217
|
+
| `channel_policy` | `{}` | Channels by importance value, for a notification naming none |
|
|
218
|
+
| `admin_recipients` | `()` | Who the application's own failures are reported to (`email` required) |
|
|
219
|
+
| `notification_on_failed_messages` | `False` | Report a message the bus gave up on to the administrators |
|
|
220
|
+
| `http_client` | `None` | Qualifier of the HTTP client providers are reached through |
|
|
221
|
+
|
|
222
|
+
What the bundle registers:
|
|
223
|
+
|
|
224
|
+
- `NotifierInterface` / `AdminRecipientsProviderInterface` — always.
|
|
225
|
+
- A `Transport` builder and `Transports` per kind, under the qualifiers `chatter` and `texter`,
|
|
226
|
+
plus the chatter, the texter and the channels — only for a kind with transports. Nothing reads
|
|
227
|
+
a DSN before a message is sent, so a DSN given through `env()` is checked where it is used.
|
|
228
|
+
- With the messenger bundle: a message whose type the bus routes (by class, by its wire name, or
|
|
229
|
+
`*`) is queued, and a handler sends it through the same transports; an unrouted one is sent at
|
|
230
|
+
once. The texter queues only when both SMS and push messages are routed.
|
|
231
|
+
- With the event dispatcher bundle: a `NotificationLoggerListener`, and every message announced.
|
|
232
|
+
- `notification_on_failed_messages=True` without the messenger or the event dispatcher bundle
|
|
233
|
+
fails the build with `LogicError`.
|
|
234
|
+
|
|
235
|
+
Extension points, for an application or another bundle:
|
|
236
|
+
|
|
237
|
+
- Any class deriving from `TransportFactoryInterface` the application registers serves both
|
|
238
|
+
kinds; tag one `CHATTER_TRANSPORT_FACTORY_TAG` or `TEXTER_TRANSPORT_FACTORY_TAG` to serve one.
|
|
239
|
+
A factory keeping `AbstractTransportFactory`'s constructor is handed the HTTP client and the
|
|
240
|
+
event dispatcher; one with a constructor of its own is built by the container.
|
|
241
|
+
- Tag a `ChannelInterface` service `CHANNEL_TAG` with `channel="<name>"` to add a channel.
|
|
242
|
+
|
|
243
|
+
## Errors
|
|
244
|
+
|
|
245
|
+
Every error derives from `NotifierError`.
|
|
246
|
+
|
|
247
|
+
| Error | Raised when |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `InvalidArgumentError` | A DSN cannot be read, an option is not a boolean, a transport name is unknown |
|
|
250
|
+
| `IncompleteDsnError` | A DSN lacks a part its provider needs — a token, a user |
|
|
251
|
+
| `MissingRequiredOptionError` | A DSN lacks a required option |
|
|
252
|
+
| `UnsupportedSchemeError` | No factory serves a DSN's scheme |
|
|
253
|
+
| `LogicError` | A channel does not exist or cannot reach the recipient; no transport supports a message |
|
|
254
|
+
| `UnsupportedMessageTypeError` | A transport is handed a message of another kind |
|
|
255
|
+
| `UnsupportedOptionsError` | A transport is handed another provider's options |
|
|
256
|
+
| `MultipleExclusiveOptionsUsedError` | Options a provider takes only one of were set together |
|
|
257
|
+
| `LengthError` | A value is longer than the provider accepts |
|
|
258
|
+
| `TransportError` | A provider could not be reached or refused a message (carries the response, when one came) |
|
|
259
|
+
| `NotifierRuntimeError` | Every transport of a failover or round robin failed |
|
|
260
|
+
|
|
261
|
+
## Development
|
|
262
|
+
|
|
263
|
+
```sh
|
|
264
|
+
cd packages/xtr-notifier
|
|
265
|
+
uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT — see [LICENSE](LICENSE).
|