domainkit 0.4.0__tar.gz → 0.6.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 (234) hide show
  1. {domainkit-0.4.0 → domainkit-0.6.0}/PKG-INFO +37 -11
  2. {domainkit-0.4.0 → domainkit-0.6.0}/README.md +30 -10
  3. {domainkit-0.4.0 → domainkit-0.6.0}/pyproject.toml +10 -1
  4. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/apps/config.py +35 -0
  5. domainkit-0.6.0/src/domainkit/broker/__init__.py +35 -0
  6. domainkit-0.6.0/src/domainkit/broker/base.py +72 -0
  7. domainkit-0.6.0/src/domainkit/broker/handler.py +81 -0
  8. domainkit-0.6.0/src/domainkit/broker/memory.py +113 -0
  9. domainkit-0.6.0/src/domainkit/broker/rabbitmq.py +228 -0
  10. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/cache/core.py +43 -12
  11. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/cache/handler.py +6 -3
  12. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/conf/__init__.py +2 -0
  13. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/conf/global_settings.py +123 -1
  14. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/__init__.py +3 -0
  15. domainkit-0.6.0/src/domainkit/contrib/audit/__init__.py +37 -0
  16. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/audit/apps.py +7 -2
  17. domainkit-0.6.0/src/domainkit/contrib/audit/management/commands/auditforget.py +41 -0
  18. domainkit-0.6.0/src/domainkit/contrib/audit/management/commands/auditseal.py +22 -0
  19. domainkit-0.6.0/src/domainkit/contrib/audit/management/commands/auditverify.py +57 -0
  20. domainkit-0.6.0/src/domainkit/contrib/audit/personal.py +161 -0
  21. domainkit-0.6.0/src/domainkit/contrib/audit/protection.py +89 -0
  22. domainkit-0.6.0/src/domainkit/contrib/audit/recorder.py +220 -0
  23. domainkit-0.6.0/src/domainkit/contrib/audit/seals.py +333 -0
  24. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/audit/settings.py +8 -0
  25. domainkit-0.6.0/src/domainkit/contrib/audit/tables.py +59 -0
  26. domainkit-0.6.0/src/domainkit/contrib/audit/tasks.py +15 -0
  27. domainkit-0.6.0/src/domainkit/contrib/auth/__init__.py +171 -0
  28. domainkit-0.6.0/src/domainkit/contrib/auth/access.py +464 -0
  29. domainkit-0.6.0/src/domainkit/contrib/auth/admin.py +641 -0
  30. domainkit-0.6.0/src/domainkit/contrib/auth/api.py +292 -0
  31. domainkit-0.6.0/src/domainkit/contrib/auth/apps.py +222 -0
  32. domainkit-0.6.0/src/domainkit/contrib/auth/authentication.py +384 -0
  33. domainkit-0.6.0/src/domainkit/contrib/auth/backends.py +202 -0
  34. domainkit-0.6.0/src/domainkit/contrib/auth/common-passwords.NOTICE +32 -0
  35. domainkit-0.6.0/src/domainkit/contrib/auth/common-passwords.txt.gz +0 -0
  36. domainkit-0.6.0/src/domainkit/contrib/auth/events.py +130 -0
  37. domainkit-0.6.0/src/domainkit/contrib/auth/exceptions.py +54 -0
  38. domainkit-0.6.0/src/domainkit/contrib/auth/hashers.py +405 -0
  39. domainkit-0.6.0/src/domainkit/contrib/auth/management/__init__.py +1 -0
  40. domainkit-0.6.0/src/domainkit/contrib/auth/management/commands/__init__.py +1 -0
  41. domainkit-0.6.0/src/domainkit/contrib/auth/management/commands/changepassword.py +70 -0
  42. domainkit-0.6.0/src/domainkit/contrib/auth/management/commands/cleartokens.py +47 -0
  43. domainkit-0.6.0/src/domainkit/contrib/auth/management/commands/createsuperuser.py +157 -0
  44. domainkit-0.6.0/src/domainkit/contrib/auth/management/commands/createtoken.py +58 -0
  45. domainkit-0.6.0/src/domainkit/contrib/auth/middleware.py +118 -0
  46. domainkit-0.6.0/src/domainkit/contrib/auth/models.py +512 -0
  47. domainkit-0.6.0/src/domainkit/contrib/auth/permissions.py +101 -0
  48. domainkit-0.6.0/src/domainkit/contrib/auth/repositories.py +329 -0
  49. domainkit-0.6.0/src/domainkit/contrib/auth/settings.py +137 -0
  50. domainkit-0.6.0/src/domainkit/contrib/auth/tables.py +277 -0
  51. domainkit-0.6.0/src/domainkit/contrib/auth/tasks.py +15 -0
  52. domainkit-0.6.0/src/domainkit/contrib/auth/tokens.py +72 -0
  53. domainkit-0.6.0/src/domainkit/contrib/auth/uow.py +31 -0
  54. domainkit-0.6.0/src/domainkit/contrib/auth/users.py +43 -0
  55. domainkit-0.6.0/src/domainkit/contrib/auth/validators.py +203 -0
  56. domainkit-0.6.0/src/domainkit/contrib/contenttypes/__init__.py +28 -0
  57. domainkit-0.6.0/src/domainkit/contrib/contenttypes/apps.py +33 -0
  58. domainkit-0.6.0/src/domainkit/contrib/contenttypes/models.py +55 -0
  59. domainkit-0.6.0/src/domainkit/contrib/contenttypes/registry.py +76 -0
  60. domainkit-0.6.0/src/domainkit/contrib/contenttypes/sync.py +45 -0
  61. domainkit-0.6.0/src/domainkit/contrib/contenttypes/tables.py +22 -0
  62. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/evidence/__init__.py +5 -1
  63. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/evidence/apps.py +16 -2
  64. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/evidence/model.py +65 -15
  65. domainkit-0.6.0/src/domainkit/contrib/evidence/settings.py +34 -0
  66. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/evidence/store.py +42 -14
  67. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/evidence/tasks.py +2 -1
  68. domainkit-0.6.0/src/domainkit/contrib/idempotency/apps.py +78 -0
  69. domainkit-0.6.0/src/domainkit/contrib/idempotency/middleware.py +343 -0
  70. domainkit-0.6.0/src/domainkit/contrib/idempotency/operations.py +91 -0
  71. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/idempotency/settings.py +6 -1
  72. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/idempotency/tables.py +10 -1
  73. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/apps.py +23 -2
  74. domainkit-0.6.0/src/domainkit/contrib/messaging/dispatch.py +316 -0
  75. domainkit-0.6.0/src/domainkit/contrib/messaging/management/commands/runworker.py +132 -0
  76. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/metrics.py +19 -0
  77. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/outbox.py +7 -1
  78. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/registry.py +39 -4
  79. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/settings.py +8 -0
  80. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/tables.py +7 -1
  81. domainkit-0.6.0/src/domainkit/contrib/messaging/worker.py +621 -0
  82. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/__init__.py +2 -1
  83. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/connections.py +6 -3
  84. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/drivers.py +53 -4
  85. domainkit-0.6.0/src/domainkit/db/errors.py +112 -0
  86. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/migrations.py +23 -4
  87. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/routing.py +9 -0
  88. domainkit-0.6.0/src/domainkit/db/snapshots.py +152 -0
  89. domainkit-0.6.0/src/domainkit/db/types.py +204 -0
  90. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/uow.py +34 -2
  91. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ddd/__init__.py +4 -0
  92. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ddd/exceptions.py +17 -0
  93. domainkit-0.6.0/src/domainkit/ddd/personal.py +46 -0
  94. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ddd/uow.py +4 -0
  95. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/http/application.py +21 -7
  96. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/http/middleware.py +58 -15
  97. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/http/ratelimit.py +71 -12
  98. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ledger/__init__.py +2 -0
  99. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ledger/base.py +17 -3
  100. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ledger/handler.py +3 -3
  101. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ledger/immudb.py +49 -9
  102. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ledger/memory.py +13 -1
  103. domainkit-0.6.0/src/domainkit/lifecycle.py +38 -0
  104. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/base.py +15 -8
  105. domainkit-0.6.0/src/domainkit/management/commands/broker.py +69 -0
  106. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/check.py +14 -6
  107. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/init.py +3 -0
  108. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/migrate.py +4 -1
  109. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/startdomain.py +5 -0
  110. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/api/routes.py-tpl +35 -7
  111. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/tests/test_api.py-tpl +44 -27
  112. domainkit-0.6.0/src/domainkit/management/templates/project/src/config/settings/base.py-tpl +27 -0
  113. domainkit-0.6.0/src/domainkit/management/templates/project/src/config/settings/production.py-tpl +23 -0
  114. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/src/config/settings/test.py-tpl +2 -0
  115. domainkit-0.6.0/src/domainkit/management/templates/project/src/config/urls.py-tpl +17 -0
  116. domainkit-0.6.0/src/domainkit/management/templates/project/tests/test_health.py-tpl +14 -0
  117. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/testing.py +75 -4
  118. domainkit-0.6.0/src/domainkit/utils/closing.py +26 -0
  119. domainkit-0.4.0/src/domainkit/contrib/audit/__init__.py +0 -16
  120. domainkit-0.4.0/src/domainkit/contrib/audit/protection.py +0 -80
  121. domainkit-0.4.0/src/domainkit/contrib/audit/recorder.py +0 -133
  122. domainkit-0.4.0/src/domainkit/contrib/audit/tables.py +0 -24
  123. domainkit-0.4.0/src/domainkit/contrib/evidence/settings.py +0 -16
  124. domainkit-0.4.0/src/domainkit/contrib/idempotency/apps.py +0 -29
  125. domainkit-0.4.0/src/domainkit/contrib/idempotency/middleware.py +0 -238
  126. domainkit-0.4.0/src/domainkit/contrib/messaging/management/commands/runworker.py +0 -75
  127. domainkit-0.4.0/src/domainkit/contrib/messaging/worker.py +0 -336
  128. domainkit-0.4.0/src/domainkit/db/types.py +0 -39
  129. domainkit-0.4.0/src/domainkit/management/templates/project/src/config/settings/base.py-tpl +0 -13
  130. domainkit-0.4.0/src/domainkit/management/templates/project/src/config/settings/production.py-tpl +0 -16
  131. domainkit-0.4.0/src/domainkit/management/templates/project/src/config/urls.py-tpl +0 -8
  132. domainkit-0.4.0/src/domainkit/management/templates/project/tests/test_health.py-tpl +0 -8
  133. {domainkit-0.4.0 → domainkit-0.6.0}/LICENSE +0 -0
  134. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/__init__.py +0 -0
  135. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/apps/__init__.py +0 -0
  136. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/apps/registry.py +0 -0
  137. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/architecture.py +0 -0
  138. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/cache/__init__.py +0 -0
  139. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/cache/base.py +0 -0
  140. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/cache/decorators.py +0 -0
  141. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/cache/memory.py +0 -0
  142. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/cache/metrics.py +0 -0
  143. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/cache/redis.py +0 -0
  144. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/conf/env.py +0 -0
  145. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/conf/lazy.py +0 -0
  146. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/context.py +0 -0
  147. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/audit/management/__init__.py +0 -0
  148. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/audit/management/commands/__init__.py +0 -0
  149. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/audit/management/commands/auditlog.py +0 -0
  150. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/evidence/management/__init__.py +0 -0
  151. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/evidence/management/commands/__init__.py +0 -0
  152. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/evidence/management/commands/evidence.py +0 -0
  153. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/idempotency/__init__.py +0 -0
  154. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/idempotency/tasks.py +0 -0
  155. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/__init__.py +0 -0
  156. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/admin.py +0 -0
  157. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/management/__init__.py +0 -0
  158. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/management/commands/__init__.py +0 -0
  159. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/contrib/messaging/management/commands/messages.py +0 -0
  160. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/migration_env/__init__.py +0 -0
  161. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/migration_env/env.py +0 -0
  162. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/migration_env/script.py.mako +0 -0
  163. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/orm.py +0 -0
  164. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/partitioning.py +0 -0
  165. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/db/repository.py +0 -0
  166. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ddd/_dataclass.py +0 -0
  167. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ddd/events.py +0 -0
  168. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ddd/messages.py +0 -0
  169. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ddd/model.py +0 -0
  170. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ddd/pagination.py +0 -0
  171. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/ddd/repository.py +0 -0
  172. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/deprecation.py +0 -0
  173. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/exceptions.py +0 -0
  174. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/http/__init__.py +0 -0
  175. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/http/errors.py +0 -0
  176. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/http/health.py +0 -0
  177. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/http/metrics.py +0 -0
  178. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/http/pagination.py +0 -0
  179. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/log.py +0 -0
  180. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/__init__.py +0 -0
  181. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/__init__.py +0 -0
  182. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/cache.py +0 -0
  183. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/makemigrations.py +0 -0
  184. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/partitions.py +0 -0
  185. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/runserver.py +0 -0
  186. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/shell.py +0 -0
  187. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/commands/showmigrations.py +0 -0
  188. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/__init__.py-tpl +0 -0
  189. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/api/__init__.py-tpl +0 -0
  190. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/api/dependencies.py-tpl +0 -0
  191. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/api/schemas.py-tpl +0 -0
  192. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/application/__init__.py-tpl +0 -0
  193. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/application/commands.py-tpl +0 -0
  194. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/application/handlers.py-tpl +0 -0
  195. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/application/services.py-tpl +0 -0
  196. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/application/unit_of_work.py-tpl +0 -0
  197. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/apps.py-tpl +0 -0
  198. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/domain/__init__.py-tpl +0 -0
  199. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/domain/events.py-tpl +0 -0
  200. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/domain/exceptions.py-tpl +0 -0
  201. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/domain/model.py-tpl +0 -0
  202. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/domain/repositories.py-tpl +0 -0
  203. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/infrastructure/__init__.py-tpl +0 -0
  204. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/infrastructure/orm.py-tpl +0 -0
  205. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/infrastructure/repositories.py-tpl +0 -0
  206. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/infrastructure/unit_of_work.py-tpl +0 -0
  207. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/management/__init__.py-tpl +0 -0
  208. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/management/commands/__init__.py-tpl +0 -0
  209. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/management/commands/{{ aggregates }}.py-tpl +0 -0
  210. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/package/settings.py-tpl +0 -0
  211. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/tests/__init__.py-tpl +0 -0
  212. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/tests/fakes.py-tpl +0 -0
  213. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/tests/test_model.py-tpl +0 -0
  214. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/tests/test_services.py-tpl +0 -0
  215. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/domain/tests/test_settings.py-tpl +0 -0
  216. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/dot-env-tpl +0 -0
  217. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/dot-gitignore-tpl +0 -0
  218. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/manage.py-tpl +0 -0
  219. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/pyproject.toml-tpl +0 -0
  220. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/src/config/__init__.py-tpl +0 -0
  221. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/src/config/asgi.py-tpl +0 -0
  222. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/src/config/settings/__init__.py-tpl +0 -0
  223. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/src/config/settings/local.py-tpl +0 -0
  224. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/src/domains/__init__.py-tpl +0 -0
  225. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/management/templates/project/tests/__init__.py-tpl +0 -0
  226. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/py.typed +0 -0
  227. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/reporting.py +0 -0
  228. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/tracing.py +0 -0
  229. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/utils/__init__.py +0 -0
  230. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/utils/formatting.py +0 -0
  231. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/utils/module_loading.py +0 -0
  232. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/utils/rates.py +0 -0
  233. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/utils/readiness.py +0 -0
  234. {domainkit-0.4.0 → domainkit-0.6.0}/src/domainkit/utils/text.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: domainkit
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: A Django-style framework for domain-driven FastAPI services.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -16,15 +16,20 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
18
18
  Classifier: Typing :: Typed
19
+ Provides-Extra: argon2
19
20
  Provides-Extra: immudb
21
+ Provides-Extra: jwt
20
22
  Provides-Extra: metrics
21
23
  Provides-Extra: postgres
24
+ Provides-Extra: rabbitmq
22
25
  Provides-Extra: redis
23
26
  Provides-Extra: sqlite
24
27
  Provides-Extra: testing
25
28
  Provides-Extra: tracing
29
+ Requires-Dist: aio-pika (>=10.1.0,<11.0.0) ; extra == "rabbitmq"
26
30
  Requires-Dist: aiosqlite (>=0.22.1,<0.23.0) ; extra == "sqlite"
27
31
  Requires-Dist: alembic (>=1.20.0,<2.0.0)
32
+ Requires-Dist: argon2-cffi (>=25.1.0,<26.0.0) ; extra == "argon2"
28
33
  Requires-Dist: asyncpg (>=0.31.0,<0.32.0) ; extra == "postgres"
29
34
  Requires-Dist: fastapi (>=0.141.1,<0.142.0)
30
35
  Requires-Dist: httpx2 (>=2.13.0,<3.0.0) ; extra == "testing"
@@ -32,6 +37,7 @@ Requires-Dist: immudb-py (>=1.5.0,<1.6.0) ; extra == "immudb"
32
37
  Requires-Dist: opentelemetry-api (>=1.45.1,<2.0.0) ; extra == "tracing"
33
38
  Requires-Dist: prometheus-client (>=0.26.0,<0.27.0) ; extra == "metrics"
34
39
  Requires-Dist: pydantic (>=2.14.1,<3.0.0)
40
+ Requires-Dist: pyjwt[crypto] (>=2.10.1,<3.0.0) ; extra == "jwt"
35
41
  Requires-Dist: pytest (>=9.1.1,<10.0.0) ; extra == "testing"
36
42
  Requires-Dist: python-dotenv (>=1.2.4,<2.0.0)
37
43
  Requires-Dist: redis (>=8.1.0,<9.0.0) ; extra == "redis"
@@ -54,11 +60,17 @@ A Django-style framework for domain-driven FastAPI services. It provides:
54
60
  PostgreSQL range partitioning, and immudb ledgers for evidence
55
61
  - caches in memory or in Redis and Valkey, with stampede protection, and
56
62
  rate limits shared by every replica
63
+ - background work through a transactional outbox, run by polling workers or
64
+ through RabbitMQ, with queues for separate worker pools
65
+ - users, groups, and permissions per content type, as in Django's
66
+ `contrib.auth`: password hashing and rules, backends, object permissions,
67
+ and a swappable user model; token or JWT authentication, and route rules
68
+ as in Django REST framework
57
69
  - optional apps for reliable messaging, audit trails, idempotent requests,
58
70
  and tamper-evident evidence
59
71
 
60
72
  ```bash
61
- pip install "domainkit[postgres]" # extras: postgres, sqlite, immudb, redis, metrics, tracing, testing
73
+ pip install "domainkit[postgres]" # extras: postgres, sqlite, argon2, jwt, immudb, redis, rabbitmq, metrics, tracing, testing
62
74
  ```
63
75
 
64
76
  ## Documentation
@@ -69,7 +81,10 @@ pip install "domainkit[postgres]" # extras: postgres, sqlite, immudb, redis,
69
81
  sharding, partitioning, and ledgers
70
82
  - [docs/cache.md](https://github.com/karimmattar/domainkit/blob/main/docs/cache.md): caches in memory, Redis, and Valkey,
71
83
  and shared rate limits
72
- - [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging, audit, idempotency, and evidence
84
+ - [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging (and workers on RabbitMQ),
85
+ audit, idempotency, and evidence
86
+ - [docs/auth.md](https://github.com/karimmattar/domainkit/blob/main/docs/auth.md): users, groups, permissions, content
87
+ types, passwords, backends, tokens, route rules, and the admin API
73
88
  - [docs/versioning.md](https://github.com/karimmattar/domainkit/blob/main/docs/versioning.md): compatibility, deprecations, and releases
74
89
  - [docs/adr/](https://github.com/karimmattar/domainkit/tree/main/docs/adr): the decisions behind the design
75
90
  - [CHANGELOG.md](https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md) and [SECURITY.md](https://github.com/karimmattar/domainkit/blob/main/SECURITY.md)
@@ -84,11 +99,17 @@ cd billing
84
99
  python manage.py check
85
100
  python manage.py startdomain invoices --aggregate Invoice # prints how to install it
86
101
  python manage.py makemigrations -m "create invoices" && python manage.py migrate
102
+ python manage.py createsuperuser
87
103
  pytest
88
104
  python manage.py runserver # http://127.0.0.1:8000/docs
89
105
  ```
90
106
 
91
- `domainkit init` creates a project that passes `check` and its tests as it is:
107
+ `domainkit init` creates a project that passes `check` and its tests as it is.
108
+ Users, groups, and permissions are installed, requests are authenticated,
109
+ and every route must say who may call it: generated domains need their
110
+ model permissions, and `/auth` and `/admin/auth` log users in and manage
111
+ them ([docs/auth.md](https://github.com/karimmattar/domainkit/blob/main/docs/auth.md)).
112
+
92
113
 
93
114
  ```text
94
115
  billing/
@@ -105,7 +126,10 @@ billing/
105
126
 
106
127
  The production settings read `BILLING_API_ALLOWED_HOSTS` and
107
128
  `BILLING_API_DATABASE_URL` from the environment, and the deployment checks
108
- refuse to start without them, or without verified TLS to the database.
129
+ refuse to start without them, or without verified TLS to the database. They
130
+ also turn the API docs off, send HSTS for a year, and limit each caller to
131
+ 300 requests a minute; `BILLING_API_API_DOCS`,
132
+ `BILLING_API_SECURE_HSTS_SECONDS`, and `BILLING_API_RATE_LIMIT` change that.
109
133
 
110
134
  ## Developing domainkit
111
135
 
@@ -118,9 +142,9 @@ poetry run pytest --cov # random order; coverage gate in pyprojec
118
142
  poetry run mutmut run "domainkit.http*" && poetry run mutmut results
119
143
  ```
120
144
 
121
- Without servers, the database tests use SQLite, the Redis tests use an
122
- in-memory fake (fakeredis), and the tests that need PostgreSQL, immudb, or
123
- a real Redis or Valkey are skipped. Point them at throwaway servers to run
145
+ Without servers, the database tests use SQLite, the Redis and RabbitMQ
146
+ tests use in-memory fakes, and the tests that need PostgreSQL, immudb, or a
147
+ real Redis, Valkey, or RabbitMQ are skipped. Point them at throwaway servers to run
124
148
  them; each test cleans up after itself:
125
149
 
126
150
  ```bash
@@ -132,8 +156,9 @@ DOMAINKIT_TEST_REDIS_URL=redis://127.0.0.1:56379/0 \
132
156
  poetry run pytest
133
157
  ```
134
158
 
135
- The other variables (immudb, Redis over TLS, Valkey) are listed at the top
136
- of `domainkit_tests/test_immudb.py` and `test_redis.py`. Each run must cover
159
+ The other variables (immudb, Redis over TLS, Valkey, RabbitMQ) are listed
160
+ at the top of `domainkit_tests/test_immudb.py`, `test_redis.py`, and
161
+ `test_rabbitmq.py`. Each run must cover
137
162
  98%; CI also combines the runs with and without servers, which must cover
138
163
  100%. To check that locally, run `pytest --cov` once without and once with
139
164
  the servers, each with its own `COVERAGE_FILE`, then `coverage combine` and
@@ -144,7 +169,8 @@ CI (`.github/workflows/ci.yml`) runs on every push and pull request:
144
169
  - lint and strict typing
145
170
  - the tests on Python 3.13 and 3.14
146
171
  - the tests again against PostgreSQL 18, immudb with TLS and a signing key,
147
- Redis 8 (plain and TLS), and Valkey 9; no test may be skipped there
172
+ Redis 8 (plain and TLS), Valkey 9, and RabbitMQ 4 (plain and TLS); no test
173
+ may be skipped there
148
174
  - coverage of both runs combined, which must be 100%
149
175
  - a wheel build, installed and imported in a clean environment
150
176
  - pip-audit, and a gitleaks scan of the whole history
@@ -9,11 +9,17 @@ A Django-style framework for domain-driven FastAPI services. It provides:
9
9
  PostgreSQL range partitioning, and immudb ledgers for evidence
10
10
  - caches in memory or in Redis and Valkey, with stampede protection, and
11
11
  rate limits shared by every replica
12
+ - background work through a transactional outbox, run by polling workers or
13
+ through RabbitMQ, with queues for separate worker pools
14
+ - users, groups, and permissions per content type, as in Django's
15
+ `contrib.auth`: password hashing and rules, backends, object permissions,
16
+ and a swappable user model; token or JWT authentication, and route rules
17
+ as in Django REST framework
12
18
  - optional apps for reliable messaging, audit trails, idempotent requests,
13
19
  and tamper-evident evidence
14
20
 
15
21
  ```bash
16
- pip install "domainkit[postgres]" # extras: postgres, sqlite, immudb, redis, metrics, tracing, testing
22
+ pip install "domainkit[postgres]" # extras: postgres, sqlite, argon2, jwt, immudb, redis, rabbitmq, metrics, tracing, testing
17
23
  ```
18
24
 
19
25
  ## Documentation
@@ -24,7 +30,10 @@ pip install "domainkit[postgres]" # extras: postgres, sqlite, immudb, redis,
24
30
  sharding, partitioning, and ledgers
25
31
  - [docs/cache.md](https://github.com/karimmattar/domainkit/blob/main/docs/cache.md): caches in memory, Redis, and Valkey,
26
32
  and shared rate limits
27
- - [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging, audit, idempotency, and evidence
33
+ - [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging (and workers on RabbitMQ),
34
+ audit, idempotency, and evidence
35
+ - [docs/auth.md](https://github.com/karimmattar/domainkit/blob/main/docs/auth.md): users, groups, permissions, content
36
+ types, passwords, backends, tokens, route rules, and the admin API
28
37
  - [docs/versioning.md](https://github.com/karimmattar/domainkit/blob/main/docs/versioning.md): compatibility, deprecations, and releases
29
38
  - [docs/adr/](https://github.com/karimmattar/domainkit/tree/main/docs/adr): the decisions behind the design
30
39
  - [CHANGELOG.md](https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md) and [SECURITY.md](https://github.com/karimmattar/domainkit/blob/main/SECURITY.md)
@@ -39,11 +48,17 @@ cd billing
39
48
  python manage.py check
40
49
  python manage.py startdomain invoices --aggregate Invoice # prints how to install it
41
50
  python manage.py makemigrations -m "create invoices" && python manage.py migrate
51
+ python manage.py createsuperuser
42
52
  pytest
43
53
  python manage.py runserver # http://127.0.0.1:8000/docs
44
54
  ```
45
55
 
46
- `domainkit init` creates a project that passes `check` and its tests as it is:
56
+ `domainkit init` creates a project that passes `check` and its tests as it is.
57
+ Users, groups, and permissions are installed, requests are authenticated,
58
+ and every route must say who may call it: generated domains need their
59
+ model permissions, and `/auth` and `/admin/auth` log users in and manage
60
+ them ([docs/auth.md](https://github.com/karimmattar/domainkit/blob/main/docs/auth.md)).
61
+
47
62
 
48
63
  ```text
49
64
  billing/
@@ -60,7 +75,10 @@ billing/
60
75
 
61
76
  The production settings read `BILLING_API_ALLOWED_HOSTS` and
62
77
  `BILLING_API_DATABASE_URL` from the environment, and the deployment checks
63
- refuse to start without them, or without verified TLS to the database.
78
+ refuse to start without them, or without verified TLS to the database. They
79
+ also turn the API docs off, send HSTS for a year, and limit each caller to
80
+ 300 requests a minute; `BILLING_API_API_DOCS`,
81
+ `BILLING_API_SECURE_HSTS_SECONDS`, and `BILLING_API_RATE_LIMIT` change that.
64
82
 
65
83
  ## Developing domainkit
66
84
 
@@ -73,9 +91,9 @@ poetry run pytest --cov # random order; coverage gate in pyprojec
73
91
  poetry run mutmut run "domainkit.http*" && poetry run mutmut results
74
92
  ```
75
93
 
76
- Without servers, the database tests use SQLite, the Redis tests use an
77
- in-memory fake (fakeredis), and the tests that need PostgreSQL, immudb, or
78
- a real Redis or Valkey are skipped. Point them at throwaway servers to run
94
+ Without servers, the database tests use SQLite, the Redis and RabbitMQ
95
+ tests use in-memory fakes, and the tests that need PostgreSQL, immudb, or a
96
+ real Redis, Valkey, or RabbitMQ are skipped. Point them at throwaway servers to run
79
97
  them; each test cleans up after itself:
80
98
 
81
99
  ```bash
@@ -87,8 +105,9 @@ DOMAINKIT_TEST_REDIS_URL=redis://127.0.0.1:56379/0 \
87
105
  poetry run pytest
88
106
  ```
89
107
 
90
- The other variables (immudb, Redis over TLS, Valkey) are listed at the top
91
- of `domainkit_tests/test_immudb.py` and `test_redis.py`. Each run must cover
108
+ The other variables (immudb, Redis over TLS, Valkey, RabbitMQ) are listed
109
+ at the top of `domainkit_tests/test_immudb.py`, `test_redis.py`, and
110
+ `test_rabbitmq.py`. Each run must cover
92
111
  98%; CI also combines the runs with and without servers, which must cover
93
112
  100%. To check that locally, run `pytest --cov` once without and once with
94
113
  the servers, each with its own `COVERAGE_FILE`, then `coverage combine` and
@@ -99,7 +118,8 @@ CI (`.github/workflows/ci.yml`) runs on every push and pull request:
99
118
  - lint and strict typing
100
119
  - the tests on Python 3.13 and 3.14
101
120
  - the tests again against PostgreSQL 18, immudb with TLS and a signing key,
102
- Redis 8 (plain and TLS), and Valkey 9; no test may be skipped there
121
+ Redis 8 (plain and TLS), Valkey 9, and RabbitMQ 4 (plain and TLS); no test
122
+ may be skipped there
103
123
  - coverage of both runs combined, which must be 100%
104
124
  - a wheel build, installed and imported in a clean environment
105
125
  - pip-audit, and a gitleaks scan of the whole history
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "domainkit"
3
- version = "0.4.0"
3
+ version = "0.6.0"
4
4
  description = "A Django-style framework for domain-driven FastAPI services."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -35,8 +35,15 @@ postgres = ["asyncpg (>=0.31.0,<0.32.0)"]
35
35
  sqlite = ["aiosqlite (>=0.22.1,<0.23.0)"]
36
36
  metrics = ["prometheus-client (>=0.26.0,<0.27.0)"]
37
37
  tracing = ["opentelemetry-api (>=1.45.1,<2.0.0)"]
38
+ # Argon2id password hashing for domainkit.contrib.auth (PBKDF2 needs nothing).
39
+ argon2 = ["argon2-cffi (>=25.1.0,<26.0.0)"]
40
+ # JSON Web Tokens for domainkit.contrib.auth (JWTAuthentication), with the
41
+ # RSA, ECDSA, and EdDSA algorithms.
42
+ jwt = ["pyjwt[crypto] (>=2.10.1,<3.0.0)"]
38
43
  # Ledgers for evidence (WORM): immudb's SDK, with its verified operations.
39
44
  immudb = ["immudb-py (>=1.5.0,<1.6.0)"]
45
+ # A message broker for the worker: RabbitMQ, through aio-pika.
46
+ rabbitmq = ["aio-pika (>=10.1.0,<11.0.0)"]
40
47
  # Shared caches and rate limits: Redis or Valkey, through redis-py.
41
48
  redis = ["redis (>=8.1.0,<9.0.0)"]
42
49
  # The domainkit.testing pytest plugin; Starlette's TestClient needs httpx2.
@@ -72,6 +79,8 @@ dev = [
72
79
  "httpx2 (>=2.13.0,<3.0.0)",
73
80
  # An in-memory Redis, with Lua, so the Redis tests also run without a server.
74
81
  "fakeredis[lua] (>=2.38.0,<3.0.0)",
82
+ # Throwaway certificates for the TLS tests.
83
+ "cryptography (>=50.0.2,<51.0.0)",
75
84
  # In-memory span exporter for the tracing tests.
76
85
  "opentelemetry-sdk (>=1.45.1,<2.0.0)",
77
86
  # Random test order, property-based tests, mutation testing.
@@ -11,6 +11,10 @@ from pydantic import BaseModel
11
11
  from domainkit.exceptions import ImproperlyConfigured
12
12
 
13
13
  if TYPE_CHECKING:
14
+ from fastapi import FastAPI
15
+ from sqlalchemy import Table
16
+ from sqlalchemy.ext.asyncio import AsyncConnection
17
+
14
18
  from domainkit.db.routing import DatabaseRouter
15
19
  from domainkit.ddd import DeferredTask, DomainEvent, UnitOfWork
16
20
 
@@ -101,6 +105,37 @@ class DomainConfig:
101
105
  ) -> None:
102
106
  """Add rows to a committing SQL unit of work (outbox, audit trail)."""
103
107
 
108
+ async def committed(self, uow: "UnitOfWork") -> None:
109
+ """Follow up after a SQL unit of work committed (publish its messages).
110
+
111
+ The data is already committed: a failure here is logged and reported,
112
+ and the unit of work carries on.
113
+ """
114
+
115
+ async def migrated(self, connection: "AsyncConnection", database: str) -> None:
116
+ """Fill alias ``database`` after ``migrate`` upgraded it (``post_migrate``).
117
+
118
+ Runs for every installed domain, in order, in one transaction on the
119
+ schema owner's ``connection``; check ``database`` before writing.
120
+ Content types and permissions are created this way.
121
+ """
122
+
123
+ def check_application(self, app: "FastAPI") -> list[str]:
124
+ """Problems with the built application, such as routes no rule guards.
125
+
126
+ ``check`` reports them, and staging and production refuse to start
127
+ with any; elsewhere the application starts and logs them.
128
+ """
129
+ return []
130
+
131
+ def holds_table(self, table: "Table") -> bool:
132
+ """Whether ``table``, tagged with this domain, is in use now.
133
+
134
+ A table that is not is neither migrated nor given a content type,
135
+ such as the default user table while ``AUTH.USER_MODEL`` swaps it.
136
+ """
137
+ return True
138
+
104
139
  def databases(self, router: "DatabaseRouter") -> frozenset[str]:
105
140
  """The targets that hold this domain's tables (see ``DATABASE_ROUTING``).
106
141
 
@@ -0,0 +1,35 @@
1
+ """Message brokers: durable work queues for the worker (RabbitMQ, or in memory).
2
+
3
+ Configure them in ``BROKERS`` and point ``MESSAGING.BROKER`` at one; the
4
+ worker then takes its work from the broker instead of polling the outbox
5
+ table. The outbox stays the record of every message (see ADR 0014)::
6
+
7
+ BROKERS = {"default": {"URL": "amqps://billing:pw@mq.internal/billing",
8
+ "PREFIX": "billing"}}
9
+ MESSAGING = {"BROKER": "default"}
10
+ """
11
+
12
+ from domainkit.broker.base import (
13
+ Broker,
14
+ BrokerError,
15
+ BrokerRefused,
16
+ BrokerUnavailable,
17
+ Delivery,
18
+ Handler,
19
+ Subscription,
20
+ )
21
+ from domainkit.broker.handler import BrokerHandler, brokers
22
+ from domainkit.broker.memory import MemoryBroker
23
+
24
+ __all__ = [
25
+ "Broker",
26
+ "BrokerError",
27
+ "BrokerHandler",
28
+ "BrokerRefused",
29
+ "BrokerUnavailable",
30
+ "Delivery",
31
+ "Handler",
32
+ "MemoryBroker",
33
+ "Subscription",
34
+ "brokers",
35
+ ]
@@ -0,0 +1,72 @@
1
+ """The broker port: work queues that hand each message to one consumer.
2
+
3
+ A message published to a queue goes to one of the queue's consumers, which
4
+ acknowledges it once its work is recorded; a message whose consumer goes
5
+ away before that goes to another. Adapters translate their failures into
6
+ ``BrokerUnavailable`` (unreachable, or no confirmation in time) and
7
+ ``BrokerRefused`` (the broker said no, as for a message no queue takes).
8
+ """
9
+
10
+ from abc import ABC, abstractmethod
11
+ from collections.abc import Awaitable, Callable, Iterable
12
+ from dataclasses import dataclass
13
+
14
+
15
+ class BrokerError(Exception):
16
+ """Base class of broker errors."""
17
+
18
+
19
+ class BrokerUnavailable(BrokerError):
20
+ """The broker could not be reached, or did not confirm in time."""
21
+
22
+
23
+ class BrokerRefused(BrokerError):
24
+ """The broker refused a message (no queue took it, or it said no)."""
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class Delivery:
29
+ """One message handed to a consumer; ``ack`` or ``requeue`` it exactly once."""
30
+
31
+ queue: str
32
+ body: bytes
33
+ message_id: str
34
+ ack: Callable[[], Awaitable[None]]
35
+ """Done with it: the broker forgets the message."""
36
+ requeue: Callable[[], Awaitable[None]]
37
+ """Not now: the broker hands the message out again."""
38
+
39
+
40
+ type Handler = Callable[[Delivery], Awaitable[None]]
41
+
42
+
43
+ class Subscription(ABC):
44
+ """Deliveries flowing from queues to a handler."""
45
+
46
+ @abstractmethod
47
+ async def cancel(self) -> None:
48
+ """Stop new deliveries; deliveries being handled carry on."""
49
+
50
+
51
+ class Broker(ABC):
52
+ """Durable work queues. Queue names are the application's; adapters prefix them."""
53
+
54
+ @abstractmethod
55
+ async def declare(self, queues: Iterable[str]) -> None:
56
+ """Create the queues if they do not exist yet."""
57
+
58
+ @abstractmethod
59
+ async def publish(self, queue: str, body: bytes, *, message_id: str) -> None:
60
+ """Hand ``body`` to ``queue``; return once the broker has it safely."""
61
+
62
+ @abstractmethod
63
+ async def consume(
64
+ self, queues: Iterable[str], prefetch: int, handler: Handler
65
+ ) -> Subscription:
66
+ """Call ``handler`` for messages of ``queues``, at most ``prefetch`` at once."""
67
+
68
+ async def check(self) -> None: # noqa: B027 - optional; most brokers have it.
69
+ """Raise if the broker cannot be reached."""
70
+
71
+ async def close(self) -> None: # noqa: B027 - optional, for connections.
72
+ """Release connections; the broker is not used afterwards."""
@@ -0,0 +1,81 @@
1
+ """Brokers per ``BROKERS`` alias (the broker counterpart of ``caches``).
2
+
3
+ AMQP connections, and the in-memory broker's queues, belong to the event
4
+ loop that opened them, so each running loop gets its own brokers.
5
+ """
6
+
7
+ import asyncio
8
+ from collections.abc import Callable
9
+ from typing import Final
10
+ from weakref import WeakKeyDictionary
11
+
12
+ from domainkit.broker.base import Broker
13
+ from domainkit.broker.memory import MemoryBroker
14
+ from domainkit.conf import BrokerConfig, settings
15
+ from domainkit.exceptions import ImproperlyConfigured
16
+ from domainkit.utils.closing import close_each
17
+
18
+ type BrokerFactory = Callable[[BrokerConfig], Broker]
19
+
20
+
21
+ def rabbitmq_broker(config: BrokerConfig) -> Broker:
22
+ """Connect to RabbitMQ; needs ``domainkit[rabbitmq]``."""
23
+ try:
24
+ from domainkit.broker.rabbitmq import RabbitMQBroker
25
+ except ImportError as exc: # pragma: no cover - only without the [rabbitmq] extra.
26
+ raise ImproperlyConfigured(
27
+ "RabbitMQ brokers need the rabbitmq extra: "
28
+ "pip install 'domainkit[rabbitmq]'."
29
+ ) from exc
30
+ return RabbitMQBroker(config)
31
+
32
+
33
+ class BrokerHandler:
34
+ """Create each configured broker on first use, per event loop."""
35
+
36
+ def __init__(self, connect: BrokerFactory = rabbitmq_broker) -> None:
37
+ """Use ``connect`` to build RabbitMQ brokers (tests may replace it)."""
38
+ self._connect = connect
39
+ self._brokers: WeakKeyDictionary[
40
+ asyncio.AbstractEventLoop, dict[str, tuple[BrokerConfig, Broker]]
41
+ ] = WeakKeyDictionary()
42
+ self._replaced: WeakKeyDictionary[asyncio.AbstractEventLoop, list[Broker]] = (
43
+ WeakKeyDictionary()
44
+ )
45
+
46
+ def config(self, alias: str) -> BrokerConfig:
47
+ """Return the ``BROKERS`` entry of ``alias``."""
48
+ try:
49
+ return settings.BROKERS[alias]
50
+ except KeyError:
51
+ raise ImproperlyConfigured(f"BROKERS has no {alias!r} alias.") from None
52
+
53
+ def __getitem__(self, alias: str) -> Broker:
54
+ """The running loop's broker for ``alias``; new if its settings changed."""
55
+ config = self.config(alias)
56
+ try:
57
+ loop = asyncio.get_running_loop()
58
+ except RuntimeError:
59
+ raise RuntimeError(
60
+ f"brokers[{alias!r}] needs a running event loop: connections "
61
+ "belong to one, so look brokers up inside async code."
62
+ ) from None
63
+ brokers = self._brokers.setdefault(loop, {})
64
+ current = brokers.get(alias)
65
+ if current is not None and current[0] == config:
66
+ return current[1]
67
+ if current is not None: # override_settings changed it; close it later.
68
+ self._replaced.setdefault(loop, []).append(current[1])
69
+ broker = MemoryBroker() if config.scheme == "memory" else self._connect(config)
70
+ brokers[alias] = (config, broker)
71
+ return broker
72
+
73
+ async def close_all(self) -> None:
74
+ """Close the running loop's brokers; the next use opens new ones."""
75
+ loop = asyncio.get_running_loop()
76
+ current = [broker for _, broker in self._brokers.pop(loop, {}).values()]
77
+ await close_each([*current, *self._replaced.pop(loop, [])], Broker.close)
78
+
79
+
80
+ brokers: Final = BrokerHandler()
81
+ """The process-wide brokers: ``brokers["default"]``."""
@@ -0,0 +1,113 @@
1
+ """An in-process broker (``memory://``) for tests and development.
2
+
3
+ Its queues live in one process and one event loop: the relay and the
4
+ consumers of a single ``runworker`` share them, but nothing else does, so
5
+ staging and production refuse it.
6
+ """
7
+
8
+ import asyncio
9
+ from collections.abc import Iterable
10
+
11
+ from domainkit.broker.base import Broker, BrokerRefused, Delivery, Handler, Subscription
12
+
13
+ type _Message = tuple[bytes, str]
14
+
15
+
16
+ class _MemorySubscription(Subscription):
17
+ """The pumps that hand queued messages to a handler."""
18
+
19
+ def __init__(self, pumps: list[asyncio.Task[None]]) -> None:
20
+ """Own ``pumps``."""
21
+ self._pumps = pumps
22
+
23
+ async def cancel(self) -> None:
24
+ """Stop the pumps; handlers already running finish."""
25
+ for pump in self._pumps:
26
+ pump.cancel()
27
+ await asyncio.gather(*self._pumps, return_exceptions=True)
28
+
29
+
30
+ class MemoryBroker(Broker):
31
+ """Queues of this process: one consumer gets each message, as in RabbitMQ."""
32
+
33
+ def __init__(self) -> None:
34
+ """Start without queues."""
35
+ self._queues: dict[str, asyncio.Queue[_Message]] = {}
36
+ self._handling: set[asyncio.Task[None]] = set()
37
+
38
+ async def declare(self, queues: Iterable[str]) -> None:
39
+ """Create the missing queues."""
40
+ for queue in queues:
41
+ self._queues.setdefault(queue, asyncio.Queue())
42
+
43
+ async def publish(self, queue: str, body: bytes, *, message_id: str) -> None:
44
+ """Append to ``queue``; refuse queues nobody declared, as RabbitMQ does."""
45
+ if queue not in self._queues:
46
+ raise BrokerRefused(f"No queue {queue!r} takes the message.")
47
+ self._queues[queue].put_nowait((body, message_id))
48
+
49
+ def pending(self, queue: str) -> int:
50
+ """Messages waiting in ``queue`` (tests); 0 for a queue never declared."""
51
+ return self._queues[queue].qsize() if queue in self._queues else 0
52
+
53
+ async def consume(
54
+ self, queues: Iterable[str], prefetch: int, handler: Handler
55
+ ) -> Subscription:
56
+ """Pump each queue into ``handler``, at most ``prefetch`` at once."""
57
+ queues = list(queues)
58
+ await self.declare(queues)
59
+ slots = asyncio.Semaphore(prefetch)
60
+ pumps = [
61
+ asyncio.create_task(self._pump(queue, slots, handler)) for queue in queues
62
+ ]
63
+ return _MemorySubscription(pumps)
64
+
65
+ async def _pump(
66
+ self, queue: str, slots: asyncio.Semaphore, handler: Handler
67
+ ) -> None:
68
+ """Hand out ``queue``'s messages while a slot is free."""
69
+ source = self._queues[queue]
70
+ while True:
71
+ await slots.acquire()
72
+ try:
73
+ body, message_id = await source.get()
74
+ except asyncio.CancelledError:
75
+ slots.release()
76
+ raise
77
+ task = asyncio.create_task(
78
+ self._deliver(queue, body, message_id, slots, handler)
79
+ )
80
+ self._handling.add(task)
81
+ task.add_done_callback(self._handling.discard)
82
+
83
+ async def _deliver(
84
+ self,
85
+ queue: str,
86
+ body: bytes,
87
+ message_id: str,
88
+ slots: asyncio.Semaphore,
89
+ handler: Handler,
90
+ ) -> None:
91
+ """Run ``handler``; the message holds its slot until it is answered.
92
+
93
+ As in RabbitMQ, a handler may answer after it returns, and only the
94
+ first answer counts.
95
+ """
96
+ answered = False
97
+
98
+ def settle() -> bool:
99
+ nonlocal answered
100
+ if answered:
101
+ return False
102
+ answered = True
103
+ slots.release()
104
+ return True
105
+
106
+ async def ack() -> None:
107
+ settle()
108
+
109
+ async def requeue() -> None:
110
+ if settle():
111
+ self._queues[queue].put_nowait((body, message_id))
112
+
113
+ await handler(Delivery(queue, body, message_id, ack, requeue))