django-aiogram 4.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.
- django_aiogram-4.0.0/.gitignore +26 -0
- django_aiogram-4.0.0/AGENTS.md +300 -0
- django_aiogram-4.0.0/CHANGELOG.md +2058 -0
- django_aiogram-4.0.0/CONTRIBUTING.md +206 -0
- django_aiogram-4.0.0/LICENSE +21 -0
- django_aiogram-4.0.0/PKG-INFO +193 -0
- django_aiogram-4.0.0/README.md +148 -0
- django_aiogram-4.0.0/SECURITY.md +96 -0
- django_aiogram-4.0.0/docs/wiki/AI-assistants.md +139 -0
- django_aiogram-4.0.0/docs/wiki/API.md +376 -0
- django_aiogram-4.0.0/docs/wiki/Delivery.md +292 -0
- django_aiogram-4.0.0/docs/wiki/Deployment.md +533 -0
- django_aiogram-4.0.0/docs/wiki/Event-log.md +535 -0
- django_aiogram-4.0.0/docs/wiki/Handlers.md +116 -0
- django_aiogram-4.0.0/docs/wiki/Home.md +52 -0
- django_aiogram-4.0.0/docs/wiki/Installation.md +156 -0
- django_aiogram-4.0.0/docs/wiki/Kafka.md +156 -0
- django_aiogram-4.0.0/docs/wiki/Logging.md +133 -0
- django_aiogram-4.0.0/docs/wiki/RabbitMQ.md +104 -0
- django_aiogram-4.0.0/docs/wiki/Rate-limits.md +90 -0
- django_aiogram-4.0.0/docs/wiki/Redis-Streams.md +88 -0
- django_aiogram-4.0.0/docs/wiki/Redis-list.md +103 -0
- django_aiogram-4.0.0/docs/wiki/Sending-messages.md +175 -0
- django_aiogram-4.0.0/docs/wiki/Serialization.md +222 -0
- django_aiogram-4.0.0/docs/wiki/Settings.md +321 -0
- django_aiogram-4.0.0/docs/wiki/Testing.md +269 -0
- django_aiogram-4.0.0/docs/wiki/Troubleshooting.md +331 -0
- django_aiogram-4.0.0/docs/wiki/Upgrading.md +662 -0
- django_aiogram-4.0.0/docs/wiki/Webhook.md +196 -0
- django_aiogram-4.0.0/docs/wiki/_Sidebar.md +29 -0
- django_aiogram-4.0.0/pyproject.toml +251 -0
- django_aiogram-4.0.0/scripts/measurements/README.md +166 -0
- django_aiogram-4.0.0/scripts/measurements/__init__.py +11 -0
- django_aiogram-4.0.0/scripts/measurements/_timing.py +80 -0
- django_aiogram-4.0.0/scripts/measurements/amqp_driver_choice.py +403 -0
- django_aiogram-4.0.0/scripts/measurements/constraints.txt +12 -0
- django_aiogram-4.0.0/scripts/measurements/kafka_driver_choice.py +251 -0
- django_aiogram-4.0.0/scripts/measurements/redis_baseline.py +62 -0
- django_aiogram-4.0.0/scripts/smoke_install.sh +307 -0
- django_aiogram-4.0.0/src/django_aiogram/__init__.py +60 -0
- django_aiogram-4.0.0/src/django_aiogram/_singleton.py +22 -0
- django_aiogram-4.0.0/src/django_aiogram/admin.py +410 -0
- django_aiogram-4.0.0/src/django_aiogram/api.py +38 -0
- django_aiogram-4.0.0/src/django_aiogram/apps.py +56 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/__init__.py +10 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/base.py +390 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/exceptions.py +75 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/kafka/__init__.py +6 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/kafka/broker.py +660 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/kafka/client.py +281 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/kafka/exceptions.py +38 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/models.py +31 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/rabbitmq/__init__.py +6 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/rabbitmq/broker.py +350 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/rabbitmq/client.py +198 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/rabbitmq/exceptions.py +37 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/redis_list/__init__.py +6 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/redis_list/broker.py +263 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/redis_streams/__init__.py +6 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/redis_streams/broker.py +694 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/redis_streams/exceptions.py +68 -0
- django_aiogram-4.0.0/src/django_aiogram/broker/registry.py +146 -0
- django_aiogram-4.0.0/src/django_aiogram/config/__init__.py +15 -0
- django_aiogram-4.0.0/src/django_aiogram/config/checks/__init__.py +179 -0
- django_aiogram-4.0.0/src/django_aiogram/config/checks/bot.py +216 -0
- django_aiogram-4.0.0/src/django_aiogram/config/checks/conditions.py +98 -0
- django_aiogram-4.0.0/src/django_aiogram/config/checks/eventlog.py +320 -0
- django_aiogram-4.0.0/src/django_aiogram/config/checks/problems.py +90 -0
- django_aiogram-4.0.0/src/django_aiogram/config/checks/shapes.py +202 -0
- django_aiogram-4.0.0/src/django_aiogram/config/checks/transport.py +455 -0
- django_aiogram-4.0.0/src/django_aiogram/config/defaults.py +109 -0
- django_aiogram-4.0.0/src/django_aiogram/config/enums.py +132 -0
- django_aiogram-4.0.0/src/django_aiogram/config/settings.py +286 -0
- django_aiogram-4.0.0/src/django_aiogram/consumer/__init__.py +13 -0
- django_aiogram-4.0.0/src/django_aiogram/consumer/delivery.py +653 -0
- django_aiogram-4.0.0/src/django_aiogram/consumer/routers.py +19 -0
- django_aiogram-4.0.0/src/django_aiogram/consumer/webhook.py +161 -0
- django_aiogram-4.0.0/src/django_aiogram/context.py +34 -0
- django_aiogram-4.0.0/src/django_aiogram/db.py +87 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/__init__.py +20 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/bookkeeping.py +159 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/dbrouter.py +55 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/events.py +185 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/instrumentation.py +233 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/moving.py +97 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/pacing.py +67 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/publishing.py +97 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/recorder.py +689 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/records.py +68 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/signals.py +84 -0
- django_aiogram-4.0.0/src/django_aiogram/eventlog/writer.py +235 -0
- django_aiogram-4.0.0/src/django_aiogram/exceptions.py +97 -0
- django_aiogram-4.0.0/src/django_aiogram/healthcheck.py +599 -0
- django_aiogram-4.0.0/src/django_aiogram/management/__init__.py +1 -0
- django_aiogram-4.0.0/src/django_aiogram/management/commands/__init__.py +1 -0
- django_aiogram-4.0.0/src/django_aiogram/management/commands/start_tgbot.py +321 -0
- django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_backfill_short_ids.py +97 -0
- django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_healthcheck.py +65 -0
- django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_move_events.py +299 -0
- django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_prune_events.py +144 -0
- django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_reclaim.py +161 -0
- django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_webhook.py +87 -0
- django_aiogram-4.0.0/src/django_aiogram/migrations/0001_initial.py +50 -0
- django_aiogram-4.0.0/src/django_aiogram/migrations/0002_kind_id_index.py +32 -0
- django_aiogram-4.0.0/src/django_aiogram/migrations/0003_short_id.py +34 -0
- django_aiogram-4.0.0/src/django_aiogram/migrations/__init__.py +1 -0
- django_aiogram-4.0.0/src/django_aiogram/models.py +85 -0
- django_aiogram-4.0.0/src/django_aiogram/producer/__init__.py +13 -0
- django_aiogram-4.0.0/src/django_aiogram/producer/client.py +1215 -0
- django_aiogram-4.0.0/src/django_aiogram/producer/from_settings.py +88 -0
- django_aiogram-4.0.0/src/django_aiogram/producer/looping.py +110 -0
- django_aiogram-4.0.0/src/django_aiogram/producer/outbound.py +115 -0
- django_aiogram-4.0.0/src/django_aiogram/producer/queueing.py +152 -0
- django_aiogram-4.0.0/src/django_aiogram/producer/routing.py +104 -0
- django_aiogram-4.0.0/src/django_aiogram/producer/throttling.py +336 -0
- django_aiogram-4.0.0/src/django_aiogram/py.typed +0 -0
- django_aiogram-4.0.0/src/django_aiogram/redis.py +464 -0
- django_aiogram-4.0.0/src/django_aiogram/wire/__init__.py +14 -0
- django_aiogram-4.0.0/src/django_aiogram/wire/envelope.py +156 -0
- django_aiogram-4.0.0/src/django_aiogram/wire/payloads.py +205 -0
- django_aiogram-4.0.0/src/django_aiogram/wire/serializers.py +539 -0
- django_aiogram-4.0.0/tests/__init__.py +0 -0
- django_aiogram-4.0.0/tests/bare_settings.py +18 -0
- django_aiogram-4.0.0/tests/conftest.py +79 -0
- django_aiogram-4.0.0/tests/db/__init__.py +0 -0
- django_aiogram-4.0.0/tests/db/conftest.py +172 -0
- django_aiogram-4.0.0/tests/db/test_admin.py +641 -0
- django_aiogram-4.0.0/tests/db/test_connections.py +143 -0
- django_aiogram-4.0.0/tests/db/test_event_log_model.py +163 -0
- django_aiogram-4.0.0/tests/db/test_harness.py +73 -0
- django_aiogram-4.0.0/tests/db/test_inbound.py +301 -0
- django_aiogram-4.0.0/tests/db/test_move_events.py +544 -0
- django_aiogram-4.0.0/tests/db/test_outbound.py +379 -0
- django_aiogram-4.0.0/tests/db/test_prune.py +270 -0
- django_aiogram-4.0.0/tests/db/test_recorder.py +1078 -0
- django_aiogram-4.0.0/tests/db/test_redaction.py +74 -0
- django_aiogram-4.0.0/tests/db/test_short_id.py +223 -0
- django_aiogram-4.0.0/tests/db_settings.py +56 -0
- django_aiogram-4.0.0/tests/db_urls.py +6 -0
- django_aiogram-4.0.0/tests/fake_app/__init__.py +0 -0
- django_aiogram-4.0.0/tests/fake_app/broken_router.py +7 -0
- django_aiogram-4.0.0/tests/fake_app/tg_router.py +17 -0
- django_aiogram-4.0.0/tests/integration/__init__.py +0 -0
- django_aiogram-4.0.0/tests/integration/conftest.py +246 -0
- django_aiogram-4.0.0/tests/integration/test_async_producer_against_redis.py +166 -0
- django_aiogram-4.0.0/tests/integration/test_delivery_against_redis.py +338 -0
- django_aiogram-4.0.0/tests/integration/test_fsm_across_restart.py +94 -0
- django_aiogram-4.0.0/tests/integration/test_kafka_against_broker.py +559 -0
- django_aiogram-4.0.0/tests/integration/test_rabbitmq_against_broker.py +601 -0
- django_aiogram-4.0.0/tests/integration/test_streams_against_redis.py +210 -0
- django_aiogram-4.0.0/tests/marker_app/__init__.py +6 -0
- django_aiogram-4.0.0/tests/marker_app/apps.py +26 -0
- django_aiogram-4.0.0/tests/marker_settings.py +13 -0
- django_aiogram-4.0.0/tests/postgres_settings.py +56 -0
- django_aiogram-4.0.0/tests/settings.py +8 -0
- django_aiogram-4.0.0/tests/support.py +41 -0
- django_aiogram-4.0.0/tests/test_async_producer.py +707 -0
- django_aiogram-4.0.0/tests/test_autodiscover.py +72 -0
- django_aiogram-4.0.0/tests/test_bot_properties.py +187 -0
- django_aiogram-4.0.0/tests/test_broker_conformance.py +522 -0
- django_aiogram-4.0.0/tests/test_broker_lifecycle.py +207 -0
- django_aiogram-4.0.0/tests/test_broker_neutral_prose.py +171 -0
- django_aiogram-4.0.0/tests/test_checks.py +2006 -0
- django_aiogram-4.0.0/tests/test_conf_edge_cases.py +239 -0
- django_aiogram-4.0.0/tests/test_crash_safety.py +1162 -0
- django_aiogram-4.0.0/tests/test_db_suite_guard.py +34 -0
- django_aiogram-4.0.0/tests/test_delivery.py +788 -0
- django_aiogram-4.0.0/tests/test_dependency_floors.py +180 -0
- django_aiogram-4.0.0/tests/test_docs_examples.py +139 -0
- django_aiogram-4.0.0/tests/test_docstring_coverage.py +252 -0
- django_aiogram-4.0.0/tests/test_docstring_references.py +190 -0
- django_aiogram-4.0.0/tests/test_documented_recipes.py +708 -0
- django_aiogram-4.0.0/tests/test_dotted_paths.py +45 -0
- django_aiogram-4.0.0/tests/test_enabled_flag.py +215 -0
- django_aiogram-4.0.0/tests/test_envelope.py +176 -0
- django_aiogram-4.0.0/tests/test_event_log_off.py +236 -0
- django_aiogram-4.0.0/tests/test_healthcheck.py +1232 -0
- django_aiogram-4.0.0/tests/test_inflight_by_worker.py +146 -0
- django_aiogram-4.0.0/tests/test_kafka_publish.py +403 -0
- django_aiogram-4.0.0/tests/test_lazy_init.py +471 -0
- django_aiogram-4.0.0/tests/test_logging_discipline.py +416 -0
- django_aiogram-4.0.0/tests/test_measurements.py +282 -0
- django_aiogram-4.0.0/tests/test_metrics_seam.py +968 -0
- django_aiogram-4.0.0/tests/test_package_layout.py +313 -0
- django_aiogram-4.0.0/tests/test_payloads.py +363 -0
- django_aiogram-4.0.0/tests/test_public_surface.py +437 -0
- django_aiogram-4.0.0/tests/test_rabbitmq_channel.py +68 -0
- django_aiogram-4.0.0/tests/test_redis_helpers.py +427 -0
- django_aiogram-4.0.0/tests/test_refusals.py +216 -0
- django_aiogram-4.0.0/tests/test_serializers.py +373 -0
- django_aiogram-4.0.0/tests/test_shutdown.py +634 -0
- django_aiogram-4.0.0/tests/test_start_command.py +522 -0
- django_aiogram-4.0.0/tests/test_streams_recovery.py +251 -0
- django_aiogram-4.0.0/tests/test_suite_discipline.py +197 -0
- django_aiogram-4.0.0/tests/test_throttling.py +522 -0
- django_aiogram-4.0.0/tests/test_transport_pages.py +169 -0
- django_aiogram-4.0.0/tests/test_unified_send.py +223 -0
- django_aiogram-4.0.0/tests/test_webhook.py +1208 -0
- django_aiogram-4.0.0/tests/test_wiki.py +441 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.idea/
|
|
2
|
+
.vscode/
|
|
3
|
+
venv/
|
|
4
|
+
.venv/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
|
|
7
|
+
# Python cache.
|
|
8
|
+
*.py[cod]
|
|
9
|
+
__pycache__/
|
|
10
|
+
|
|
11
|
+
# build
|
|
12
|
+
build/
|
|
13
|
+
dist/
|
|
14
|
+
|
|
15
|
+
# tooling caches
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
|
|
22
|
+
# JetBrains AI plugin checkpoints: 45 MB of local cache, not source
|
|
23
|
+
.proxyai/
|
|
24
|
+
|
|
25
|
+
# macOS
|
|
26
|
+
.DS_Store
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Instructions for coding agents working on **this repository**. For agents
|
|
4
|
+
integrating the package into a project, see the wiki page
|
|
5
|
+
[AI assistants](https://github.com/CorneiZeR/django-aiogram/wiki/AI-assistants).
|
|
6
|
+
|
|
7
|
+
## What this is
|
|
8
|
+
|
|
9
|
+
A Django app that runs aiogram in a neighbouring container and queues Telegram
|
|
10
|
+
messages through Redis. The Django processes never poll; they push a payload
|
|
11
|
+
onto a Redis list, and the bot container consumes it.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
src/django_aiogram/
|
|
15
|
+
__init__.py lazy exports: bot, conf, redis_conn, get_redis, __version__
|
|
16
|
+
apps.py AppConfig.ready(): checks and autodiscover, both behind ENABLED
|
|
17
|
+
models.py TelegramEvent, the append-only feed; migrations/ beside it
|
|
18
|
+
admin.py the read-only changelist; registered from ready(), not on import
|
|
19
|
+
healthcheck.py the container probe; must import nothing needing the app registry
|
|
20
|
+
api.py the allowlist of Telegram API method names a payload may use
|
|
21
|
+
exceptions.py one error family for the whole package
|
|
22
|
+
context.py the correlation id a handler's replies inherit
|
|
23
|
+
_singleton.py once-per-process construction, under the import lock
|
|
24
|
+
redis.py lazy connection
|
|
25
|
+
config/
|
|
26
|
+
settings.py lazy settings with an environment fallback
|
|
27
|
+
defaults.py the only place a default lives
|
|
28
|
+
enums.py the values a setting accepts
|
|
29
|
+
checks/ system checks E001-E048, W001-W009, I001-I003:
|
|
30
|
+
the registry in __init__, rules beside their subject
|
|
31
|
+
(bot, transport, eventlog), shapes for what a value
|
|
32
|
+
must look like, conditions for what a rule asks first
|
|
33
|
+
broker/ one transport per package, and the contract they answer
|
|
34
|
+
producer/
|
|
35
|
+
client.py TelegramBot: bot/dispatcher/loop, send, send_raw, shutdown
|
|
36
|
+
outbound.py what names one send in flight, and what settles it
|
|
37
|
+
looping.py who may drive the loop, and for how long
|
|
38
|
+
queueing.py everything a queue write does except the write
|
|
39
|
+
from_settings.py the aiogram objects the settings describe, and their refusals
|
|
40
|
+
routing.py the handler decorators, which read the router and nothing else
|
|
41
|
+
throttling.py GCRA reservations, one budget per name
|
|
42
|
+
consumer/
|
|
43
|
+
delivery.py BlpopDelivery, the one consumer
|
|
44
|
+
webhook.py the view an update arrives at
|
|
45
|
+
routers.py autodiscover
|
|
46
|
+
wire/
|
|
47
|
+
serializers.py tagged JSON, and pickle behind ALLOW_PICKLE
|
|
48
|
+
envelope.py what a queued payload looks like, both shapes
|
|
49
|
+
payloads.py summarize, redact, cap — in that order, and never lossless
|
|
50
|
+
eventlog/
|
|
51
|
+
recorder.py the bounded queue and the writer thread; no django.db here
|
|
52
|
+
records.py Event and Wake: the shapes that cross the queue
|
|
53
|
+
pacing.py the writer's numbers, and the promise that reading one never raises
|
|
54
|
+
bookkeeping.py the drop ledger and the thread marks, one lock each
|
|
55
|
+
publishing.py the fan-out to events_recorded, which cannot raise
|
|
56
|
+
writer.py the only module that touches the ORM
|
|
57
|
+
events.py the event-kind registry and the correlation id
|
|
58
|
+
instrumentation.py the update middleware and the storage wrapper
|
|
59
|
+
signals.py events_recorded, the metrics seam; imports only django.dispatch
|
|
60
|
+
moving.py what 3.x's table was, and what the two tables share
|
|
61
|
+
dbrouter.py optional routing of the log to its own database
|
|
62
|
+
docs/wiki/ the wiki, published from main
|
|
63
|
+
tests/ pytest, fakeredis, no network
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
python -m pip install --upgrade pip # --group is PEP 735, pip 25.1 and up
|
|
70
|
+
pip install -e '.[redis]' --group dev
|
|
71
|
+
ruff check . && ruff format --check . && mypy && python -m pytest -q
|
|
72
|
+
python -m pytest -q --ds=tests.db_settings tests/db
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The extra is named on purpose — the driver is an extra since 4.0, and CI names it in
|
|
76
|
+
every job for the same reason. **Name only the extras that job needs, and keep a
|
|
77
|
+
`.[redis]`-only environment to run the gate in.** A venv carrying `rabbitmq` and `kafka`
|
|
78
|
+
as well — which anyone doing integration work ends up with — takes a different branch
|
|
79
|
+
through the checks: `E047` reports a missing driver, so a case about anything reported
|
|
80
|
+
*after* that finding passes locally and fails on the unit legs, which install one driver.
|
|
81
|
+
That has cost a red build. The integration suite and the conformance cases for the other
|
|
82
|
+
transports need their extras; the gate does not.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
Those gate every pull request. `pytest` needs no Redis and no token.
|
|
86
|
+
|
|
87
|
+
The second invocation is the database-backed half. `tests/settings.py` has
|
|
88
|
+
`DATABASES = {}` on purpose — proving the package boots without one is part of
|
|
89
|
+
what the suite tests — so anything needing a database lives in `tests/db` under
|
|
90
|
+
`tests/db_settings.py`, and the default run ignores that directory.
|
|
91
|
+
|
|
92
|
+
CI also runs the three below — integration against a real Redis service, the
|
|
93
|
+
database suite against a real PostgreSQL, and the smoke install — so a change
|
|
94
|
+
that only passes the loop above can still fail the build. Run them locally when
|
|
95
|
+
you touch delivery, packaging, the public surface or anything about the event
|
|
96
|
+
log's table:
|
|
97
|
+
|
|
98
|
+
```shell
|
|
99
|
+
DJANGO_AIOGRAM_TEST_REDIS_URL=redis://localhost:6399/0 python -m pytest -m integration
|
|
100
|
+
python -m pytest -q --ds=tests.postgres_settings tests/db # the same suite, on a real backend
|
|
101
|
+
bash scripts/smoke_install.sh
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The second needs a PostgreSQL and the `DJANGO_AIOGRAM_TEST_PG_*` variables its
|
|
105
|
+
settings module reads — host, port, user, password and two database names, all
|
|
106
|
+
with defaults for a local container. It exists for the one thing SQLite cannot
|
|
107
|
+
answer: a sequence. `sqlite_sequence` follows an explicit id on its own and a
|
|
108
|
+
PostgreSQL sequence does not, so a copy that inserts explicit ids leaves the
|
|
109
|
+
next insert colliding, and only here does that show.
|
|
110
|
+
|
|
111
|
+
Every case runs on both. Where one backend cannot express the condition, the
|
|
112
|
+
fixture supplies it there and stays out of the way here: a connection that can
|
|
113
|
+
really die, a close that really closes, and a planner asked with `enable_seqscan`
|
|
114
|
+
off so its answer is about the index rather than about the size of a fixture.
|
|
115
|
+
|
|
116
|
+
The first needs a real server; run it when you touch delivery, serialization,
|
|
117
|
+
FSM persistence or connection cleanup. It flushes the database it is pointed at,
|
|
118
|
+
so point it at a throwaway one. RabbitMQ and Kafka have their own variables and
|
|
119
|
+
their own modules — `DJANGO_AIOGRAM_TEST_AMQP_URL`,
|
|
120
|
+
`DJANGO_AIOGRAM_TEST_KAFKA_BOOTSTRAP`, and `..._AMQP_CONTAINER` /
|
|
121
|
+
`..._KAFKA_CONTAINER` for the two cases that restart the broker. `CONTRIBUTING.md`
|
|
122
|
+
has the commands.
|
|
123
|
+
|
|
124
|
+
The second builds and installs the wheel; run it when you touch packaging,
|
|
125
|
+
Django startup or the public surface — it type-checks a consumer file against
|
|
126
|
+
the installed package, so a moved export fails there and nowhere else.
|
|
127
|
+
Packaging-only work does not need the Redis suite, and vice versa.
|
|
128
|
+
|
|
129
|
+
## Rules that are not negotiable
|
|
130
|
+
|
|
131
|
+
- **Nothing happens at import time.** The package must import, and Django must
|
|
132
|
+
boot, with no token and no reachable Redis. Anything that connects or
|
|
133
|
+
validates credentials goes behind a property or a function. This is the defect
|
|
134
|
+
2.0 existed to fix; re-introducing it breaks every consumer's test suite.
|
|
135
|
+
- **Importing the package stays cheap.** `__init__` resolves its exports lazily
|
|
136
|
+
(PEP 562) so `import django_aiogram` costs about 0.17 ms — it was ~1.4 ms
|
|
137
|
+
before 3.1.0, and the changelog's 0.134 ms is the same measurement on another
|
|
138
|
+
machine — and a disabled Django
|
|
139
|
+
boot never loads aiogram (~900 ms). `tests/test_lazy_init.py` pins both in
|
|
140
|
+
subprocesses; an eager import anywhere on the boot path fails them.
|
|
141
|
+
- **Every change carries a test, and the test must fail without the change.**
|
|
142
|
+
Revert your fix, watch the test fail, put it back. A test that passes either
|
|
143
|
+
way is worse than none, because it reads as coverage.
|
|
144
|
+
- **Values go in `extra`, not in the message.** `logger.warning('rate limited',
|
|
145
|
+
extra={'tg_function': name})`, never an f-string. Keys are `tg_`-prefixed so
|
|
146
|
+
they cannot collide with `LogRecord` attributes.
|
|
147
|
+
- **Never log through the root logger.** `logging.getLogger('django_aiogram')`
|
|
148
|
+
only; `tests/test_logging_discipline.py` enforces it, `logging.basicConfig()`
|
|
149
|
+
included.
|
|
150
|
+
- **Thread boundaries are real.** The delivery consumer runs in its own thread
|
|
151
|
+
while the event loop belongs to the polling thread. `create_task` across that
|
|
152
|
+
boundary corrupts the loop; go through `TelegramBot._schedule`.
|
|
153
|
+
- **Anything reaching the network can fail.** `run()` is a thread target: an
|
|
154
|
+
exception escaping it ends the consumer for the life of the container. Log and
|
|
155
|
+
continue, or retry.
|
|
156
|
+
- **`tests/test_public_surface.py` is a contract.** It pins the shape of
|
|
157
|
+
`TelegramBot` that predates 2.0 — attributes, methods and the observer
|
|
158
|
+
decorators. Adding to that surface is fine; moving or removing anything on it
|
|
159
|
+
is a breaking change and needs the changelog entry to say so.
|
|
160
|
+
- **`models.py` and `admin.py` import no aiogram.** Django imports the model on
|
|
161
|
+
every `django.setup()`, before `ready()` and regardless of `ENABLED`, and
|
|
162
|
+
`admin.autodiscover` imports the other on every boot of a project with the
|
|
163
|
+
admin installed — so a migration container pays for whatever either pulls.
|
|
164
|
+
`models.py` takes `django.db.models` and `django_aiogram.eventlog.events`, and nothing
|
|
165
|
+
else. `admin.py` needs more — `config.settings`, `eventlog.events` and
|
|
166
|
+
`eventlog.writer` — because a changelist reads settings and knows which alias the rows
|
|
167
|
+
are on. Neither takes
|
|
168
|
+
`client`, `serializers` or `api`. A subprocess test pins each:
|
|
169
|
+
`tests/test_event_log_off.py` for the model, `tests/db/test_admin.py` for the
|
|
170
|
+
admin.
|
|
171
|
+
- **`healthcheck.py` never populates the app registry.** It exists so
|
|
172
|
+
`python -m django_aiogram.healthcheck` can answer without `django.setup()`,
|
|
173
|
+
which in one measured consumer cost 17.9s of `AppConfig.ready()` against 0.01s of
|
|
174
|
+
probing — more than any Docker `timeout` the wiki could publish. So: no models, no
|
|
175
|
+
aiogram, no `django_aiogram.producer.client`, and nothing that reaches them
|
|
176
|
+
transitively. `tests/test_lazy_init.py` proves it with a settings module whose app
|
|
177
|
+
writes a file from `ready()`, and asserts the file is absent — plus a control that
|
|
178
|
+
the file appears under `django.setup()`, so its absence means something.
|
|
179
|
+
- **`recorder.py` imports no `django.db`, and neither does anything it was split into.**
|
|
180
|
+
`records.py`, `pacing.py`, `bookkeeping.py` and `publishing.py` sit below it and import it
|
|
181
|
+
back never; `tests/test_package_layout.py` runs each in a fresh interpreter and asserts
|
|
182
|
+
both. Only `eventlog/writer.py` reaches the ORM, and the
|
|
183
|
+
writer thread imports it on its first *write* — not its first flush, which since
|
|
184
|
+
3.1.0 are different things. That is what makes a disabled log
|
|
185
|
+
cost nothing and what makes `record()` legal from a coroutine — `put_nowait`
|
|
186
|
+
touches no I/O, so there is no `SynchronousOnlyOperation` to avoid. `EVENT_LOG_SYNC`
|
|
187
|
+
is the one exception and is test-only: it inserts on the calling thread, which is
|
|
188
|
+
also why it refuses to act inside a running loop, where the ORM is `@async_unsafe`. Since 3.1.0
|
|
189
|
+
the writer also runs with the log *off*, for `events_recorded` receivers alone.
|
|
190
|
+
Such a process writes no rows, so `EventRecorder._run` must not call
|
|
191
|
+
`_close_connections()` on its way out: that imports `eventlog/writer.py`, which imports
|
|
192
|
+
`django.db`, to close a connection nothing ever opened. It is gated on
|
|
193
|
+
the thread marks in `eventlog/bookkeeping.py`, set only where a batch is actually handed
|
|
194
|
+
to the ORM and **read and cleared when the writer stops** — the recorder is a
|
|
195
|
+
process-wide singleton, so a mark left set outlives the writer that earned it and the
|
|
196
|
+
next one closes a connection it never opened. `tests/test_metrics_seam.py` pins both
|
|
197
|
+
directions, and pins them in either order: run that file reversed before
|
|
198
|
+
believing it.
|
|
199
|
+
- **The feed is append-only.** No updates, no foreign keys, no
|
|
200
|
+
`Meta.constraints`, no index on the JSON column. Fast pruning, shardability
|
|
201
|
+
and two processes writing one message's history without coordination all rest
|
|
202
|
+
on it; a foreign key alone breaks Django's fast-delete path.
|
|
203
|
+
- **`record()` may neither raise nor wait.** A log that can break delivery is
|
|
204
|
+
worse than no log, so everything — the settings read included — is wrapped.
|
|
205
|
+
- **The fact table stores identifiers, never descriptors.** A chat title belongs
|
|
206
|
+
in `detail` as a snapshot of the event, not in a column.
|
|
207
|
+
- **The token must not reach a row.** It is in the API URL, aiogram puts the URL
|
|
208
|
+
in its exception messages, and those messages are what an `error` column holds.
|
|
209
|
+
- **Interpolate `.value`, never a `(str, Enum)` member.** On newer Pythons a
|
|
210
|
+
member formats as its own qualified name.
|
|
211
|
+
|
|
212
|
+
## Style
|
|
213
|
+
|
|
214
|
+
- Code, comments, docstrings and documentation are in English.
|
|
215
|
+
- Comment what is not obvious from the code, one line by default. Explain *why*,
|
|
216
|
+
not *what*.
|
|
217
|
+
- **Everything in `src/` has a docstring, nested closures and private helpers
|
|
218
|
+
included.** Those two are what ruff's `D` rules cannot see, and they are where
|
|
219
|
+
this package keeps its retry loop, its acknowledgement callback and its loop
|
|
220
|
+
thread — so `tests/test_docstring_coverage.py` walks the syntax tree and names
|
|
221
|
+
the definition that is missing one — and one that only restates it, where every
|
|
222
|
+
word of the summary is filler or a word of the name. Write the *why*, which is
|
|
223
|
+
the part no test can check for you. `tests/` is exempt, which
|
|
224
|
+
`pyproject.toml` records as `"D", # test names are the documentation`.
|
|
225
|
+
- Public API is annotated; the package ships `py.typed` and mypy runs on it.
|
|
226
|
+
- No new runtime dependencies without a reason that survives being questioned.
|
|
227
|
+
|
|
228
|
+
## Package layout
|
|
229
|
+
|
|
230
|
+
`src/django_aiogram/` groups by what a thing *is for*, not by what it is made of.
|
|
231
|
+
|
|
232
|
+
| package | what belongs in it |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `config/` | what a project configures, and what refuses a bad value |
|
|
235
|
+
| `broker/` | one transport per package; the contract they answer |
|
|
236
|
+
| `producer/` | the send side: the bot, the producer, the pacing |
|
|
237
|
+
| `consumer/` | the receive side: the queue consumer, the webhook view, router discovery |
|
|
238
|
+
| `wire/` | how a message becomes bytes and comes back |
|
|
239
|
+
| `eventlog/` | the optional table, the writer thread, the metrics seam |
|
|
240
|
+
|
|
241
|
+
**The root keeps two kinds of thing**, and nothing else:
|
|
242
|
+
|
|
243
|
+
- **What cannot move.** Django looks for `apps.py`, `models.py`, `admin.py` and
|
|
244
|
+
`migrations/` there — moving them costs an `app_label` on every model and a
|
|
245
|
+
`MIGRATION_MODULES` in every consuming project. And `python -m
|
|
246
|
+
django_aiogram.healthcheck` sits in a compose file, where nothing can rewrite it and no
|
|
247
|
+
check can see it.
|
|
248
|
+
- **What every cluster needs and none of them owns**: `api.py`, `exceptions.py`,
|
|
249
|
+
`context.py`, `_singleton.py` and `redis.py`. A package for five small modules would
|
|
250
|
+
add a directory and answer no question — and putting a shared one *inside* a cluster
|
|
251
|
+
would make every other cluster import that cluster to reach it.
|
|
252
|
+
|
|
253
|
+
Everything else moves, including paths a project wrote down. Two of those exist and are
|
|
254
|
+
worth knowing by name, because neither is found by an import a test would notice:
|
|
255
|
+
`DATABASE_ROUTERS` holds `django_aiogram.eventlog.dbrouter.TelegramEventLogRouter`, and a
|
|
256
|
+
project's own `urls.py` names the webhook view. `FSM_STORAGE` takes a dotted path too, but
|
|
257
|
+
to a class of the project's choosing rather than one of ours.
|
|
258
|
+
|
|
259
|
+
`DELIVERY` and `SERIALIZER` are *not* in that list, however much they look like it: they
|
|
260
|
+
hold short names — `blpop`, `json`, `pickle` — validated against an enum, so moving the
|
|
261
|
+
classes behind them breaks nothing a project wrote.
|
|
262
|
+
|
|
263
|
+
So a module's location is still an API decision rather than a filing decision, and a move
|
|
264
|
+
belongs in the changelog table and in `Upgrading.md` with the old path against the new.
|
|
265
|
+
|
|
266
|
+
Two rules that are not style:
|
|
267
|
+
|
|
268
|
+
- **`__init__.py` exports deliberately.** Every package declares `__all__`. The cluster
|
|
269
|
+
packages declare it *empty*: callers import from the modules, because a re-export makes
|
|
270
|
+
a second path to every name and the one nobody chose is the one that cannot be moved.
|
|
271
|
+
`tests/test_package_layout.py` fails when a package has no `__all__`.
|
|
272
|
+
- **A transport imports its driver lazily, never at module scope.** The base install pulls
|
|
273
|
+
no driver, so `import django_aiogram.broker.kafka` must not fail on a machine without
|
|
274
|
+
Kafka — otherwise the check that names the missing extra can never run, and the reader
|
|
275
|
+
gets an `ImportError` instead of `pip install "django-aiogram[kafka]"`.
|
|
276
|
+
|
|
277
|
+
This binds the modules a transport *reaches through* as well, which is where it was
|
|
278
|
+
first broken: `django_aiogram.redis` and `producer/client.py` both imported the driver
|
|
279
|
+
at module scope, so `from django_aiogram import bot` failed on a base install.
|
|
280
|
+
`tests/test_package_layout.py` imports the transport package and the producer in a fresh
|
|
281
|
+
interpreter each and fails if `redis` lands in `sys.modules`. Type annotations go under
|
|
282
|
+
`TYPE_CHECKING`; a client is built inside the function that builds it.
|
|
283
|
+
|
|
284
|
+
## Documentation
|
|
285
|
+
|
|
286
|
+
Wiki pages live in `docs/wiki/` and are edited in the same pull request as the
|
|
287
|
+
code they describe. Links are `[[Page-Name]]`, or `[[Page-Name|Link text]]`
|
|
288
|
+
with the page first when the label differs;
|
|
289
|
+
`tests/test_wiki.py` checks that every link resolves, that the sidebar lists
|
|
290
|
+
every page, and that the README's wiki links are not stale. Configuration
|
|
291
|
+
examples in the docs are executed by `tests/test_docs_examples.py` and
|
|
292
|
+
`tests/test_documented_recipes.py`, so a snippet that cannot run fails the build.
|
|
293
|
+
|
|
294
|
+
## Pull requests
|
|
295
|
+
|
|
296
|
+
One reviewable change per pull request, green on `ruff`, `ruff format`, `mypy`
|
|
297
|
+
and `pytest`. Say why the change is needed and what failure it produces.
|
|
298
|
+
[CodeRabbit](https://github.com/apps/coderabbitai) reviews automatically;
|
|
299
|
+
answer its findings, fix what is still valid and say plainly what you skipped
|
|
300
|
+
and why.
|