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.
Files changed (199) hide show
  1. django_aiogram-4.0.0/.gitignore +26 -0
  2. django_aiogram-4.0.0/AGENTS.md +300 -0
  3. django_aiogram-4.0.0/CHANGELOG.md +2058 -0
  4. django_aiogram-4.0.0/CONTRIBUTING.md +206 -0
  5. django_aiogram-4.0.0/LICENSE +21 -0
  6. django_aiogram-4.0.0/PKG-INFO +193 -0
  7. django_aiogram-4.0.0/README.md +148 -0
  8. django_aiogram-4.0.0/SECURITY.md +96 -0
  9. django_aiogram-4.0.0/docs/wiki/AI-assistants.md +139 -0
  10. django_aiogram-4.0.0/docs/wiki/API.md +376 -0
  11. django_aiogram-4.0.0/docs/wiki/Delivery.md +292 -0
  12. django_aiogram-4.0.0/docs/wiki/Deployment.md +533 -0
  13. django_aiogram-4.0.0/docs/wiki/Event-log.md +535 -0
  14. django_aiogram-4.0.0/docs/wiki/Handlers.md +116 -0
  15. django_aiogram-4.0.0/docs/wiki/Home.md +52 -0
  16. django_aiogram-4.0.0/docs/wiki/Installation.md +156 -0
  17. django_aiogram-4.0.0/docs/wiki/Kafka.md +156 -0
  18. django_aiogram-4.0.0/docs/wiki/Logging.md +133 -0
  19. django_aiogram-4.0.0/docs/wiki/RabbitMQ.md +104 -0
  20. django_aiogram-4.0.0/docs/wiki/Rate-limits.md +90 -0
  21. django_aiogram-4.0.0/docs/wiki/Redis-Streams.md +88 -0
  22. django_aiogram-4.0.0/docs/wiki/Redis-list.md +103 -0
  23. django_aiogram-4.0.0/docs/wiki/Sending-messages.md +175 -0
  24. django_aiogram-4.0.0/docs/wiki/Serialization.md +222 -0
  25. django_aiogram-4.0.0/docs/wiki/Settings.md +321 -0
  26. django_aiogram-4.0.0/docs/wiki/Testing.md +269 -0
  27. django_aiogram-4.0.0/docs/wiki/Troubleshooting.md +331 -0
  28. django_aiogram-4.0.0/docs/wiki/Upgrading.md +662 -0
  29. django_aiogram-4.0.0/docs/wiki/Webhook.md +196 -0
  30. django_aiogram-4.0.0/docs/wiki/_Sidebar.md +29 -0
  31. django_aiogram-4.0.0/pyproject.toml +251 -0
  32. django_aiogram-4.0.0/scripts/measurements/README.md +166 -0
  33. django_aiogram-4.0.0/scripts/measurements/__init__.py +11 -0
  34. django_aiogram-4.0.0/scripts/measurements/_timing.py +80 -0
  35. django_aiogram-4.0.0/scripts/measurements/amqp_driver_choice.py +403 -0
  36. django_aiogram-4.0.0/scripts/measurements/constraints.txt +12 -0
  37. django_aiogram-4.0.0/scripts/measurements/kafka_driver_choice.py +251 -0
  38. django_aiogram-4.0.0/scripts/measurements/redis_baseline.py +62 -0
  39. django_aiogram-4.0.0/scripts/smoke_install.sh +307 -0
  40. django_aiogram-4.0.0/src/django_aiogram/__init__.py +60 -0
  41. django_aiogram-4.0.0/src/django_aiogram/_singleton.py +22 -0
  42. django_aiogram-4.0.0/src/django_aiogram/admin.py +410 -0
  43. django_aiogram-4.0.0/src/django_aiogram/api.py +38 -0
  44. django_aiogram-4.0.0/src/django_aiogram/apps.py +56 -0
  45. django_aiogram-4.0.0/src/django_aiogram/broker/__init__.py +10 -0
  46. django_aiogram-4.0.0/src/django_aiogram/broker/base.py +390 -0
  47. django_aiogram-4.0.0/src/django_aiogram/broker/exceptions.py +75 -0
  48. django_aiogram-4.0.0/src/django_aiogram/broker/kafka/__init__.py +6 -0
  49. django_aiogram-4.0.0/src/django_aiogram/broker/kafka/broker.py +660 -0
  50. django_aiogram-4.0.0/src/django_aiogram/broker/kafka/client.py +281 -0
  51. django_aiogram-4.0.0/src/django_aiogram/broker/kafka/exceptions.py +38 -0
  52. django_aiogram-4.0.0/src/django_aiogram/broker/models.py +31 -0
  53. django_aiogram-4.0.0/src/django_aiogram/broker/rabbitmq/__init__.py +6 -0
  54. django_aiogram-4.0.0/src/django_aiogram/broker/rabbitmq/broker.py +350 -0
  55. django_aiogram-4.0.0/src/django_aiogram/broker/rabbitmq/client.py +198 -0
  56. django_aiogram-4.0.0/src/django_aiogram/broker/rabbitmq/exceptions.py +37 -0
  57. django_aiogram-4.0.0/src/django_aiogram/broker/redis_list/__init__.py +6 -0
  58. django_aiogram-4.0.0/src/django_aiogram/broker/redis_list/broker.py +263 -0
  59. django_aiogram-4.0.0/src/django_aiogram/broker/redis_streams/__init__.py +6 -0
  60. django_aiogram-4.0.0/src/django_aiogram/broker/redis_streams/broker.py +694 -0
  61. django_aiogram-4.0.0/src/django_aiogram/broker/redis_streams/exceptions.py +68 -0
  62. django_aiogram-4.0.0/src/django_aiogram/broker/registry.py +146 -0
  63. django_aiogram-4.0.0/src/django_aiogram/config/__init__.py +15 -0
  64. django_aiogram-4.0.0/src/django_aiogram/config/checks/__init__.py +179 -0
  65. django_aiogram-4.0.0/src/django_aiogram/config/checks/bot.py +216 -0
  66. django_aiogram-4.0.0/src/django_aiogram/config/checks/conditions.py +98 -0
  67. django_aiogram-4.0.0/src/django_aiogram/config/checks/eventlog.py +320 -0
  68. django_aiogram-4.0.0/src/django_aiogram/config/checks/problems.py +90 -0
  69. django_aiogram-4.0.0/src/django_aiogram/config/checks/shapes.py +202 -0
  70. django_aiogram-4.0.0/src/django_aiogram/config/checks/transport.py +455 -0
  71. django_aiogram-4.0.0/src/django_aiogram/config/defaults.py +109 -0
  72. django_aiogram-4.0.0/src/django_aiogram/config/enums.py +132 -0
  73. django_aiogram-4.0.0/src/django_aiogram/config/settings.py +286 -0
  74. django_aiogram-4.0.0/src/django_aiogram/consumer/__init__.py +13 -0
  75. django_aiogram-4.0.0/src/django_aiogram/consumer/delivery.py +653 -0
  76. django_aiogram-4.0.0/src/django_aiogram/consumer/routers.py +19 -0
  77. django_aiogram-4.0.0/src/django_aiogram/consumer/webhook.py +161 -0
  78. django_aiogram-4.0.0/src/django_aiogram/context.py +34 -0
  79. django_aiogram-4.0.0/src/django_aiogram/db.py +87 -0
  80. django_aiogram-4.0.0/src/django_aiogram/eventlog/__init__.py +20 -0
  81. django_aiogram-4.0.0/src/django_aiogram/eventlog/bookkeeping.py +159 -0
  82. django_aiogram-4.0.0/src/django_aiogram/eventlog/dbrouter.py +55 -0
  83. django_aiogram-4.0.0/src/django_aiogram/eventlog/events.py +185 -0
  84. django_aiogram-4.0.0/src/django_aiogram/eventlog/instrumentation.py +233 -0
  85. django_aiogram-4.0.0/src/django_aiogram/eventlog/moving.py +97 -0
  86. django_aiogram-4.0.0/src/django_aiogram/eventlog/pacing.py +67 -0
  87. django_aiogram-4.0.0/src/django_aiogram/eventlog/publishing.py +97 -0
  88. django_aiogram-4.0.0/src/django_aiogram/eventlog/recorder.py +689 -0
  89. django_aiogram-4.0.0/src/django_aiogram/eventlog/records.py +68 -0
  90. django_aiogram-4.0.0/src/django_aiogram/eventlog/signals.py +84 -0
  91. django_aiogram-4.0.0/src/django_aiogram/eventlog/writer.py +235 -0
  92. django_aiogram-4.0.0/src/django_aiogram/exceptions.py +97 -0
  93. django_aiogram-4.0.0/src/django_aiogram/healthcheck.py +599 -0
  94. django_aiogram-4.0.0/src/django_aiogram/management/__init__.py +1 -0
  95. django_aiogram-4.0.0/src/django_aiogram/management/commands/__init__.py +1 -0
  96. django_aiogram-4.0.0/src/django_aiogram/management/commands/start_tgbot.py +321 -0
  97. django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_backfill_short_ids.py +97 -0
  98. django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_healthcheck.py +65 -0
  99. django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_move_events.py +299 -0
  100. django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_prune_events.py +144 -0
  101. django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_reclaim.py +161 -0
  102. django_aiogram-4.0.0/src/django_aiogram/management/commands/tgbot_webhook.py +87 -0
  103. django_aiogram-4.0.0/src/django_aiogram/migrations/0001_initial.py +50 -0
  104. django_aiogram-4.0.0/src/django_aiogram/migrations/0002_kind_id_index.py +32 -0
  105. django_aiogram-4.0.0/src/django_aiogram/migrations/0003_short_id.py +34 -0
  106. django_aiogram-4.0.0/src/django_aiogram/migrations/__init__.py +1 -0
  107. django_aiogram-4.0.0/src/django_aiogram/models.py +85 -0
  108. django_aiogram-4.0.0/src/django_aiogram/producer/__init__.py +13 -0
  109. django_aiogram-4.0.0/src/django_aiogram/producer/client.py +1215 -0
  110. django_aiogram-4.0.0/src/django_aiogram/producer/from_settings.py +88 -0
  111. django_aiogram-4.0.0/src/django_aiogram/producer/looping.py +110 -0
  112. django_aiogram-4.0.0/src/django_aiogram/producer/outbound.py +115 -0
  113. django_aiogram-4.0.0/src/django_aiogram/producer/queueing.py +152 -0
  114. django_aiogram-4.0.0/src/django_aiogram/producer/routing.py +104 -0
  115. django_aiogram-4.0.0/src/django_aiogram/producer/throttling.py +336 -0
  116. django_aiogram-4.0.0/src/django_aiogram/py.typed +0 -0
  117. django_aiogram-4.0.0/src/django_aiogram/redis.py +464 -0
  118. django_aiogram-4.0.0/src/django_aiogram/wire/__init__.py +14 -0
  119. django_aiogram-4.0.0/src/django_aiogram/wire/envelope.py +156 -0
  120. django_aiogram-4.0.0/src/django_aiogram/wire/payloads.py +205 -0
  121. django_aiogram-4.0.0/src/django_aiogram/wire/serializers.py +539 -0
  122. django_aiogram-4.0.0/tests/__init__.py +0 -0
  123. django_aiogram-4.0.0/tests/bare_settings.py +18 -0
  124. django_aiogram-4.0.0/tests/conftest.py +79 -0
  125. django_aiogram-4.0.0/tests/db/__init__.py +0 -0
  126. django_aiogram-4.0.0/tests/db/conftest.py +172 -0
  127. django_aiogram-4.0.0/tests/db/test_admin.py +641 -0
  128. django_aiogram-4.0.0/tests/db/test_connections.py +143 -0
  129. django_aiogram-4.0.0/tests/db/test_event_log_model.py +163 -0
  130. django_aiogram-4.0.0/tests/db/test_harness.py +73 -0
  131. django_aiogram-4.0.0/tests/db/test_inbound.py +301 -0
  132. django_aiogram-4.0.0/tests/db/test_move_events.py +544 -0
  133. django_aiogram-4.0.0/tests/db/test_outbound.py +379 -0
  134. django_aiogram-4.0.0/tests/db/test_prune.py +270 -0
  135. django_aiogram-4.0.0/tests/db/test_recorder.py +1078 -0
  136. django_aiogram-4.0.0/tests/db/test_redaction.py +74 -0
  137. django_aiogram-4.0.0/tests/db/test_short_id.py +223 -0
  138. django_aiogram-4.0.0/tests/db_settings.py +56 -0
  139. django_aiogram-4.0.0/tests/db_urls.py +6 -0
  140. django_aiogram-4.0.0/tests/fake_app/__init__.py +0 -0
  141. django_aiogram-4.0.0/tests/fake_app/broken_router.py +7 -0
  142. django_aiogram-4.0.0/tests/fake_app/tg_router.py +17 -0
  143. django_aiogram-4.0.0/tests/integration/__init__.py +0 -0
  144. django_aiogram-4.0.0/tests/integration/conftest.py +246 -0
  145. django_aiogram-4.0.0/tests/integration/test_async_producer_against_redis.py +166 -0
  146. django_aiogram-4.0.0/tests/integration/test_delivery_against_redis.py +338 -0
  147. django_aiogram-4.0.0/tests/integration/test_fsm_across_restart.py +94 -0
  148. django_aiogram-4.0.0/tests/integration/test_kafka_against_broker.py +559 -0
  149. django_aiogram-4.0.0/tests/integration/test_rabbitmq_against_broker.py +601 -0
  150. django_aiogram-4.0.0/tests/integration/test_streams_against_redis.py +210 -0
  151. django_aiogram-4.0.0/tests/marker_app/__init__.py +6 -0
  152. django_aiogram-4.0.0/tests/marker_app/apps.py +26 -0
  153. django_aiogram-4.0.0/tests/marker_settings.py +13 -0
  154. django_aiogram-4.0.0/tests/postgres_settings.py +56 -0
  155. django_aiogram-4.0.0/tests/settings.py +8 -0
  156. django_aiogram-4.0.0/tests/support.py +41 -0
  157. django_aiogram-4.0.0/tests/test_async_producer.py +707 -0
  158. django_aiogram-4.0.0/tests/test_autodiscover.py +72 -0
  159. django_aiogram-4.0.0/tests/test_bot_properties.py +187 -0
  160. django_aiogram-4.0.0/tests/test_broker_conformance.py +522 -0
  161. django_aiogram-4.0.0/tests/test_broker_lifecycle.py +207 -0
  162. django_aiogram-4.0.0/tests/test_broker_neutral_prose.py +171 -0
  163. django_aiogram-4.0.0/tests/test_checks.py +2006 -0
  164. django_aiogram-4.0.0/tests/test_conf_edge_cases.py +239 -0
  165. django_aiogram-4.0.0/tests/test_crash_safety.py +1162 -0
  166. django_aiogram-4.0.0/tests/test_db_suite_guard.py +34 -0
  167. django_aiogram-4.0.0/tests/test_delivery.py +788 -0
  168. django_aiogram-4.0.0/tests/test_dependency_floors.py +180 -0
  169. django_aiogram-4.0.0/tests/test_docs_examples.py +139 -0
  170. django_aiogram-4.0.0/tests/test_docstring_coverage.py +252 -0
  171. django_aiogram-4.0.0/tests/test_docstring_references.py +190 -0
  172. django_aiogram-4.0.0/tests/test_documented_recipes.py +708 -0
  173. django_aiogram-4.0.0/tests/test_dotted_paths.py +45 -0
  174. django_aiogram-4.0.0/tests/test_enabled_flag.py +215 -0
  175. django_aiogram-4.0.0/tests/test_envelope.py +176 -0
  176. django_aiogram-4.0.0/tests/test_event_log_off.py +236 -0
  177. django_aiogram-4.0.0/tests/test_healthcheck.py +1232 -0
  178. django_aiogram-4.0.0/tests/test_inflight_by_worker.py +146 -0
  179. django_aiogram-4.0.0/tests/test_kafka_publish.py +403 -0
  180. django_aiogram-4.0.0/tests/test_lazy_init.py +471 -0
  181. django_aiogram-4.0.0/tests/test_logging_discipline.py +416 -0
  182. django_aiogram-4.0.0/tests/test_measurements.py +282 -0
  183. django_aiogram-4.0.0/tests/test_metrics_seam.py +968 -0
  184. django_aiogram-4.0.0/tests/test_package_layout.py +313 -0
  185. django_aiogram-4.0.0/tests/test_payloads.py +363 -0
  186. django_aiogram-4.0.0/tests/test_public_surface.py +437 -0
  187. django_aiogram-4.0.0/tests/test_rabbitmq_channel.py +68 -0
  188. django_aiogram-4.0.0/tests/test_redis_helpers.py +427 -0
  189. django_aiogram-4.0.0/tests/test_refusals.py +216 -0
  190. django_aiogram-4.0.0/tests/test_serializers.py +373 -0
  191. django_aiogram-4.0.0/tests/test_shutdown.py +634 -0
  192. django_aiogram-4.0.0/tests/test_start_command.py +522 -0
  193. django_aiogram-4.0.0/tests/test_streams_recovery.py +251 -0
  194. django_aiogram-4.0.0/tests/test_suite_discipline.py +197 -0
  195. django_aiogram-4.0.0/tests/test_throttling.py +522 -0
  196. django_aiogram-4.0.0/tests/test_transport_pages.py +169 -0
  197. django_aiogram-4.0.0/tests/test_unified_send.py +223 -0
  198. django_aiogram-4.0.0/tests/test_webhook.py +1208 -0
  199. 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.