django-aiogram 4.0.0.dev0__py3-none-any.whl
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.
- django_aiogram/__init__.py +64 -0
- django_aiogram/_singleton.py +22 -0
- django_aiogram/admin.py +380 -0
- django_aiogram/api.py +38 -0
- django_aiogram/apps.py +56 -0
- django_aiogram/config/__init__.py +15 -0
- django_aiogram/config/checks.py +759 -0
- django_aiogram/config/defaults.py +102 -0
- django_aiogram/config/enums.py +108 -0
- django_aiogram/config/settings.py +266 -0
- django_aiogram/consumer/__init__.py +13 -0
- django_aiogram/consumer/delivery.py +625 -0
- django_aiogram/consumer/routers.py +19 -0
- django_aiogram/consumer/webhook.py +154 -0
- django_aiogram/context.py +34 -0
- django_aiogram/eventlog/__init__.py +16 -0
- django_aiogram/eventlog/dbrouter.py +55 -0
- django_aiogram/eventlog/events.py +120 -0
- django_aiogram/eventlog/instrumentation.py +231 -0
- django_aiogram/eventlog/recorder.py +922 -0
- django_aiogram/eventlog/signals.py +84 -0
- django_aiogram/eventlog/writer.py +231 -0
- django_aiogram/exceptions.py +60 -0
- django_aiogram/healthcheck.py +412 -0
- django_aiogram/management/__init__.py +1 -0
- django_aiogram/management/commands/__init__.py +1 -0
- django_aiogram/management/commands/start_tgbot.py +308 -0
- django_aiogram/management/commands/tgbot_healthcheck.py +57 -0
- django_aiogram/management/commands/tgbot_prune_events.py +144 -0
- django_aiogram/management/commands/tgbot_reclaim.py +135 -0
- django_aiogram/management/commands/tgbot_webhook.py +87 -0
- django_aiogram/migrations/0001_initial.py +50 -0
- django_aiogram/migrations/0002_kind_id_index.py +32 -0
- django_aiogram/migrations/__init__.py +1 -0
- django_aiogram/models.py +79 -0
- django_aiogram/producer/__init__.py +13 -0
- django_aiogram/producer/client.py +1540 -0
- django_aiogram/producer/throttling.py +336 -0
- django_aiogram/py.typed +0 -0
- django_aiogram/redis.py +394 -0
- django_aiogram/wire/__init__.py +14 -0
- django_aiogram/wire/envelope.py +146 -0
- django_aiogram/wire/payloads.py +195 -0
- django_aiogram/wire/serializers.py +533 -0
- django_aiogram-4.0.0.dev0.dist-info/METADATA +145 -0
- django_aiogram-4.0.0.dev0.dist-info/RECORD +48 -0
- django_aiogram-4.0.0.dev0.dist-info/WHEEL +4 -0
- django_aiogram-4.0.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""The seam a project gets metrics out of.
|
|
2
|
+
|
|
3
|
+
Deliberately a ``django.dispatch.Signal`` rather than a setting naming a dotted
|
|
4
|
+
path. A setting would need an entry in ``Settings.md``, a check id, an
|
|
5
|
+
``import_string``, a lazy cache to keep the import off the hot path, and a
|
|
6
|
+
decision about what to do when the path is wrong. A signal needs none of that,
|
|
7
|
+
``send_robust`` contains most of what a receiver can do wrong, and connecting one is
|
|
8
|
+
the thing every Django developer already knows how to do. *Most*: see
|
|
9
|
+
:meth:`~django_aiogram.eventlog.recorder.EventRecorder._publish` for the receiver shape
|
|
10
|
+
Django's own containment misses, which is why this package does not rely on it
|
|
11
|
+
alone.
|
|
12
|
+
|
|
13
|
+
This module imports ``django.dispatch`` and nothing else — not aiogram, not the
|
|
14
|
+
ORM, not the rest of this package. A metrics module can import it at settings
|
|
15
|
+
time without dragging anything in.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from django.dispatch import Signal
|
|
19
|
+
|
|
20
|
+
#: Fired once per batch of recorded events, on whichever thread flushed that batch —
|
|
21
|
+
#: normally the event writer's own, and three other threads can be it.
|
|
22
|
+
#:
|
|
23
|
+
#: Receivers get ``events``: a tuple of :class:`~django_aiogram.eventlog.recorder.Event`,
|
|
24
|
+
#: whose field names are pinned by ``tests/test_public_surface.py`` and are
|
|
25
|
+
#: therefore public API. ``sender`` is the recorder instance.
|
|
26
|
+
#:
|
|
27
|
+
#: Four things about it are load-bearing, and three of them are surprising:
|
|
28
|
+
#:
|
|
29
|
+
#: * **It fires whether or not the event log is on.** The table and the metrics
|
|
30
|
+
#: are separate decisions: connect a receiver and the events flow, with
|
|
31
|
+
#: ``EVENT_LOG`` left off and no migration in sight.
|
|
32
|
+
#: * **Payload summaries are the only part of ``detail`` the log gates.** With the
|
|
33
|
+
#: log off, ``detail`` still carries what the recording seam measured itself —
|
|
34
|
+
#: a send's ``duration_ms``, a retry's ``retry_after``, a queueing failure's
|
|
35
|
+
#: ``stage``, a gap's ``dropped`` count. What is missing is the summarized
|
|
36
|
+
#: arguments, because redacting and bounding a payload is the expensive part of
|
|
37
|
+
#: recording and no part of counting. A receiver that needs message bodies needs
|
|
38
|
+
#: the log on as well, and then ``EVENT_LOG_PAYLOAD`` decides what is in there.
|
|
39
|
+
#: * **``EVENT_LOG_KINDS`` filters this too, with one exemption.** It is one answer
|
|
40
|
+
#: to "which events does this deployment care about", not two — so a receiver
|
|
41
|
+
#: sees exactly the kinds the table would have kept. ``log.dropped`` is the
|
|
42
|
+
#: exception, in both directions: it is the record that recording itself fell
|
|
43
|
+
#: behind, and a deployment that filtered it out would read the hole as quiet
|
|
44
|
+
#: traffic. The table has always been exempt from the filter for that row, and
|
|
45
|
+
#: receivers are exempt with it.
|
|
46
|
+
#: * **The write is attempted before receivers see the batch.** So nothing a
|
|
47
|
+
#: receiver does can change a row that was written, which is why they get real
|
|
48
|
+
#: ``Event`` objects rather than copies — the ``detail`` dict inside one is an
|
|
49
|
+
#: ordinary mutable dict shared with the other receivers, so treat it as
|
|
50
|
+
#: read-only. It is *attempted*, not guaranteed: a write that failed still
|
|
51
|
+
#: publishes, because a database being down is exactly when someone is watching a
|
|
52
|
+
#: dashboard. Receiving a batch is therefore not evidence that a row exists for
|
|
53
|
+
#: it, and with ``EVENT_LOG`` off there is no row by design.
|
|
54
|
+
#:
|
|
55
|
+
#: The rule is **whichever thread flushed the batch publishes it**, and normally that
|
|
56
|
+
#: is the event writer's, once the batch's own write has been attempted — so a slow
|
|
57
|
+
#: receiver delays neither a send nor that write, only later batches and the writer's
|
|
58
|
+
#: shutdown. Never delaying a send is the point of the design; the rest follows from
|
|
59
|
+
#: the rule rather than being promised separately.
|
|
60
|
+
#:
|
|
61
|
+
#: Three other threads can flush a batch, so three other threads can publish one:
|
|
62
|
+
#:
|
|
63
|
+
#: * whatever calls ``EventRecorder.drain_once()``, which exists so a test can drive
|
|
64
|
+
#: the real flush path on its own thread
|
|
65
|
+
#: * at shutdown, whichever thread called ``stop()``, for whatever the writer had not
|
|
66
|
+
#: drained. Those events are published rather than dropped because they are the last
|
|
67
|
+
#: ones before the process goes, and there is by then no writer left to hand them to
|
|
68
|
+
#: * under ``EVENT_LOG_SYNC``, the thread that recorded the event, which is the one
|
|
69
|
+
#: case with no batch and no writer involved at all
|
|
70
|
+
#:
|
|
71
|
+
#: ``EVENT_LOG_SYNC`` only takes effect with the log on — there is nothing to insert
|
|
72
|
+
#: synchronously otherwise — so the write is always attempted there, and may still
|
|
73
|
+
#: fail. It is a testing setting, and receivers running inside the send path is one
|
|
74
|
+
#: more reason to keep it one.
|
|
75
|
+
#:
|
|
76
|
+
#: A dispatch that fails can leave later receivers without the batch: ``send_robust``
|
|
77
|
+
#: stops its own loop when Django's failure logging raises, which it does for a
|
|
78
|
+
#: callable instance. This package catches that and logs
|
|
79
|
+
#: ``publishing recorded events failed``, but the receivers after the offending one
|
|
80
|
+
#: were never called.
|
|
81
|
+
#:
|
|
82
|
+
#: The write happens before either, and only when ``EVENT_LOG`` is on: with the log
|
|
83
|
+
#: off nothing is written at all, and a receiver is the only thing the batch reaches.
|
|
84
|
+
events_recorded: Signal = Signal()
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Turning recorded events into rows, on the one thread allowed to do it.
|
|
2
|
+
|
|
3
|
+
This is the only module in the package that touches the ORM, and it is imported
|
|
4
|
+
from inside the writer thread rather than at module scope — that is what keeps
|
|
5
|
+
``recorder`` free of ``django.db``, and therefore keeps ``record()`` legal from
|
|
6
|
+
a coroutine and free in a process with the log switched off.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import datetime
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
|
|
13
|
+
from django.conf import settings as django_settings
|
|
14
|
+
from django.db import (
|
|
15
|
+
DEFAULT_DB_ALIAS,
|
|
16
|
+
DatabaseError,
|
|
17
|
+
InterfaceError,
|
|
18
|
+
OperationalError,
|
|
19
|
+
connections,
|
|
20
|
+
transaction,
|
|
21
|
+
)
|
|
22
|
+
from django.utils import timezone
|
|
23
|
+
|
|
24
|
+
from django_aiogram.eventlog.dbrouter import event_log_database
|
|
25
|
+
from django_aiogram.eventlog.recorder import Event
|
|
26
|
+
from django_aiogram.exceptions import DjangoRedisAiogramError
|
|
27
|
+
from django_aiogram.models import TelegramEvent
|
|
28
|
+
from django_aiogram.wire.payloads import redact_keys, redact_text, redact_values, secrets
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger('django_aiogram')
|
|
31
|
+
|
|
32
|
+
#: below this a failed batch is retried row by row rather than bisected
|
|
33
|
+
ROW_BY_ROW = 32
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EventLogRefusedError(DjangoRedisAiogramError):
|
|
37
|
+
"""Every row of a batch was refused.
|
|
38
|
+
|
|
39
|
+
Not a `DatabaseError`: it is this module's verdict on one, and the difference
|
|
40
|
+
matters because the recorder treats it as a failed flush rather than as
|
|
41
|
+
something to bisect further. It never escapes that flush.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, count: int) -> None:
|
|
45
|
+
"""Name how many rows were lost, which is what the log line reports."""
|
|
46
|
+
super().__init__(f'the database refused all {count} rows of this batch')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def log_alias() -> str:
|
|
50
|
+
"""Return the alias rows are written to and read from."""
|
|
51
|
+
return event_log_database() or DEFAULT_DB_ALIAS
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _moment(stamp: float) -> datetime.datetime:
|
|
55
|
+
"""Return the recorded instant, in whichever flavour of datetime this project stores."""
|
|
56
|
+
moment = datetime.datetime.fromtimestamp(stamp, tz=datetime.timezone.utc)
|
|
57
|
+
return moment if django_settings.USE_TZ else timezone.make_naive(moment)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _text(value: object, length: int) -> str:
|
|
61
|
+
"""Cut a value to its column's width, without the NULs PostgreSQL refuses."""
|
|
62
|
+
if value is None:
|
|
63
|
+
return ''
|
|
64
|
+
return str(value).replace('\x00', '')[:length]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def to_row(
|
|
68
|
+
event: Event,
|
|
69
|
+
keys: frozenset[str] | None = None,
|
|
70
|
+
configured: tuple[str, ...] | None = None,
|
|
71
|
+
) -> TelegramEvent:
|
|
72
|
+
"""Build the unsaved row for one event, sanitised so it cannot poison a batch.
|
|
73
|
+
|
|
74
|
+
Redaction happens here as well as at the producer. This is the boundary rows
|
|
75
|
+
cross, and the rule is that the token must not reach one: a caller that
|
|
76
|
+
builds an Event by hand, or a new seam that forgets, would otherwise put an
|
|
77
|
+
aiogram error message — which carries the API URL, which carries the token —
|
|
78
|
+
straight into a column.
|
|
79
|
+
"""
|
|
80
|
+
if keys is None:
|
|
81
|
+
keys = redact_keys()
|
|
82
|
+
if configured is None:
|
|
83
|
+
configured = secrets()
|
|
84
|
+
return TelegramEvent(
|
|
85
|
+
created_at=_moment(event.created_at),
|
|
86
|
+
correlation_id=event.correlation_id,
|
|
87
|
+
kind=_text(event.kind, 48),
|
|
88
|
+
function=_text(event.function, 64),
|
|
89
|
+
chat_id=event.chat_id,
|
|
90
|
+
user_id=event.user_id,
|
|
91
|
+
message_id=event.message_id,
|
|
92
|
+
update_id=event.update_id,
|
|
93
|
+
worker=_text(event.worker, 128),
|
|
94
|
+
attempt=max(0, event.attempt),
|
|
95
|
+
duration_ms=event.duration_ms,
|
|
96
|
+
error_code=_text(event.error_code, 64),
|
|
97
|
+
error=_text(redact_text(str(event.error or ''), configured), 20000),
|
|
98
|
+
detail=redact_values(event.detail or {}, keys, configured),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_batch(events: Sequence[Event]) -> int:
|
|
103
|
+
"""Insert one batch, recycling a connection the database has since dropped.
|
|
104
|
+
|
|
105
|
+
Returns how many rows did **not** land, so the caller can count them. A total
|
|
106
|
+
refusal raises; a partial one used to return here indistinguishable from a clean
|
|
107
|
+
write, so the rows the database refused one at a time disappeared with no counter,
|
|
108
|
+
no ``log.dropped`` row and nothing but a per-row line in the log — while the feed
|
|
109
|
+
read as complete coverage of the period that lost them.
|
|
110
|
+
"""
|
|
111
|
+
alias = log_alias()
|
|
112
|
+
_recycle(alias)
|
|
113
|
+
# resolved once for the batch: both walk the settings, and a batch is 200 rows
|
|
114
|
+
keys, configured = redact_keys(), secrets()
|
|
115
|
+
rows = [to_row(event, keys, configured) for event in events]
|
|
116
|
+
manager = TelegramEvent.objects.using(alias)
|
|
117
|
+
try:
|
|
118
|
+
# the savepoint is what keeps a failed log write from taking the
|
|
119
|
+
# caller's data with it: under EVENT_LOG_SYNC this runs on the caller's
|
|
120
|
+
# thread, inside whatever atomic() block the caller opened
|
|
121
|
+
with transaction.atomic(using=alias):
|
|
122
|
+
manager.bulk_create(rows)
|
|
123
|
+
except (OperationalError, InterfaceError) as error:
|
|
124
|
+
# the connection died between the check above and the insert; one retry
|
|
125
|
+
# on a fresh one is the difference between losing a batch and not
|
|
126
|
+
_recycle(alias, force=True)
|
|
127
|
+
# the retry needs the same net as the first attempt: a fresh connection
|
|
128
|
+
# rejecting one poison row must not cost the whole batch
|
|
129
|
+
written = _write_half(rows, alias)
|
|
130
|
+
_refused(rows, written, error)
|
|
131
|
+
return len(rows) - written
|
|
132
|
+
except DatabaseError as error:
|
|
133
|
+
written = _write_one_by_one(rows, alias)
|
|
134
|
+
_refused(rows, written, error)
|
|
135
|
+
return len(rows) - written
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _recycle(alias: str, *, force: bool = False) -> None:
|
|
140
|
+
"""Discard a connection the database has since dropped, unless we are inside a transaction.
|
|
141
|
+
|
|
142
|
+
Before the work, not after: this is what discards a connection whose
|
|
143
|
+
CONN_MAX_AGE expired, that a restart killed, or that a previous error marked
|
|
144
|
+
unusable. Closing afterward would leave a broken one in place.
|
|
145
|
+
|
|
146
|
+
Never while the caller holds a transaction open, though. Under EVENT_LOG_SYNC
|
|
147
|
+
this runs on the caller's thread inside their ``atomic()`` block — and on
|
|
148
|
+
PostgreSQL and MySQL, closing a connection there marks the whole transaction
|
|
149
|
+
for rollback, so recording an event would destroy the writes the caller made
|
|
150
|
+
alongside it. A connection Django is already using is not stale anyway.
|
|
151
|
+
|
|
152
|
+
``in_atomic_block`` is only half of what *open transaction* means. With autocommit
|
|
153
|
+
off — ``transaction.set_autocommit(False)``, or ``AUTOCOMMIT: False`` on the alias —
|
|
154
|
+
the server holds one from the first statement with no block anywhere in sight, and
|
|
155
|
+
then it is worse than the block case: ``close_if_unusable_or_obsolete`` sees
|
|
156
|
+
``get_autocommit()`` disagreeing with the configured value and closes on that basis,
|
|
157
|
+
while ``close()`` skips setting ``needs_rollback`` precisely *because*
|
|
158
|
+
``in_atomic_block`` is False. So the caller's writes are rolled back by the server
|
|
159
|
+
and nothing raises. Measured on PostgreSQL 16: the caller's row was gone after a
|
|
160
|
+
successful ``commit()``, with the event row written.
|
|
161
|
+
|
|
162
|
+
One alias, not all of them. ``close_old_connections()`` walks every initialized
|
|
163
|
+
connection, so with ``EVENT_LOG_DATABASE`` pointing somewhere of its own it
|
|
164
|
+
reaches past the log's connection — which is not in a transaction — and closes
|
|
165
|
+
the caller's ``default`` one, which is. The guard above would then be reading
|
|
166
|
+
the wrong connection's state. The log has no business touching one it never
|
|
167
|
+
writes to.
|
|
168
|
+
"""
|
|
169
|
+
connection = connections[alias]
|
|
170
|
+
if connection.in_atomic_block or not connection.get_autocommit():
|
|
171
|
+
return
|
|
172
|
+
if force:
|
|
173
|
+
connection.close()
|
|
174
|
+
return
|
|
175
|
+
connection.close_if_unusable_or_obsolete()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _refused(rows: list[TelegramEvent], written: int, error: Exception) -> None:
|
|
179
|
+
"""Raise when a batch reached the database and none of it landed.
|
|
180
|
+
|
|
181
|
+
The bisecting ladder below catches ``DatabaseError`` at every rung, so a
|
|
182
|
+
database that refuses everything — no table, no permission, no disk — used to
|
|
183
|
+
end in a normal return. The recorder read that as success: its failure counter
|
|
184
|
+
never moved, so the backoff never engaged and no ``log.dropped`` row was ever
|
|
185
|
+
written. It hammered the database once per flush interval, for ever.
|
|
186
|
+
"""
|
|
187
|
+
if rows and not written:
|
|
188
|
+
raise EventLogRefusedError(len(rows)) from error
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _write_half(rows: list[TelegramEvent], alias: str) -> int:
|
|
192
|
+
"""Insert one half of a bisected batch, splitting it again if it still fails."""
|
|
193
|
+
try:
|
|
194
|
+
with transaction.atomic(using=alias):
|
|
195
|
+
TelegramEvent.objects.using(alias).bulk_create(rows)
|
|
196
|
+
except (DatabaseError, InterfaceError):
|
|
197
|
+
# both, because Django defines them as siblings under `Error`: a connection the
|
|
198
|
+
# server dropped raises `InterfaceError`, which `DatabaseError` alone lets escape
|
|
199
|
+
# `write_batch` — with no refused count and no `EventLogRefusedError`
|
|
200
|
+
return _write_one_by_one(rows, alias)
|
|
201
|
+
return len(rows)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _write_row(row: TelegramEvent, alias: str) -> bool:
|
|
205
|
+
"""Insert one row, dropping it if the database refuses it."""
|
|
206
|
+
try:
|
|
207
|
+
# the savepoint is not optional: on PostgreSQL a failed statement aborts
|
|
208
|
+
# the transaction, so one bad row would take every later one with it
|
|
209
|
+
with transaction.atomic(using=alias):
|
|
210
|
+
row.save(force_insert=True, using=alias)
|
|
211
|
+
except (DatabaseError, InterfaceError):
|
|
212
|
+
logger.exception('dropping an event the database refused', extra={'tg_kind': row.kind})
|
|
213
|
+
return False
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _write_one_by_one(rows: list[TelegramEvent], alias: str) -> int:
|
|
218
|
+
"""Save rows individually, dropping only the ones the database refuses."""
|
|
219
|
+
if len(rows) > ROW_BY_ROW:
|
|
220
|
+
# bisect first, so a 200-row batch does not become 200 statements
|
|
221
|
+
middle = len(rows) // 2
|
|
222
|
+
return _write_half(rows[:middle], alias) + _write_half(rows[middle:], alias)
|
|
223
|
+
return sum(_write_row(row, alias) for row in rows)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def close_connections() -> None:
|
|
227
|
+
"""Release the calling thread's connections; nothing else ever will."""
|
|
228
|
+
try:
|
|
229
|
+
connections.close_all()
|
|
230
|
+
except Exception:
|
|
231
|
+
logger.exception('could not close the event writer connection')
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""The exceptions this package raises.
|
|
2
|
+
|
|
3
|
+
They live together, and build their own messages, so that call sites raise a
|
|
4
|
+
named domain error instead of a bare builtin with a formatted string.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DjangoRedisAiogramError(Exception):
|
|
9
|
+
"""Base class for every error this package raises."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LoopUnavailableError(DjangoRedisAiogramError, RuntimeError):
|
|
13
|
+
"""No event loop in this process can take an update right now.
|
|
14
|
+
|
|
15
|
+
A refusal, not a handler failure: nothing has run, so the update is still
|
|
16
|
+
Telegram's to redeliver. The webhook view answers a non-2xx for this and 200
|
|
17
|
+
for everything else, which is the difference between a retry and a loop.
|
|
18
|
+
|
|
19
|
+
Raised as one of the two below, so a caller can tell a shutdown from a fault.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ShuttingDownError(LoopUnavailableError):
|
|
24
|
+
"""The update arrived while this process was tearing the bot down."""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Say which window this is, since it closes on its own."""
|
|
28
|
+
super().__init__(
|
|
29
|
+
'the bot is shutting down, so this update was not handled here. '
|
|
30
|
+
'Telegram will redeliver it; another process, or this one after it '
|
|
31
|
+
'restarts, will take it.',
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LoopThreadNotStartedError(LoopUnavailableError):
|
|
36
|
+
"""The thread this process gives the loop did not start in time."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, timeout: float) -> None:
|
|
39
|
+
"""Name the deadline, which is the only number worth acting on."""
|
|
40
|
+
super().__init__(
|
|
41
|
+
f'the event loop thread did not start within {timeout}s, and driving '
|
|
42
|
+
'the loop from this thread instead would put two threads on one loop. '
|
|
43
|
+
'The update was not handled here.',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SerializationError(DjangoRedisAiogramError):
|
|
48
|
+
"""A payload could not be encoded or decoded."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UnknownApiMethodError(DjangoRedisAiogramError, ValueError):
|
|
52
|
+
"""A queued payload named something that is not a Telegram API method."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, function: str, method_count: int) -> None:
|
|
55
|
+
"""Name the rejected method and how many the Bot API actually has."""
|
|
56
|
+
super().__init__(
|
|
57
|
+
f'{function!r} is not a Telegram API method. Queued payloads may only '
|
|
58
|
+
f'name one of the {method_count} methods aiogram exposes for the '
|
|
59
|
+
f'Bot API; see the Serialization page.',
|
|
60
|
+
)
|