django-aiogram 4.0.0.dev0__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 (128) hide show
  1. django_aiogram-4.0.0.dev0/.gitignore +26 -0
  2. django_aiogram-4.0.0.dev0/AGENTS.md +249 -0
  3. django_aiogram-4.0.0.dev0/CHANGELOG.md +1153 -0
  4. django_aiogram-4.0.0.dev0/CONTRIBUTING.md +136 -0
  5. django_aiogram-4.0.0.dev0/LICENSE +21 -0
  6. django_aiogram-4.0.0.dev0/PKG-INFO +145 -0
  7. django_aiogram-4.0.0.dev0/README.md +106 -0
  8. django_aiogram-4.0.0.dev0/SECURITY.md +87 -0
  9. django_aiogram-4.0.0.dev0/docs/wiki/AI-assistants.md +138 -0
  10. django_aiogram-4.0.0.dev0/docs/wiki/API.md +292 -0
  11. django_aiogram-4.0.0.dev0/docs/wiki/Delivery.md +177 -0
  12. django_aiogram-4.0.0.dev0/docs/wiki/Deployment.md +325 -0
  13. django_aiogram-4.0.0.dev0/docs/wiki/Event-log.md +418 -0
  14. django_aiogram-4.0.0.dev0/docs/wiki/Handlers.md +116 -0
  15. django_aiogram-4.0.0.dev0/docs/wiki/Home.md +47 -0
  16. django_aiogram-4.0.0.dev0/docs/wiki/Installation.md +98 -0
  17. django_aiogram-4.0.0.dev0/docs/wiki/Logging.md +123 -0
  18. django_aiogram-4.0.0.dev0/docs/wiki/Rate-limits.md +90 -0
  19. django_aiogram-4.0.0.dev0/docs/wiki/Sending-messages.md +165 -0
  20. django_aiogram-4.0.0.dev0/docs/wiki/Serialization.md +222 -0
  21. django_aiogram-4.0.0.dev0/docs/wiki/Settings.md +196 -0
  22. django_aiogram-4.0.0.dev0/docs/wiki/Testing.md +262 -0
  23. django_aiogram-4.0.0.dev0/docs/wiki/Troubleshooting.md +275 -0
  24. django_aiogram-4.0.0.dev0/docs/wiki/Upgrading.md +343 -0
  25. django_aiogram-4.0.0.dev0/docs/wiki/Webhook.md +180 -0
  26. django_aiogram-4.0.0.dev0/docs/wiki/_Sidebar.md +23 -0
  27. django_aiogram-4.0.0.dev0/pyproject.toml +184 -0
  28. django_aiogram-4.0.0.dev0/scripts/smoke_install.sh +217 -0
  29. django_aiogram-4.0.0.dev0/src/django_aiogram/__init__.py +64 -0
  30. django_aiogram-4.0.0.dev0/src/django_aiogram/_singleton.py +22 -0
  31. django_aiogram-4.0.0.dev0/src/django_aiogram/admin.py +380 -0
  32. django_aiogram-4.0.0.dev0/src/django_aiogram/api.py +38 -0
  33. django_aiogram-4.0.0.dev0/src/django_aiogram/apps.py +56 -0
  34. django_aiogram-4.0.0.dev0/src/django_aiogram/config/__init__.py +15 -0
  35. django_aiogram-4.0.0.dev0/src/django_aiogram/config/checks.py +759 -0
  36. django_aiogram-4.0.0.dev0/src/django_aiogram/config/defaults.py +102 -0
  37. django_aiogram-4.0.0.dev0/src/django_aiogram/config/enums.py +108 -0
  38. django_aiogram-4.0.0.dev0/src/django_aiogram/config/settings.py +266 -0
  39. django_aiogram-4.0.0.dev0/src/django_aiogram/consumer/__init__.py +13 -0
  40. django_aiogram-4.0.0.dev0/src/django_aiogram/consumer/delivery.py +625 -0
  41. django_aiogram-4.0.0.dev0/src/django_aiogram/consumer/routers.py +19 -0
  42. django_aiogram-4.0.0.dev0/src/django_aiogram/consumer/webhook.py +154 -0
  43. django_aiogram-4.0.0.dev0/src/django_aiogram/context.py +34 -0
  44. django_aiogram-4.0.0.dev0/src/django_aiogram/eventlog/__init__.py +16 -0
  45. django_aiogram-4.0.0.dev0/src/django_aiogram/eventlog/dbrouter.py +55 -0
  46. django_aiogram-4.0.0.dev0/src/django_aiogram/eventlog/events.py +120 -0
  47. django_aiogram-4.0.0.dev0/src/django_aiogram/eventlog/instrumentation.py +231 -0
  48. django_aiogram-4.0.0.dev0/src/django_aiogram/eventlog/recorder.py +922 -0
  49. django_aiogram-4.0.0.dev0/src/django_aiogram/eventlog/signals.py +84 -0
  50. django_aiogram-4.0.0.dev0/src/django_aiogram/eventlog/writer.py +231 -0
  51. django_aiogram-4.0.0.dev0/src/django_aiogram/exceptions.py +60 -0
  52. django_aiogram-4.0.0.dev0/src/django_aiogram/healthcheck.py +412 -0
  53. django_aiogram-4.0.0.dev0/src/django_aiogram/management/__init__.py +1 -0
  54. django_aiogram-4.0.0.dev0/src/django_aiogram/management/commands/__init__.py +1 -0
  55. django_aiogram-4.0.0.dev0/src/django_aiogram/management/commands/start_tgbot.py +308 -0
  56. django_aiogram-4.0.0.dev0/src/django_aiogram/management/commands/tgbot_healthcheck.py +57 -0
  57. django_aiogram-4.0.0.dev0/src/django_aiogram/management/commands/tgbot_prune_events.py +144 -0
  58. django_aiogram-4.0.0.dev0/src/django_aiogram/management/commands/tgbot_reclaim.py +135 -0
  59. django_aiogram-4.0.0.dev0/src/django_aiogram/management/commands/tgbot_webhook.py +87 -0
  60. django_aiogram-4.0.0.dev0/src/django_aiogram/migrations/0001_initial.py +50 -0
  61. django_aiogram-4.0.0.dev0/src/django_aiogram/migrations/0002_kind_id_index.py +32 -0
  62. django_aiogram-4.0.0.dev0/src/django_aiogram/migrations/__init__.py +1 -0
  63. django_aiogram-4.0.0.dev0/src/django_aiogram/models.py +79 -0
  64. django_aiogram-4.0.0.dev0/src/django_aiogram/producer/__init__.py +13 -0
  65. django_aiogram-4.0.0.dev0/src/django_aiogram/producer/client.py +1540 -0
  66. django_aiogram-4.0.0.dev0/src/django_aiogram/producer/throttling.py +336 -0
  67. django_aiogram-4.0.0.dev0/src/django_aiogram/py.typed +0 -0
  68. django_aiogram-4.0.0.dev0/src/django_aiogram/redis.py +394 -0
  69. django_aiogram-4.0.0.dev0/src/django_aiogram/wire/__init__.py +14 -0
  70. django_aiogram-4.0.0.dev0/src/django_aiogram/wire/envelope.py +146 -0
  71. django_aiogram-4.0.0.dev0/src/django_aiogram/wire/payloads.py +195 -0
  72. django_aiogram-4.0.0.dev0/src/django_aiogram/wire/serializers.py +533 -0
  73. django_aiogram-4.0.0.dev0/tests/__init__.py +0 -0
  74. django_aiogram-4.0.0.dev0/tests/bare_settings.py +18 -0
  75. django_aiogram-4.0.0.dev0/tests/conftest.py +53 -0
  76. django_aiogram-4.0.0.dev0/tests/db/__init__.py +0 -0
  77. django_aiogram-4.0.0.dev0/tests/db/conftest.py +66 -0
  78. django_aiogram-4.0.0.dev0/tests/db/test_admin.py +554 -0
  79. django_aiogram-4.0.0.dev0/tests/db/test_event_log_model.py +112 -0
  80. django_aiogram-4.0.0.dev0/tests/db/test_harness.py +63 -0
  81. django_aiogram-4.0.0.dev0/tests/db/test_inbound.py +301 -0
  82. django_aiogram-4.0.0.dev0/tests/db/test_outbound.py +375 -0
  83. django_aiogram-4.0.0.dev0/tests/db/test_prune.py +270 -0
  84. django_aiogram-4.0.0.dev0/tests/db/test_recorder.py +1085 -0
  85. django_aiogram-4.0.0.dev0/tests/db/test_redaction.py +74 -0
  86. django_aiogram-4.0.0.dev0/tests/db_settings.py +56 -0
  87. django_aiogram-4.0.0.dev0/tests/db_urls.py +6 -0
  88. django_aiogram-4.0.0.dev0/tests/fake_app/__init__.py +0 -0
  89. django_aiogram-4.0.0.dev0/tests/fake_app/broken_router.py +7 -0
  90. django_aiogram-4.0.0.dev0/tests/fake_app/tg_router.py +17 -0
  91. django_aiogram-4.0.0.dev0/tests/integration/__init__.py +0 -0
  92. django_aiogram-4.0.0.dev0/tests/integration/conftest.py +53 -0
  93. django_aiogram-4.0.0.dev0/tests/integration/test_async_producer_against_redis.py +166 -0
  94. django_aiogram-4.0.0.dev0/tests/integration/test_delivery_against_redis.py +323 -0
  95. django_aiogram-4.0.0.dev0/tests/integration/test_fsm_across_restart.py +94 -0
  96. django_aiogram-4.0.0.dev0/tests/marker_app/__init__.py +6 -0
  97. django_aiogram-4.0.0.dev0/tests/marker_app/apps.py +26 -0
  98. django_aiogram-4.0.0.dev0/tests/marker_settings.py +13 -0
  99. django_aiogram-4.0.0.dev0/tests/settings.py +8 -0
  100. django_aiogram-4.0.0.dev0/tests/test_async_producer.py +611 -0
  101. django_aiogram-4.0.0.dev0/tests/test_autodiscover.py +72 -0
  102. django_aiogram-4.0.0.dev0/tests/test_bot_properties.py +169 -0
  103. django_aiogram-4.0.0.dev0/tests/test_checks.py +779 -0
  104. django_aiogram-4.0.0.dev0/tests/test_conf_edge_cases.py +239 -0
  105. django_aiogram-4.0.0.dev0/tests/test_crash_safety.py +1161 -0
  106. django_aiogram-4.0.0.dev0/tests/test_db_suite_guard.py +34 -0
  107. django_aiogram-4.0.0.dev0/tests/test_delivery.py +549 -0
  108. django_aiogram-4.0.0.dev0/tests/test_docs_examples.py +43 -0
  109. django_aiogram-4.0.0.dev0/tests/test_docstring_coverage.py +252 -0
  110. django_aiogram-4.0.0.dev0/tests/test_documented_recipes.py +528 -0
  111. django_aiogram-4.0.0.dev0/tests/test_enabled_flag.py +173 -0
  112. django_aiogram-4.0.0.dev0/tests/test_envelope.py +176 -0
  113. django_aiogram-4.0.0.dev0/tests/test_event_log_off.py +224 -0
  114. django_aiogram-4.0.0.dev0/tests/test_healthcheck.py +838 -0
  115. django_aiogram-4.0.0.dev0/tests/test_lazy_init.py +505 -0
  116. django_aiogram-4.0.0.dev0/tests/test_logging_discipline.py +416 -0
  117. django_aiogram-4.0.0.dev0/tests/test_metrics_seam.py +913 -0
  118. django_aiogram-4.0.0.dev0/tests/test_package_layout.py +145 -0
  119. django_aiogram-4.0.0.dev0/tests/test_payloads.py +320 -0
  120. django_aiogram-4.0.0.dev0/tests/test_public_surface.py +360 -0
  121. django_aiogram-4.0.0.dev0/tests/test_redis_helpers.py +428 -0
  122. django_aiogram-4.0.0.dev0/tests/test_serializers.py +373 -0
  123. django_aiogram-4.0.0.dev0/tests/test_shutdown.py +630 -0
  124. django_aiogram-4.0.0.dev0/tests/test_start_command.py +479 -0
  125. django_aiogram-4.0.0.dev0/tests/test_throttling.py +522 -0
  126. django_aiogram-4.0.0.dev0/tests/test_unified_send.py +223 -0
  127. django_aiogram-4.0.0.dev0/tests/test_webhook.py +1207 -0
  128. django_aiogram-4.0.0.dev0/tests/test_wiki.py +413 -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,249 @@
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.py system checks E001-E046, W001-W009, I001-I002
30
+ broker/ one transport per package, and the contract they answer
31
+ producer/
32
+ client.py TelegramBot: bot/dispatcher/loop, send, send_raw
33
+ throttling.py GCRA reservations, one budget per name
34
+ consumer/
35
+ delivery.py BlpopDelivery, the one consumer
36
+ webhook.py the view an update arrives at
37
+ routers.py autodiscover
38
+ wire/
39
+ serializers.py tagged JSON, and pickle behind ALLOW_PICKLE
40
+ envelope.py what a queued payload looks like, both shapes
41
+ payloads.py summarize, redact, cap — in that order, and never lossless
42
+ eventlog/
43
+ recorder.py the bounded queue and the writer thread; no django.db here
44
+ writer.py the only module that touches the ORM
45
+ events.py the event-kind registry and the correlation id
46
+ instrumentation.py the update middleware and the storage wrapper
47
+ signals.py events_recorded, the metrics seam; imports only django.dispatch
48
+ dbrouter.py optional routing of the log to its own database
49
+ docs/wiki/ the wiki, published from main
50
+ tests/ pytest, fakeredis, no network
51
+ ```
52
+
53
+ ## Commands
54
+
55
+ ```shell
56
+ python -m pip install --upgrade pip # --group is PEP 735, pip 25.1 and up
57
+ pip install -e . --group dev
58
+ ruff check . && ruff format --check . && mypy && python -m pytest -q
59
+ python -m pytest -q --ds=tests.db_settings tests/db
60
+ ```
61
+
62
+ Those gate every pull request. `pytest` needs no Redis and no token.
63
+
64
+ The second invocation is the database-backed half. `tests/settings.py` has
65
+ `DATABASES = {}` on purpose — proving the package boots without one is part of
66
+ what the suite tests — so anything needing a database lives in `tests/db` under
67
+ `tests/db_settings.py`, and the default run ignores that directory.
68
+
69
+ CI also runs the two below — integration against a real Redis service, and the
70
+ smoke install — so a change that only passes the loop above can still fail the
71
+ build. Run them locally when you touch delivery, packaging or the public
72
+ surface:
73
+
74
+ ```shell
75
+ DJANGO_AIOGRAM_TEST_REDIS_URL=redis://localhost:6399/0 python -m pytest -m integration
76
+ bash scripts/smoke_install.sh
77
+ ```
78
+
79
+ The first needs a real server; run it when you touch delivery, serialization,
80
+ FSM persistence or connection cleanup. It flushes the database it is pointed at,
81
+ so point it at a throwaway one.
82
+
83
+ The second builds and installs the wheel; run it when you touch packaging,
84
+ Django startup or the public surface — it type-checks a consumer file against
85
+ the installed package, so a moved export fails there and nowhere else.
86
+ Packaging-only work does not need the Redis suite, and vice versa.
87
+
88
+ ## Rules that are not negotiable
89
+
90
+ - **Nothing happens at import time.** The package must import, and Django must
91
+ boot, with no token and no reachable Redis. Anything that connects or
92
+ validates credentials goes behind a property or a function. This is the defect
93
+ 2.0 existed to fix; re-introducing it breaks every consumer's test suite.
94
+ - **Importing the package stays cheap.** `__init__` resolves its exports lazily
95
+ (PEP 562) so `import django_aiogram` costs about 0.17 ms — it was ~1.4 ms
96
+ before 3.1.0, and the changelog's 0.134 ms is the same measurement on another
97
+ machine — and a disabled Django
98
+ boot never loads aiogram (~900 ms). `tests/test_lazy_init.py` pins both in
99
+ subprocesses; an eager import anywhere on the boot path fails them.
100
+ - **Every change carries a test, and the test must fail without the change.**
101
+ Revert your fix, watch the test fail, put it back. A test that passes either
102
+ way is worse than none, because it reads as coverage.
103
+ - **Values go in `extra`, not in the message.** `logger.warning('rate limited',
104
+ extra={'tg_function': name})`, never an f-string. Keys are `tg_`-prefixed so
105
+ they cannot collide with `LogRecord` attributes.
106
+ - **Never log through the root logger.** `logging.getLogger('django_aiogram')`
107
+ only; `tests/test_logging_discipline.py` enforces it, `logging.basicConfig()`
108
+ included.
109
+ - **Thread boundaries are real.** The delivery consumer runs in its own thread
110
+ while the event loop belongs to the polling thread. `create_task` across that
111
+ boundary corrupts the loop; go through `TelegramBot._schedule`.
112
+ - **Anything reaching the network can fail.** `run()` is a thread target: an
113
+ exception escaping it ends the consumer for the life of the container. Log and
114
+ continue, or retry.
115
+ - **`tests/test_public_surface.py` is a contract.** It pins the shape of
116
+ `TelegramBot` that predates 2.0 — attributes, methods and the observer
117
+ decorators. Adding to that surface is fine; moving or removing anything on it
118
+ is a breaking change and needs the changelog entry to say so.
119
+ - **`models.py` and `admin.py` import no aiogram.** Django imports the model on
120
+ every `django.setup()`, before `ready()` and regardless of `ENABLED`, and
121
+ `admin.autodiscover` imports the other on every boot of a project with the
122
+ admin installed — so a migration container pays for whatever either pulls.
123
+ `models.py` takes `django.db.models` and `django_aiogram.eventlog.events`, and nothing
124
+ else. `admin.py` needs more — `config.settings`, `eventlog.events` and
125
+ `eventlog.writer` — because a changelist reads settings and knows which alias the rows
126
+ are on. Neither takes
127
+ `client`, `serializers` or `api`. A subprocess test pins each:
128
+ `tests/test_event_log_off.py` for the model, `tests/db/test_admin.py` for the
129
+ admin.
130
+ - **`healthcheck.py` never populates the app registry.** It exists so
131
+ `python -m django_aiogram.healthcheck` can answer without `django.setup()`,
132
+ which in one measured consumer cost 17.9s of `AppConfig.ready()` against 0.01s of
133
+ probing — more than any Docker `timeout` the wiki could publish. So: no models, no
134
+ aiogram, no `django_aiogram.producer.client`, and nothing that reaches them
135
+ transitively. `tests/test_lazy_init.py` proves it with a settings module whose app
136
+ writes a file from `ready()`, and asserts the file is absent — plus a control that
137
+ the file appears under `django.setup()`, so its absence means something.
138
+ - **`recorder.py` imports no `django.db`.** Only `eventlog/writer.py` does, and the
139
+ writer thread imports it on its first *write* — not its first flush, which since
140
+ 3.1.0 are different things. That is what makes a disabled log
141
+ cost nothing and what makes `record()` legal from a coroutine — `put_nowait`
142
+ touches no I/O, so there is no `SynchronousOnlyOperation` to avoid. `EVENT_LOG_SYNC`
143
+ is the one exception and is test-only: it inserts on the calling thread, which is
144
+ also why it refuses to act inside a running loop, where the ORM is `@async_unsafe`. Since 3.1.0
145
+ the writer also runs with the log *off*, for `events_recorded` receivers alone.
146
+ Such a process writes no rows, so `EventRecorder._run` must not call
147
+ `_close_connections()` on its way out: that imports `eventlog/writer.py`, which imports
148
+ `django.db`, to close a connection nothing ever opened. It is gated on
149
+ `_touched_database`, set only where a batch is actually handed to the ORM and
150
+ **read and cleared when the writer stops** — the recorder is a process-wide
151
+ singleton, so a flag left set outlives the writer that set it and the next one
152
+ closes a connection it never opened. `tests/test_metrics_seam.py` pins both
153
+ directions, and pins them in either order: run that file reversed before
154
+ believing it.
155
+ - **The feed is append-only.** No updates, no foreign keys, no
156
+ `Meta.constraints`, no index on the JSON column. Fast pruning, shardability
157
+ and two processes writing one message's history without coordination all rest
158
+ on it; a foreign key alone breaks Django's fast-delete path.
159
+ - **`record()` may neither raise nor wait.** A log that can break delivery is
160
+ worse than no log, so everything — the settings read included — is wrapped.
161
+ - **The fact table stores identifiers, never descriptors.** A chat title belongs
162
+ in `detail` as a snapshot of the event, not in a column.
163
+ - **The token must not reach a row.** It is in the API URL, aiogram puts the URL
164
+ in its exception messages, and those messages are what an `error` column holds.
165
+ - **Interpolate `.value`, never a `(str, Enum)` member.** On newer Pythons a
166
+ member formats as its own qualified name.
167
+
168
+ ## Style
169
+
170
+ - Code, comments, docstrings and documentation are in English.
171
+ - Comment what is not obvious from the code, one line by default. Explain *why*,
172
+ not *what*.
173
+ - **Everything in `src/` has a docstring, nested closures and private helpers
174
+ included.** Those two are what ruff's `D` rules cannot see, and they are where
175
+ this package keeps its retry loop, its acknowledgement callback and its loop
176
+ thread — so `tests/test_docstring_coverage.py` walks the syntax tree and names
177
+ the definition that is missing one — and one that only restates it, where every
178
+ word of the summary is filler or a word of the name. Write the *why*, which is
179
+ the part no test can check for you. `tests/` is exempt, which
180
+ `pyproject.toml` records as `"D", # test names are the documentation`.
181
+ - Public API is annotated; the package ships `py.typed` and mypy runs on it.
182
+ - No new runtime dependencies without a reason that survives being questioned.
183
+
184
+ ## Package layout
185
+
186
+ `src/django_aiogram/` groups by what a thing *is for*, not by what it is made of.
187
+
188
+ | package | what belongs in it |
189
+ | --- | --- |
190
+ | `config/` | what a project configures, and what refuses a bad value |
191
+ | `broker/` | one transport per package; the contract they answer |
192
+ | `producer/` | the send side: the bot, the producer, the pacing |
193
+ | `consumer/` | the receive side: the queue consumer, the webhook view, router discovery |
194
+ | `wire/` | how a message becomes bytes and comes back |
195
+ | `eventlog/` | the optional table, the writer thread, the metrics seam |
196
+
197
+ **The root keeps two kinds of thing**, and nothing else:
198
+
199
+ - **What cannot move.** Django looks for `apps.py`, `models.py`, `admin.py` and
200
+ `migrations/` there — moving them costs an `app_label` on every model and a
201
+ `MIGRATION_MODULES` in every consuming project. And `python -m
202
+ django_aiogram.healthcheck` sits in a compose file, where nothing can rewrite it and no
203
+ check can see it.
204
+ - **What every cluster needs and none of them owns**: `api.py`, `exceptions.py`,
205
+ `context.py`, `_singleton.py` and `redis.py`. A package for five small modules would
206
+ add a directory and answer no question — and putting a shared one *inside* a cluster
207
+ would make every other cluster import that cluster to reach it.
208
+
209
+ Everything else moves, including paths a project wrote down. Two of those exist and are
210
+ worth knowing by name, because neither is found by an import a test would notice:
211
+ `DATABASE_ROUTERS` holds `django_aiogram.eventlog.dbrouter.TelegramEventLogRouter`, and a
212
+ project's own `urls.py` names the webhook view. `FSM_STORAGE` takes a dotted path too, but
213
+ to a class of the project's choosing rather than one of ours.
214
+
215
+ `DELIVERY` and `SERIALIZER` are *not* in that list, however much they look like it: they
216
+ hold short names — `blpop`, `json`, `pickle` — validated against an enum, so moving the
217
+ classes behind them breaks nothing a project wrote.
218
+
219
+ So a module's location is still an API decision rather than a filing decision, and a move
220
+ belongs in the changelog table and in `Upgrading.md` with the old path against the new.
221
+
222
+ Two rules that are not style:
223
+
224
+ - **`__init__.py` exports deliberately.** Every package declares `__all__`. The cluster
225
+ packages declare it *empty*: callers import from the modules, because a re-export makes
226
+ a second path to every name and the one nobody chose is the one that cannot be moved.
227
+ `tests/test_package_layout.py` fails when a package has no `__all__`.
228
+ - **A transport imports its driver lazily, never at module scope.** The base install pulls
229
+ no driver, so `import django_aiogram.broker.kafka` must not fail on a machine without
230
+ Kafka — otherwise the check that names the missing extra can never run, and the reader
231
+ gets an `ImportError` instead of `pip install "django-aiogram[kafka]"`.
232
+
233
+ ## Documentation
234
+
235
+ Wiki pages live in `docs/wiki/` and are edited in the same pull request as the
236
+ code they describe. Links are `[[Page-Name]]`, or `[[Page-Name|Link text]]`
237
+ with the page first when the label differs;
238
+ `tests/test_wiki.py` checks that every link resolves, that the sidebar lists
239
+ every page, and that the README's wiki links are not stale. Configuration
240
+ examples in the docs are executed by `tests/test_docs_examples.py` and
241
+ `tests/test_documented_recipes.py`, so a snippet that cannot run fails the build.
242
+
243
+ ## Pull requests
244
+
245
+ One reviewable change per pull request, green on `ruff`, `ruff format`, `mypy`
246
+ and `pytest`. Say why the change is needed and what failure it produces.
247
+ [CodeRabbit](https://github.com/apps/coderabbitai) reviews automatically;
248
+ answer its findings, fix what is still valid and say plainly what you skipped
249
+ and why.