domainkit 0.6.2__tar.gz → 0.7.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 (397) hide show
  1. {domainkit-0.6.2 → domainkit-0.7.0}/PKG-INFO +45 -8
  2. {domainkit-0.6.2 → domainkit-0.7.0}/README.md +35 -7
  3. {domainkit-0.6.2 → domainkit-0.7.0}/pyproject.toml +14 -1
  4. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/apps/config.py +23 -1
  5. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/architecture.py +4 -4
  6. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/__init__.py +6 -0
  7. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/core.py +2 -2
  8. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/memory.py +2 -2
  9. domainkit-0.7.0/src/domainkit/cache/pages.py +228 -0
  10. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/redis.py +135 -15
  11. domainkit-0.7.0/src/domainkit/clock.py +198 -0
  12. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/conf/__init__.py +2 -0
  13. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/conf/global_settings.py +206 -22
  14. domainkit-0.7.0/src/domainkit/contrib/admin/__init__.py +62 -0
  15. domainkit-0.7.0/src/domainkit/contrib/admin/sites.py +588 -0
  16. domainkit-0.7.0/src/domainkit/contrib/audit/api.py +148 -0
  17. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/apps.py +8 -2
  18. domainkit-0.7.0/src/domainkit/contrib/audit/history.py +206 -0
  19. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/management/commands/auditforget.py +3 -2
  20. domainkit-0.7.0/src/domainkit/contrib/audit/management/commands/auditlog.py +101 -0
  21. domainkit-0.7.0/src/domainkit/contrib/audit/models.py +21 -0
  22. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/personal.py +3 -2
  23. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/recorder.py +2 -1
  24. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/seals.py +4 -3
  25. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/tables.py +7 -3
  26. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/__init__.py +6 -0
  27. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/access.py +62 -12
  28. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/admin.py +67 -95
  29. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/api.py +80 -92
  30. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/apps.py +153 -7
  31. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/authentication.py +49 -11
  32. domainkit-0.7.0/src/domainkit/contrib/auth/certificates.py +188 -0
  33. domainkit-0.7.0/src/domainkit/contrib/auth/cookies.py +384 -0
  34. domainkit-0.7.0/src/domainkit/contrib/auth/erasure.py +44 -0
  35. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/events.py +59 -1
  36. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/exceptions.py +4 -0
  37. domainkit-0.7.0/src/domainkit/contrib/auth/management/commands/addcertificate.py +51 -0
  38. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/commands/createsuperuser.py +7 -2
  39. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/commands/createtoken.py +41 -5
  40. domainkit-0.7.0/src/domainkit/contrib/auth/management/commands/resetmfa.py +42 -0
  41. domainkit-0.7.0/src/domainkit/contrib/auth/mfa.py +479 -0
  42. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/middleware.py +14 -6
  43. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/models.py +163 -15
  44. domainkit-0.7.0/src/domainkit/contrib/auth/onetime.py +119 -0
  45. domainkit-0.7.0/src/domainkit/contrib/auth/personal_tokens.py +220 -0
  46. domainkit-0.7.0/src/domainkit/contrib/auth/recovery.py +352 -0
  47. domainkit-0.7.0/src/domainkit/contrib/auth/repositories.py +738 -0
  48. domainkit-0.7.0/src/domainkit/contrib/auth/settings.py +291 -0
  49. domainkit-0.7.0/src/domainkit/contrib/auth/signup.py +286 -0
  50. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/tables.py +75 -0
  51. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/tasks.py +6 -2
  52. domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/email_verification.subject.txt +1 -0
  53. domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/email_verification.txt +7 -0
  54. domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/password_reset.subject.txt +1 -0
  55. domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/password_reset.txt +8 -0
  56. domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/signup_confirm.subject.txt +1 -0
  57. domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/signup_confirm.txt +8 -0
  58. domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/signup_exists.subject.txt +1 -0
  59. domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/signup_exists.txt +7 -0
  60. domainkit-0.7.0/src/domainkit/contrib/auth/throttling.py +124 -0
  61. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/tokens.py +13 -2
  62. domainkit-0.7.0/src/domainkit/contrib/auth/totp.py +84 -0
  63. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/uow.py +12 -1
  64. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/model.py +2 -1
  65. domainkit-0.7.0/src/domainkit/contrib/flags/__init__.py +55 -0
  66. domainkit-0.7.0/src/domainkit/contrib/flags/admin.py +225 -0
  67. domainkit-0.7.0/src/domainkit/contrib/flags/apps.py +32 -0
  68. domainkit-0.7.0/src/domainkit/contrib/flags/dependencies.py +28 -0
  69. domainkit-0.7.0/src/domainkit/contrib/flags/events.py +32 -0
  70. domainkit-0.7.0/src/domainkit/contrib/flags/exceptions.py +15 -0
  71. domainkit-0.7.0/src/domainkit/contrib/flags/management/__init__.py +1 -0
  72. domainkit-0.7.0/src/domainkit/contrib/flags/management/commands/__init__.py +1 -0
  73. domainkit-0.7.0/src/domainkit/contrib/flags/management/commands/flags.py +161 -0
  74. domainkit-0.7.0/src/domainkit/contrib/flags/models.py +113 -0
  75. domainkit-0.7.0/src/domainkit/contrib/flags/settings.py +18 -0
  76. domainkit-0.7.0/src/domainkit/contrib/flags/store.py +208 -0
  77. domainkit-0.7.0/src/domainkit/contrib/flags/tables.py +38 -0
  78. domainkit-0.7.0/src/domainkit/contrib/flags/uow.py +40 -0
  79. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/middleware.py +3 -2
  80. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/operations.py +2 -2
  81. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/tasks.py +3 -4
  82. domainkit-0.7.0/src/domainkit/contrib/mail/__init__.py +56 -0
  83. domainkit-0.7.0/src/domainkit/contrib/mail/apps.py +43 -0
  84. domainkit-0.7.0/src/domainkit/contrib/mail/backends.py +173 -0
  85. domainkit-0.7.0/src/domainkit/contrib/mail/delivery.py +113 -0
  86. domainkit-0.7.0/src/domainkit/contrib/mail/message.py +204 -0
  87. domainkit-0.7.0/src/domainkit/contrib/mail/settings.py +76 -0
  88. domainkit-0.7.0/src/domainkit/contrib/mail/templates.py +157 -0
  89. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/__init__.py +22 -5
  90. domainkit-0.7.0/src/domainkit/contrib/messaging/admin.py +191 -0
  91. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/apps.py +7 -8
  92. domainkit-0.7.0/src/domainkit/contrib/messaging/cron.py +272 -0
  93. domainkit-0.7.0/src/domainkit/contrib/messaging/management/commands/messages.py +125 -0
  94. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/metrics.py +18 -5
  95. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/outbox.py +54 -11
  96. domainkit-0.7.0/src/domainkit/contrib/messaging/registry.py +399 -0
  97. domainkit-0.7.0/src/domainkit/contrib/messaging/retry.py +90 -0
  98. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/settings.py +4 -2
  99. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/tables.py +17 -1
  100. domainkit-0.7.0/src/domainkit/contrib/messaging/testing.py +401 -0
  101. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/worker.py +230 -39
  102. domainkit-0.7.0/src/domainkit/contrib/notifications/__init__.py +65 -0
  103. domainkit-0.7.0/src/domainkit/contrib/notifications/apps.py +60 -0
  104. domainkit-0.7.0/src/domainkit/contrib/notifications/backends.py +107 -0
  105. domainkit-0.7.0/src/domainkit/contrib/notifications/delivery.py +134 -0
  106. domainkit-0.7.0/src/domainkit/contrib/notifications/messages.py +107 -0
  107. domainkit-0.7.0/src/domainkit/contrib/notifications/settings.py +24 -0
  108. domainkit-0.7.0/src/domainkit/contrib/objectpermissions/__init__.py +63 -0
  109. domainkit-0.7.0/src/domainkit/contrib/objectpermissions/apps.py +25 -0
  110. domainkit-0.7.0/src/domainkit/contrib/objectpermissions/backends.py +34 -0
  111. domainkit-0.7.0/src/domainkit/contrib/objectpermissions/criteria.py +178 -0
  112. domainkit-0.7.0/src/domainkit/contrib/objectpermissions/deletions.py +47 -0
  113. domainkit-0.7.0/src/domainkit/contrib/objectpermissions/models.py +146 -0
  114. domainkit-0.7.0/src/domainkit/contrib/objectpermissions/services.py +185 -0
  115. domainkit-0.7.0/src/domainkit/contrib/objectpermissions/tables.py +50 -0
  116. domainkit-0.7.0/src/domainkit/contrib/objectpermissions/uow.py +88 -0
  117. domainkit-0.7.0/src/domainkit/contrib/privacy/__init__.py +55 -0
  118. domainkit-0.7.0/src/domainkit/contrib/privacy/apps.py +38 -0
  119. domainkit-0.7.0/src/domainkit/contrib/privacy/events.py +18 -0
  120. domainkit-0.7.0/src/domainkit/contrib/privacy/management/__init__.py +1 -0
  121. domainkit-0.7.0/src/domainkit/contrib/privacy/management/commands/__init__.py +1 -0
  122. domainkit-0.7.0/src/domainkit/contrib/privacy/management/commands/privacy.py +121 -0
  123. domainkit-0.7.0/src/domainkit/contrib/privacy/models.py +334 -0
  124. domainkit-0.7.0/src/domainkit/contrib/privacy/operations.py +423 -0
  125. domainkit-0.7.0/src/domainkit/contrib/privacy/retention.py +53 -0
  126. domainkit-0.7.0/src/domainkit/contrib/webhooks/__init__.py +35 -0
  127. domainkit-0.7.0/src/domainkit/contrib/webhooks/admin.py +403 -0
  128. domainkit-0.7.0/src/domainkit/contrib/webhooks/apps.py +67 -0
  129. domainkit-0.7.0/src/domainkit/contrib/webhooks/client.py +133 -0
  130. domainkit-0.7.0/src/domainkit/contrib/webhooks/delivery.py +529 -0
  131. domainkit-0.7.0/src/domainkit/contrib/webhooks/guard.py +165 -0
  132. domainkit-0.7.0/src/domainkit/contrib/webhooks/management/__init__.py +1 -0
  133. domainkit-0.7.0/src/domainkit/contrib/webhooks/management/commands/__init__.py +1 -0
  134. domainkit-0.7.0/src/domainkit/contrib/webhooks/management/commands/webhooks.py +133 -0
  135. domainkit-0.7.0/src/domainkit/contrib/webhooks/models.py +240 -0
  136. domainkit-0.7.0/src/domainkit/contrib/webhooks/settings.py +50 -0
  137. domainkit-0.7.0/src/domainkit/contrib/webhooks/signing.py +114 -0
  138. domainkit-0.7.0/src/domainkit/contrib/webhooks/tables.py +89 -0
  139. domainkit-0.7.0/src/domainkit/contrib/webhooks/uow.py +43 -0
  140. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/__init__.py +19 -1
  141. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/connections.py +14 -3
  142. domainkit-0.7.0/src/domainkit/db/derive.py +282 -0
  143. domainkit-0.7.0/src/domainkit/db/errors.py +302 -0
  144. domainkit-0.7.0/src/domainkit/db/fixtures.py +392 -0
  145. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/migrations.py +131 -7
  146. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/orm.py +30 -1
  147. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/partitioning.py +2 -3
  148. domainkit-0.7.0/src/domainkit/db/query.py +232 -0
  149. domainkit-0.7.0/src/domainkit/db/repository.py +182 -0
  150. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/routing.py +31 -1
  151. domainkit-0.7.0/src/domainkit/db/squashing.py +358 -0
  152. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/uow.py +52 -4
  153. domainkit-0.7.0/src/domainkit/ddd/__init__.py +80 -0
  154. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/events.py +3 -2
  155. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/exceptions.py +10 -0
  156. domainkit-0.7.0/src/domainkit/ddd/fields.py +341 -0
  157. domainkit-0.7.0/src/domainkit/ddd/messages.py +69 -0
  158. domainkit-0.7.0/src/domainkit/ddd/pagination.py +37 -0
  159. domainkit-0.7.0/src/domainkit/ddd/preconditions.py +111 -0
  160. domainkit-0.7.0/src/domainkit/ddd/query.py +804 -0
  161. domainkit-0.7.0/src/domainkit/ddd/repository.py +610 -0
  162. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/uow.py +101 -4
  163. domainkit-0.7.0/src/domainkit/debug/__init__.py +11 -0
  164. domainkit-0.7.0/src/domainkit/debug/middleware.py +123 -0
  165. domainkit-0.7.0/src/domainkit/debug/queries.py +117 -0
  166. domainkit-0.7.0/src/domainkit/encryption.py +90 -0
  167. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/__init__.py +20 -1
  168. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/application.py +19 -2
  169. domainkit-0.7.0/src/domainkit/http/errors.py +464 -0
  170. domainkit-0.7.0/src/domainkit/http/filters.py +430 -0
  171. domainkit-0.7.0/src/domainkit/http/locale.py +51 -0
  172. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/middleware.py +14 -8
  173. domainkit-0.7.0/src/domainkit/http/openapi.py +551 -0
  174. domainkit-0.7.0/src/domainkit/http/pagination.py +561 -0
  175. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/ratelimit.py +193 -34
  176. domainkit-0.7.0/src/domainkit/http/uploads.py +298 -0
  177. domainkit-0.7.0/src/domainkit/http/versioning.py +204 -0
  178. domainkit-0.7.0/src/domainkit/i18n/__init__.py +68 -0
  179. domainkit-0.7.0/src/domainkit/i18n/catalogs.py +293 -0
  180. domainkit-0.7.0/src/domainkit/i18n/formats.py +308 -0
  181. domainkit-0.7.0/src/domainkit/i18n/locale/ar/LC_MESSAGES/messages.mo +0 -0
  182. domainkit-0.7.0/src/domainkit/i18n/locale/ar/LC_MESSAGES/messages.po +626 -0
  183. domainkit-0.7.0/src/domainkit/i18n/locale/en/LC_MESSAGES/messages.mo +0 -0
  184. domainkit-0.7.0/src/domainkit/i18n/locale/en/LC_MESSAGES/messages.po +620 -0
  185. domainkit-0.7.0/src/domainkit/i18n/timezone.py +90 -0
  186. domainkit-0.7.0/src/domainkit/i18n/translation.py +271 -0
  187. domainkit-0.7.0/src/domainkit/i18n/validation.py +54 -0
  188. domainkit-0.7.0/src/domainkit/management/commands/addcommand.py +358 -0
  189. domainkit-0.7.0/src/domainkit/management/commands/addevent.py +197 -0
  190. domainkit-0.7.0/src/domainkit/management/commands/addquery.py +289 -0
  191. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/check.py +3 -0
  192. domainkit-0.7.0/src/domainkit/management/commands/compilemessages.py +101 -0
  193. domainkit-0.7.0/src/domainkit/management/commands/dbshell.py +140 -0
  194. domainkit-0.7.0/src/domainkit/management/commands/diffsettings.py +67 -0
  195. domainkit-0.7.0/src/domainkit/management/commands/dumpdata.py +86 -0
  196. domainkit-0.7.0/src/domainkit/management/commands/flush.py +57 -0
  197. domainkit-0.7.0/src/domainkit/management/commands/loaddata.py +90 -0
  198. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/makemigrations.py +25 -1
  199. domainkit-0.7.0/src/domainkit/management/commands/migrate.py +147 -0
  200. domainkit-0.7.0/src/domainkit/management/commands/openapi.py +111 -0
  201. domainkit-0.7.0/src/domainkit/management/commands/seed.py +39 -0
  202. domainkit-0.7.0/src/domainkit/management/commands/shell.py +147 -0
  203. domainkit-0.7.0/src/domainkit/management/commands/squashmigrations.py +57 -0
  204. domainkit-0.7.0/src/domainkit/management/commands/startaggregate.py +399 -0
  205. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/startdomain.py +40 -52
  206. domainkit-0.7.0/src/domainkit/management/generators.py +832 -0
  207. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/__init__.py-tpl +2 -2
  208. domainkit-0.7.0/src/domainkit/management/templates/domain/package/api/routes.py-tpl +89 -0
  209. domainkit-0.7.0/src/domainkit/management/templates/domain/package/application/commands.py-tpl +38 -0
  210. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/application/handlers.py-tpl +0 -1
  211. domainkit-0.7.0/src/domainkit/management/templates/domain/package/application/services.py-tpl +62 -0
  212. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/application/unit_of_work.py-tpl +1 -1
  213. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/apps.py-tpl +0 -3
  214. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/domain/events.py-tpl +13 -0
  215. domainkit-0.7.0/src/domainkit/management/templates/domain/package/domain/model.py-tpl +122 -0
  216. domainkit-0.7.0/src/domainkit/management/templates/domain/package/infrastructure/orm.py-tpl +44 -0
  217. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/infrastructure/unit_of_work.py-tpl +11 -3
  218. domainkit-0.7.0/src/domainkit/management/templates/domain/tests/fakes.py-tpl +54 -0
  219. domainkit-0.7.0/src/domainkit/management/templates/domain/tests/test_api.py-tpl +110 -0
  220. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/tests/test_model.py-tpl +52 -11
  221. domainkit-0.7.0/src/domainkit/management/templates/domain/tests/test_services.py-tpl +98 -0
  222. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/pyproject.toml-tpl +2 -1
  223. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/settings/base.py-tpl +9 -0
  224. domainkit-0.7.0/src/domainkit/management/templates/project/src/config/settings/local.py-tpl +16 -0
  225. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/settings/production.py-tpl +12 -6
  226. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/settings/test.py-tpl +1 -0
  227. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/tests/test_health.py-tpl +9 -1
  228. domainkit-0.7.0/src/domainkit/resources/__init__.py +59 -0
  229. domainkit-0.7.0/src/domainkit/resources/access.py +102 -0
  230. domainkit-0.7.0/src/domainkit/resources/batches.py +121 -0
  231. domainkit-0.7.0/src/domainkit/resources/conditional.py +64 -0
  232. domainkit-0.7.0/src/domainkit/resources/events.py +34 -0
  233. domainkit-0.7.0/src/domainkit/resources/policies.py +168 -0
  234. domainkit-0.7.0/src/domainkit/resources/renderers.py +174 -0
  235. domainkit-0.7.0/src/domainkit/resources/routing.py +1768 -0
  236. domainkit-0.7.0/src/domainkit/resources/schemas.py +337 -0
  237. domainkit-0.7.0/src/domainkit/signing.py +314 -0
  238. domainkit-0.7.0/src/domainkit/storage/__init__.py +43 -0
  239. domainkit-0.7.0/src/domainkit/storage/base.py +226 -0
  240. domainkit-0.7.0/src/domainkit/storage/filesystem.py +271 -0
  241. domainkit-0.7.0/src/domainkit/storage/handler.py +60 -0
  242. domainkit-0.7.0/src/domainkit/storage/memory.py +63 -0
  243. domainkit-0.7.0/src/domainkit/testing/__init__.py +215 -0
  244. domainkit-0.7.0/src/domainkit/testing/clients.py +274 -0
  245. domainkit-0.7.0/src/domainkit/testing/databases.py +259 -0
  246. domainkit-0.7.0/src/domainkit/testing/factories.py +313 -0
  247. domainkit-0.7.0/src/domainkit/testing/queries.py +124 -0
  248. domainkit-0.7.0/src/domainkit/tracing.py +150 -0
  249. domainkit-0.7.0/src/domainkit/utils/headers.py +85 -0
  250. domainkit-0.7.0/src/domainkit/utils/redis_urls.py +143 -0
  251. domainkit-0.6.2/src/domainkit/contrib/audit/management/commands/auditlog.py +0 -53
  252. domainkit-0.6.2/src/domainkit/contrib/auth/repositories.py +0 -361
  253. domainkit-0.6.2/src/domainkit/contrib/auth/settings.py +0 -137
  254. domainkit-0.6.2/src/domainkit/contrib/messaging/admin.py +0 -62
  255. domainkit-0.6.2/src/domainkit/contrib/messaging/management/commands/messages.py +0 -58
  256. domainkit-0.6.2/src/domainkit/contrib/messaging/registry.py +0 -215
  257. domainkit-0.6.2/src/domainkit/db/errors.py +0 -112
  258. domainkit-0.6.2/src/domainkit/db/repository.py +0 -33
  259. domainkit-0.6.2/src/domainkit/ddd/__init__.py +0 -48
  260. domainkit-0.6.2/src/domainkit/ddd/messages.py +0 -16
  261. domainkit-0.6.2/src/domainkit/ddd/pagination.py +0 -24
  262. domainkit-0.6.2/src/domainkit/ddd/repository.py +0 -94
  263. domainkit-0.6.2/src/domainkit/http/errors.py +0 -234
  264. domainkit-0.6.2/src/domainkit/http/pagination.py +0 -71
  265. domainkit-0.6.2/src/domainkit/management/commands/migrate.py +0 -88
  266. domainkit-0.6.2/src/domainkit/management/commands/shell.py +0 -27
  267. domainkit-0.6.2/src/domainkit/management/templates/domain/package/api/dependencies.py-tpl +0 -47
  268. domainkit-0.6.2/src/domainkit/management/templates/domain/package/api/routes.py-tpl +0 -102
  269. domainkit-0.6.2/src/domainkit/management/templates/domain/package/api/schemas.py-tpl +0 -65
  270. domainkit-0.6.2/src/domainkit/management/templates/domain/package/application/commands.py-tpl +0 -24
  271. domainkit-0.6.2/src/domainkit/management/templates/domain/package/application/services.py-tpl +0 -63
  272. domainkit-0.6.2/src/domainkit/management/templates/domain/package/domain/exceptions.py-tpl +0 -15
  273. domainkit-0.6.2/src/domainkit/management/templates/domain/package/domain/model.py-tpl +0 -71
  274. domainkit-0.6.2/src/domainkit/management/templates/domain/package/domain/repositories.py-tpl +0 -33
  275. domainkit-0.6.2/src/domainkit/management/templates/domain/package/infrastructure/orm.py-tpl +0 -46
  276. domainkit-0.6.2/src/domainkit/management/templates/domain/package/infrastructure/repositories.py-tpl +0 -26
  277. domainkit-0.6.2/src/domainkit/management/templates/domain/package/management/__init__.py-tpl +0 -1
  278. domainkit-0.6.2/src/domainkit/management/templates/domain/package/management/commands/__init__.py-tpl +0 -1
  279. domainkit-0.6.2/src/domainkit/management/templates/domain/package/management/commands/{{ aggregates }}.py-tpl +0 -25
  280. domainkit-0.6.2/src/domainkit/management/templates/domain/package/settings.py-tpl +0 -30
  281. domainkit-0.6.2/src/domainkit/management/templates/domain/tests/fakes.py-tpl +0 -47
  282. domainkit-0.6.2/src/domainkit/management/templates/domain/tests/test_api.py-tpl +0 -121
  283. domainkit-0.6.2/src/domainkit/management/templates/domain/tests/test_services.py-tpl +0 -71
  284. domainkit-0.6.2/src/domainkit/management/templates/domain/tests/test_settings.py-tpl +0 -20
  285. domainkit-0.6.2/src/domainkit/management/templates/project/src/config/settings/local.py-tpl +0 -11
  286. domainkit-0.6.2/src/domainkit/testing.py +0 -179
  287. domainkit-0.6.2/src/domainkit/tracing.py +0 -42
  288. {domainkit-0.6.2 → domainkit-0.7.0}/LICENSE +0 -0
  289. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/__init__.py +0 -0
  290. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/apps/__init__.py +0 -0
  291. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/apps/registry.py +0 -0
  292. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/broker/__init__.py +0 -0
  293. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/broker/base.py +0 -0
  294. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/broker/handler.py +0 -0
  295. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/broker/memory.py +0 -0
  296. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/broker/rabbitmq.py +0 -0
  297. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/base.py +0 -0
  298. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/decorators.py +0 -0
  299. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/handler.py +0 -0
  300. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/metrics.py +0 -0
  301. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/conf/env.py +0 -0
  302. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/conf/lazy.py +0 -0
  303. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/context.py +0 -0
  304. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/__init__.py +0 -0
  305. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/__init__.py +0 -0
  306. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/management/__init__.py +0 -0
  307. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/management/commands/__init__.py +0 -0
  308. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/management/commands/auditseal.py +0 -0
  309. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/management/commands/auditverify.py +0 -0
  310. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/protection.py +0 -0
  311. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/settings.py +0 -0
  312. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/tasks.py +0 -0
  313. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/backends.py +0 -0
  314. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/common-passwords.NOTICE +0 -0
  315. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/common-passwords.txt.gz +0 -0
  316. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/hashers.py +0 -0
  317. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/__init__.py +0 -0
  318. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/commands/__init__.py +0 -0
  319. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/commands/changepassword.py +0 -0
  320. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/commands/cleartokens.py +0 -0
  321. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/permissions.py +0 -0
  322. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/users.py +0 -0
  323. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/validators.py +0 -0
  324. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/__init__.py +0 -0
  325. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/apps.py +0 -0
  326. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/models.py +0 -0
  327. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/registry.py +0 -0
  328. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/sync.py +0 -0
  329. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/tables.py +0 -0
  330. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/__init__.py +0 -0
  331. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/apps.py +0 -0
  332. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/management/__init__.py +0 -0
  333. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/management/commands/__init__.py +0 -0
  334. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/management/commands/evidence.py +0 -0
  335. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/settings.py +0 -0
  336. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/store.py +0 -0
  337. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/tasks.py +0 -0
  338. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/__init__.py +0 -0
  339. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/apps.py +0 -0
  340. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/settings.py +0 -0
  341. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/tables.py +0 -0
  342. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/dispatch.py +0 -0
  343. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/management/__init__.py +0 -0
  344. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/management/commands/__init__.py +0 -0
  345. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/management/commands/runworker.py +0 -0
  346. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/drivers.py +0 -0
  347. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/migration_env/__init__.py +0 -0
  348. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/migration_env/env.py +0 -0
  349. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/migration_env/script.py.mako +0 -0
  350. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/snapshots.py +0 -0
  351. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/types.py +0 -0
  352. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/_dataclass.py +0 -0
  353. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/model.py +0 -0
  354. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/personal.py +0 -0
  355. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/deprecation.py +0 -0
  356. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/exceptions.py +0 -0
  357. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/health.py +0 -0
  358. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/metrics.py +0 -0
  359. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ledger/__init__.py +0 -0
  360. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ledger/base.py +0 -0
  361. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ledger/handler.py +0 -0
  362. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ledger/immudb.py +0 -0
  363. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ledger/memory.py +0 -0
  364. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/lifecycle.py +0 -0
  365. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/log.py +0 -0
  366. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/__init__.py +0 -0
  367. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/base.py +0 -0
  368. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/__init__.py +0 -0
  369. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/broker.py +0 -0
  370. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/cache.py +0 -0
  371. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/init.py +0 -0
  372. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/partitions.py +0 -0
  373. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/runserver.py +0 -0
  374. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/showmigrations.py +0 -0
  375. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/api/__init__.py-tpl +0 -0
  376. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/application/__init__.py-tpl +0 -0
  377. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/domain/__init__.py-tpl +0 -0
  378. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/infrastructure/__init__.py-tpl +0 -0
  379. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/tests/__init__.py-tpl +0 -0
  380. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/dot-env-tpl +0 -0
  381. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/dot-gitignore-tpl +0 -0
  382. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/manage.py-tpl +0 -0
  383. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/__init__.py-tpl +0 -0
  384. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/asgi.py-tpl +0 -0
  385. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/settings/__init__.py-tpl +0 -0
  386. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/urls.py-tpl +0 -0
  387. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/domains/__init__.py-tpl +0 -0
  388. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/tests/__init__.py-tpl +0 -0
  389. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/py.typed +0 -0
  390. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/reporting.py +0 -0
  391. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/__init__.py +0 -0
  392. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/closing.py +0 -0
  393. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/formatting.py +0 -0
  394. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/module_loading.py +0 -0
  395. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/rates.py +0 -0
  396. {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/readiness.py +0 -0
  397. {domainkit-0.6.2 → domainkit-0.7.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.6.2
3
+ Version: 0.7.0
4
4
  Summary: A Django-style framework for domain-driven FastAPI services.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -20,19 +20,26 @@ Provides-Extra: argon2
20
20
  Provides-Extra: immudb
21
21
  Provides-Extra: jwt
22
22
  Provides-Extra: metrics
23
+ Provides-Extra: mfa
23
24
  Provides-Extra: postgres
24
25
  Provides-Extra: rabbitmq
25
26
  Provides-Extra: redis
26
27
  Provides-Extra: sqlite
27
28
  Provides-Extra: testing
28
29
  Provides-Extra: tracing
30
+ Provides-Extra: tzdata
31
+ Provides-Extra: uploads
32
+ Provides-Extra: webhooks
29
33
  Requires-Dist: aio-pika (>=10.1.0,<11.0.0) ; extra == "rabbitmq"
30
34
  Requires-Dist: aiosqlite (>=0.22.1,<0.23.0) ; extra == "sqlite"
31
35
  Requires-Dist: alembic (>=1.20.0,<2.0.0)
32
36
  Requires-Dist: argon2-cffi (>=25.1.0,<26.0.0) ; extra == "argon2"
33
37
  Requires-Dist: asyncpg (>=0.31.0,<0.32.0) ; extra == "postgres"
38
+ Requires-Dist: cryptography (>=50.0.2,<51.0.0) ; extra == "mfa"
39
+ Requires-Dist: cryptography (>=50.0.2,<51.0.0) ; extra == "webhooks"
34
40
  Requires-Dist: fastapi (>=0.141.1,<0.142.0)
35
41
  Requires-Dist: httpx2 (>=2.13.0,<3.0.0) ; extra == "testing"
42
+ Requires-Dist: httpx2 (>=2.13.0,<3.0.0) ; extra == "webhooks"
36
43
  Requires-Dist: immudb-py (>=1.5.0,<1.6.0) ; extra == "immudb"
37
44
  Requires-Dist: opentelemetry-api (>=1.45.1,<2.0.0) ; extra == "tracing"
38
45
  Requires-Dist: prometheus-client (>=0.26.0,<0.27.0) ; extra == "metrics"
@@ -40,8 +47,10 @@ Requires-Dist: pydantic (>=2.14.1,<3.0.0)
40
47
  Requires-Dist: pyjwt[crypto] (>=2.10.1,<3.0.0) ; extra == "jwt"
41
48
  Requires-Dist: pytest (>=9.1.1,<10.0.0) ; extra == "testing"
42
49
  Requires-Dist: python-dotenv (>=1.2.4,<2.0.0)
50
+ Requires-Dist: python-multipart (>=0.0.32,<0.1.0) ; extra == "uploads"
43
51
  Requires-Dist: redis (>=8.1.0,<9.0.0) ; extra == "redis"
44
52
  Requires-Dist: sqlalchemy[asyncio] (>=2.0.54,<2.1)
53
+ Requires-Dist: tzdata (>=2025.2) ; extra == "tzdata"
45
54
  Requires-Dist: uvicorn[standard] (>=0.54.0,<0.55.0)
46
55
  Project-URL: Changelog, https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md
47
56
  Project-URL: Documentation, https://github.com/karimmattar/domainkit/blob/main/docs/reference.md
@@ -49,6 +58,10 @@ Project-URL: Homepage, https://github.com/karimmattar/domainkit
49
58
  Project-URL: Issues, https://github.com/karimmattar/domainkit/issues
50
59
  Description-Content-Type: text/markdown
51
60
 
61
+ <p align="center">
62
+ <img src="https://raw.githubusercontent.com/karimmattar/domainkit/main/docs/assets/domainkit-banner.jpg" alt="domainkit, a domain-driven FastAPI framework" width="720">
63
+ </p>
64
+
52
65
  # domainkit
53
66
 
54
67
  A Django-style framework for domain-driven FastAPI services. It provides:
@@ -58,8 +71,13 @@ A Django-style framework for domain-driven FastAPI services. It provides:
58
71
  - DDD building blocks, with the layer rules between them enforced
59
72
  - several databases: domains routed to their own, sharding by key,
60
73
  PostgreSQL range partitioning, and immudb ledgers for evidence
61
- - caches in memory or in Redis and Valkey, with stampede protection, and
62
- rate limits shared by every replica
74
+ - caches in memory or in Redis and Valkey (one server, Sentinel, or
75
+ Cluster), with stampede protection, and rate limits shared by every
76
+ replica, globally or per route
77
+ - answers in the caller's language (English and Arabic ship), times shown
78
+ in the business's time zone, and numbers and dates in local formats
79
+ - file storage and uploads checked by their content, under names the
80
+ storage makes
63
81
  - background work through a transactional outbox, run by polling workers or
64
82
  through RabbitMQ, with queues for separate worker pools
65
83
  - users, groups, and permissions per content type, as in Django's
@@ -67,24 +85,43 @@ A Django-style framework for domain-driven FastAPI services. It provides:
67
85
  and a swappable user model; token or JWT authentication, and route rules
68
86
  as in Django REST framework
69
87
  - optional apps for reliable messaging, audit trails, idempotent requests,
70
- and tamper-evident evidence
88
+ tamper-evident evidence, signed outgoing webhooks, feature flags, and privacy
89
+ requests (export, erasure, and retention)
71
90
 
72
91
  ```bash
73
- pip install "domainkit[postgres]" # extras: postgres, sqlite, argon2, jwt, immudb, redis, rabbitmq, metrics, tracing, testing
92
+ pip install "domainkit[postgres]" # extras: postgres, sqlite, argon2, jwt, mfa, immudb, redis, rabbitmq, metrics, tracing, uploads, testing, webhooks, tzdata
74
93
  ```
75
94
 
76
95
  ## Documentation
77
96
 
78
97
  - [docs/reference.md](https://github.com/karimmattar/domainkit/blob/main/docs/reference.md): settings, middleware, errors,
79
- observability, testing, commands, and extension points
98
+ the API surface (OpenAPI, versions, deprecations), observability,
99
+ testing, commands, and extension points
80
100
  - [docs/databases.md](https://github.com/karimmattar/domainkit/blob/main/docs/databases.md): several databases, routing,
81
101
  sharding, partitioning, and ledgers
102
+ - [docs/queries.md](https://github.com/karimmattar/domainkit/blob/main/docs/queries.md): querying repositories,
103
+ pages, filters, soft delete, row locks, and savepoints
104
+ - [docs/resources.md](https://github.com/karimmattar/domainkit/blob/main/docs/resources.md): aggregates over REST:
105
+ resource routes, commands and queries, derived schemas, partial changes,
106
+ ETags and conditional writes, field policies, nesting, the generators,
107
+ and the generic admin
82
108
  - [docs/cache.md](https://github.com/karimmattar/domainkit/blob/main/docs/cache.md): caches in memory, Redis, and Valkey,
83
109
  and shared rate limits
84
110
  - [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging (and workers on RabbitMQ),
85
- audit, idempotency, and evidence
111
+ webhooks, audit and records' history, idempotency, evidence, and
112
+ feature flags
86
113
  - [docs/auth.md](https://github.com/karimmattar/domainkit/blob/main/docs/auth.md): users, groups, permissions, content
87
114
  types, passwords, backends, tokens, route rules, and the admin API
115
+ - [docs/mail.md](https://github.com/karimmattar/domainkit/blob/main/docs/mail.md): email, mail templates, text messages,
116
+ and push notifications, sent through the outbox
117
+ - [docs/i18n.md](https://github.com/karimmattar/domainkit/blob/main/docs/i18n.md): responses in the caller's language,
118
+ time zones, and number and date formats
119
+ - [docs/uploads.md](https://github.com/karimmattar/domainkit/blob/main/docs/uploads.md): file storages, receiving uploads,
120
+ and serving files
121
+ - [docs/privacy.md](https://github.com/karimmattar/domainkit/blob/main/docs/privacy.md): exporting and erasing a person's
122
+ data, and retention
123
+ - [docs/testing.md](https://github.com/karimmattar/domainkit/blob/main/docs/testing.md): the pytest plugin: test databases,
124
+ sync and async clients, factories, a clock to freeze, and query counts
88
125
  - [docs/versioning.md](https://github.com/karimmattar/domainkit/blob/main/docs/versioning.md): compatibility, deprecations, and releases
89
126
  - [docs/adr/](https://github.com/karimmattar/domainkit/tree/main/docs/adr): the decisions behind the design
90
127
  - [CHANGELOG.md](https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md) and [SECURITY.md](https://github.com/karimmattar/domainkit/blob/main/SECURITY.md)
@@ -97,7 +134,7 @@ pip install "domainkit[postgres,sqlite,testing]"
97
134
  domainkit init billingAPI billing # the name sets the variable prefix: BILLING_API_*
98
135
  cd billing
99
136
  python manage.py check
100
- python manage.py startdomain invoices --aggregate Invoice # prints how to install it
137
+ python manage.py startdomain invoices --aggregate Invoice # installs and mounts it
101
138
  python manage.py makemigrations -m "create invoices" && python manage.py migrate
102
139
  python manage.py createsuperuser
103
140
  pytest
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/karimmattar/domainkit/main/docs/assets/domainkit-banner.jpg" alt="domainkit, a domain-driven FastAPI framework" width="720">
3
+ </p>
4
+
1
5
  # domainkit
2
6
 
3
7
  A Django-style framework for domain-driven FastAPI services. It provides:
@@ -7,8 +11,13 @@ A Django-style framework for domain-driven FastAPI services. It provides:
7
11
  - DDD building blocks, with the layer rules between them enforced
8
12
  - several databases: domains routed to their own, sharding by key,
9
13
  PostgreSQL range partitioning, and immudb ledgers for evidence
10
- - caches in memory or in Redis and Valkey, with stampede protection, and
11
- rate limits shared by every replica
14
+ - caches in memory or in Redis and Valkey (one server, Sentinel, or
15
+ Cluster), with stampede protection, and rate limits shared by every
16
+ replica, globally or per route
17
+ - answers in the caller's language (English and Arabic ship), times shown
18
+ in the business's time zone, and numbers and dates in local formats
19
+ - file storage and uploads checked by their content, under names the
20
+ storage makes
12
21
  - background work through a transactional outbox, run by polling workers or
13
22
  through RabbitMQ, with queues for separate worker pools
14
23
  - users, groups, and permissions per content type, as in Django's
@@ -16,24 +25,43 @@ A Django-style framework for domain-driven FastAPI services. It provides:
16
25
  and a swappable user model; token or JWT authentication, and route rules
17
26
  as in Django REST framework
18
27
  - optional apps for reliable messaging, audit trails, idempotent requests,
19
- and tamper-evident evidence
28
+ tamper-evident evidence, signed outgoing webhooks, feature flags, and privacy
29
+ requests (export, erasure, and retention)
20
30
 
21
31
  ```bash
22
- pip install "domainkit[postgres]" # extras: postgres, sqlite, argon2, jwt, immudb, redis, rabbitmq, metrics, tracing, testing
32
+ pip install "domainkit[postgres]" # extras: postgres, sqlite, argon2, jwt, mfa, immudb, redis, rabbitmq, metrics, tracing, uploads, testing, webhooks, tzdata
23
33
  ```
24
34
 
25
35
  ## Documentation
26
36
 
27
37
  - [docs/reference.md](https://github.com/karimmattar/domainkit/blob/main/docs/reference.md): settings, middleware, errors,
28
- observability, testing, commands, and extension points
38
+ the API surface (OpenAPI, versions, deprecations), observability,
39
+ testing, commands, and extension points
29
40
  - [docs/databases.md](https://github.com/karimmattar/domainkit/blob/main/docs/databases.md): several databases, routing,
30
41
  sharding, partitioning, and ledgers
42
+ - [docs/queries.md](https://github.com/karimmattar/domainkit/blob/main/docs/queries.md): querying repositories,
43
+ pages, filters, soft delete, row locks, and savepoints
44
+ - [docs/resources.md](https://github.com/karimmattar/domainkit/blob/main/docs/resources.md): aggregates over REST:
45
+ resource routes, commands and queries, derived schemas, partial changes,
46
+ ETags and conditional writes, field policies, nesting, the generators,
47
+ and the generic admin
31
48
  - [docs/cache.md](https://github.com/karimmattar/domainkit/blob/main/docs/cache.md): caches in memory, Redis, and Valkey,
32
49
  and shared rate limits
33
50
  - [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging (and workers on RabbitMQ),
34
- audit, idempotency, and evidence
51
+ webhooks, audit and records' history, idempotency, evidence, and
52
+ feature flags
35
53
  - [docs/auth.md](https://github.com/karimmattar/domainkit/blob/main/docs/auth.md): users, groups, permissions, content
36
54
  types, passwords, backends, tokens, route rules, and the admin API
55
+ - [docs/mail.md](https://github.com/karimmattar/domainkit/blob/main/docs/mail.md): email, mail templates, text messages,
56
+ and push notifications, sent through the outbox
57
+ - [docs/i18n.md](https://github.com/karimmattar/domainkit/blob/main/docs/i18n.md): responses in the caller's language,
58
+ time zones, and number and date formats
59
+ - [docs/uploads.md](https://github.com/karimmattar/domainkit/blob/main/docs/uploads.md): file storages, receiving uploads,
60
+ and serving files
61
+ - [docs/privacy.md](https://github.com/karimmattar/domainkit/blob/main/docs/privacy.md): exporting and erasing a person's
62
+ data, and retention
63
+ - [docs/testing.md](https://github.com/karimmattar/domainkit/blob/main/docs/testing.md): the pytest plugin: test databases,
64
+ sync and async clients, factories, a clock to freeze, and query counts
37
65
  - [docs/versioning.md](https://github.com/karimmattar/domainkit/blob/main/docs/versioning.md): compatibility, deprecations, and releases
38
66
  - [docs/adr/](https://github.com/karimmattar/domainkit/tree/main/docs/adr): the decisions behind the design
39
67
  - [CHANGELOG.md](https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md) and [SECURITY.md](https://github.com/karimmattar/domainkit/blob/main/SECURITY.md)
@@ -46,7 +74,7 @@ pip install "domainkit[postgres,sqlite,testing]"
46
74
  domainkit init billingAPI billing # the name sets the variable prefix: BILLING_API_*
47
75
  cd billing
48
76
  python manage.py check
49
- python manage.py startdomain invoices --aggregate Invoice # prints how to install it
77
+ python manage.py startdomain invoices --aggregate Invoice # installs and mounts it
50
78
  python manage.py makemigrations -m "create invoices" && python manage.py migrate
51
79
  python manage.py createsuperuser
52
80
  pytest
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "domainkit"
3
- version = "0.6.2"
3
+ version = "0.7.0"
4
4
  description = "A Django-style framework for domain-driven FastAPI services."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -40,14 +40,24 @@ argon2 = ["argon2-cffi (>=25.1.0,<26.0.0)"]
40
40
  # JSON Web Tokens for domainkit.contrib.auth (JWTAuthentication), with the
41
41
  # RSA, ECDSA, and EdDSA algorithms.
42
42
  jwt = ["pyjwt[crypto] (>=2.10.1,<3.0.0)"]
43
+ # Second factors for domainkit.contrib.auth: AES-GCM for the TOTP secrets
44
+ # it stores (auth.mfa).
45
+ mfa = ["cryptography (>=50.0.2,<51.0.0)"]
43
46
  # Ledgers for evidence (WORM): immudb's SDK, with its verified operations.
44
47
  immudb = ["immudb-py (>=1.5.0,<1.6.0)"]
45
48
  # A message broker for the worker: RabbitMQ, through aio-pika.
46
49
  rabbitmq = ["aio-pika (>=10.1.0,<11.0.0)"]
47
50
  # Shared caches and rate limits: Redis or Valkey, through redis-py.
48
51
  redis = ["redis (>=8.1.0,<9.0.0)"]
52
+ # File uploads (domainkit.http.uploads): FastAPI reads multipart forms with it.
53
+ uploads = ["python-multipart (>=0.0.32,<0.1.0)"]
49
54
  # The domainkit.testing pytest plugin; Starlette's TestClient needs httpx2.
50
55
  testing = ["pytest (>=9.1.1,<10.0.0)", "httpx2 (>=2.13.0,<3.0.0)"]
56
+ # Outgoing webhooks (domainkit.contrib.webhooks) send with httpx2 and encrypt
57
+ # subscription secrets with AES-GCM.
58
+ webhooks = ["httpx2 (>=2.13.0,<3.0.0)", "cryptography (>=50.0.2,<51.0.0)"]
59
+ # The IANA time zones for cron schedules where the system has none (Windows).
60
+ tzdata = ["tzdata (>=2025.2)"]
51
61
 
52
62
  # Runs commands before a project exists: "domainkit init billingAPI".
53
63
  [project.scripts]
@@ -151,3 +161,6 @@ exclude_also = ["if TYPE_CHECKING:", "raise NotImplementedError"]
151
161
  source_paths = ["src"]
152
162
  also_copy = ["domainkit_tests/", "docs/"]
153
163
  pytest_add_cli_args_test_selection = ["domainkit_tests/", "-p", "no:randomly"]
164
+ # Fork from a clean server: children forked after the suite ran hang on
165
+ # database tests, which mutmut would count as killed by timeout.
166
+ process_isolation = "forkserver"
@@ -4,7 +4,7 @@ from collections.abc import Sequence
4
4
  from importlib import import_module
5
5
  from pathlib import Path
6
6
  from types import ModuleType
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, ClassVar
8
8
 
9
9
  from pydantic import BaseModel
10
10
 
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
15
15
  from sqlalchemy import Table
16
16
  from sqlalchemy.ext.asyncio import AsyncConnection
17
17
 
18
+ from domainkit.contrib.privacy import Retention
18
19
  from domainkit.db.routing import DatabaseRouter
19
20
  from domainkit.ddd import DeferredTask, DomainEvent, UnitOfWork
20
21
 
@@ -34,6 +35,8 @@ class DomainConfig:
34
35
  verbose_name: str = ""
35
36
  settings_class: type[BaseModel] | None = None
36
37
  """Schema for this domain's settings, read from ``settings_name``."""
38
+ RETENTION: ClassVar[Sequence["Retention"]] = ()
39
+ """How long rows of its tables are kept (``domainkit.contrib.privacy``)."""
37
40
 
38
41
  def __init__(self, name: str, module: ModuleType) -> None:
39
42
  """Bind the config to the domain package ``name``."""
@@ -120,6 +123,16 @@ class DomainConfig:
120
123
  Content types and permissions are created this way.
121
124
  """
122
125
 
126
+ async def seed(self) -> None:
127
+ """Add the rows a fresh database needs to be useful (``manage.py seed``).
128
+
129
+ Sample data for development, or defaults a deployment starts with.
130
+ ``seed`` calls the hooks in ``INSTALLED_DOMAINS`` order, so a domain
131
+ may use what those before it seeded. Write rows through the domain's
132
+ own units of work, and make running twice harmless: rows that exist
133
+ stay as they are.
134
+ """
135
+
123
136
  def check_application(self, app: "FastAPI") -> list[str]:
124
137
  """Problems with the built application, such as routes no rule guards.
125
138
 
@@ -128,6 +141,15 @@ class DomainConfig:
128
141
  """
129
142
  return []
130
143
 
144
+ def deployment_warnings(self) -> list[str]:
145
+ """Weak but valid choices of this domain's settings where deployed.
146
+
147
+ ``check`` lists them after ``Settings.deployment_warnings``; they
148
+ do not stop the application. Return none outside staging and
149
+ production.
150
+ """
151
+ return []
152
+
131
153
  def holds_table(self, table: "Table") -> bool:
132
154
  """Whether ``table``, tagged with this domain, is in use now.
133
155
 
@@ -4,8 +4,8 @@ Each domain package has four layers; dependencies point inward only::
4
4
 
5
5
  api -> infrastructure -> application -> domain
6
6
 
7
- - ``domain``: the standard library, ``domainkit.ddd``, and its own domain layer.
8
- No frameworks, no I/O, no settings.
7
+ - ``domain``: the standard library, ``domainkit.ddd``, ``domainkit.clock``, and
8
+ its own domain layer. No frameworks, no I/O, no settings.
9
9
  - ``application``: no ``infrastructure`` or ``api`` code, and no web or
10
10
  database libraries; it reaches storage through repository ports.
11
11
  - ``infrastructure``: no ``api`` code and no web libraries.
@@ -26,7 +26,7 @@ from typing import Final
26
26
 
27
27
  LAYERS: Final = ("domain", "application", "infrastructure", "api")
28
28
 
29
- DOMAIN_ALLOWED_PREFIXES: Final = ("domainkit.ddd",)
29
+ DOMAIN_ALLOWED_PREFIXES: Final = ("domainkit.ddd", "domainkit.clock")
30
30
  """Non-stdlib modules a domain layer may import (besides its own layer)."""
31
31
 
32
32
  FORBIDDEN_LIBRARIES: Final = {
@@ -127,7 +127,7 @@ def _layer_rule(package: str, layer: str | None, imported: str) -> str | None:
127
127
  if not _is_stdlib(imported) and not _within(imported, allowed):
128
128
  return (
129
129
  "the domain layer may import only the standard library, "
130
- "domainkit.ddd, and its own domain layer"
130
+ "domainkit.ddd, domainkit.clock, and its own domain layer"
131
131
  )
132
132
  return None
133
133
  if layer is None:
@@ -12,6 +12,10 @@ then use them anywhere in the application layer::
12
12
  @cached(ttl=60)
13
13
  async def invoice_summary(invoice_id: UUID) -> InvoiceSummary: ...
14
14
 
15
+ @router.get("/invoices", dependencies=[Public])
16
+ @cache_page(ttl=60)
17
+ async def list_invoices() -> list[InvoiceSummary]: ...
18
+
15
19
  Values are JSON; a failing cache is skipped, not fatal. See ADR 0012 and
16
20
  ``docs/cache.md``.
17
21
  """
@@ -21,6 +25,7 @@ from domainkit.cache.core import DEFAULT_TTL, TTL, Cache
21
25
  from domainkit.cache.decorators import CachedFunction, cached, invalidate_on
22
26
  from domainkit.cache.handler import CacheHandler, caches
23
27
  from domainkit.cache.memory import DummyStore, MemoryStore
28
+ from domainkit.cache.pages import cache_page
24
29
 
25
30
  __all__ = [
26
31
  "DEFAULT_TTL",
@@ -33,6 +38,7 @@ __all__ = [
33
38
  "DummyStore",
34
39
  "MemoryStore",
35
40
  "Store",
41
+ "cache_page",
36
42
  "cached",
37
43
  "caches",
38
44
  "invalidate_on",
@@ -65,7 +65,7 @@ _MISS: Final = object()
65
65
 
66
66
 
67
67
  @cache
68
- def _adapter(type_: TypeForm) -> TypeAdapter[Any]:
68
+ def adapter(type_: TypeForm) -> TypeAdapter[Any]:
69
69
  """Build (once) the validator that turns JSON into ``type_``."""
70
70
  return TypeAdapter(type_)
71
71
 
@@ -85,7 +85,7 @@ def decode(payload: bytes, type_: TypeForm) -> Any: # noqa: ANN401 - as type_.
85
85
  """Parse JSON, validated as ``type_`` unless it is None."""
86
86
  if type_ is None:
87
87
  return from_json(payload)
88
- return _adapter(type_).validate_json(payload)
88
+ return adapter(type_).validate_json(payload)
89
89
 
90
90
 
91
91
  def split(raw: bytes) -> tuple[bytes, float | None, float]:
@@ -2,12 +2,12 @@
2
2
 
3
3
  import re
4
4
  import threading
5
- import time
6
5
  from collections import OrderedDict
7
6
  from collections.abc import Callable, Mapping, Sequence
8
7
  from typing import Final
9
8
 
10
9
  from domainkit.cache.base import CacheError, Store
10
+ from domainkit.clock import monotonic
11
11
 
12
12
  _INTEGER: Final = re.compile(rb"-?[0-9]+")
13
13
 
@@ -22,7 +22,7 @@ class MemoryStore(Store):
22
22
  """
23
23
 
24
24
  def __init__(
25
- self, max_entries: int, clock: Callable[[], float] = time.monotonic
25
+ self, max_entries: int, clock: Callable[[], float] = monotonic
26
26
  ) -> None:
27
27
  """Keep at most ``max_entries``; ``clock`` reads seconds."""
28
28
  self.max_entries = max_entries
@@ -0,0 +1,228 @@
1
+ """``@cache_page``: cache what a GET endpoint returns (Django's per-view cache).
2
+
3
+ The endpoint's return value is cached, typed by its return annotation as
4
+ with ``@cached``, and FastAPI serializes it on every request, hit or miss,
5
+ so ``response_model`` filtering always applies::
6
+
7
+ @router.get("/products", dependencies=[Public])
8
+ @cache_page(ttl=60)
9
+ async def list_products(category: str | None = None) -> list[ProductOut]: ...
10
+
11
+ Entries are keyed by the endpoint, the URL (scheme, host, and path, as
12
+ Django's absolute URIs), the query, the request's language, and the
13
+ headers listed in ``vary``. Route dependencies (rules,
14
+ throttles) run first, on every request, so a cached answer never reaches a
15
+ caller who may not see it.
16
+
17
+ A response depends on who asks unless proven otherwise, so requests that
18
+ carry credentials are never served from, or stored in, the cache, unless
19
+ the page varies on the user: ``vary_on_user=True`` keys entries by actor
20
+ and marks them ``private``. Credentials are the actor, the ``Credentials``
21
+ an authentication middleware accepted (``request.auth``: a token, a
22
+ cookie, a client certificate), or an ``Authorization`` header. Requests
23
+ with credentials but no actor or user id are never cached.
24
+
25
+ Cached pages override the framework's ``Cache-Control: no-store`` on
26
+ purpose: clients may keep them, and must revalidate them (``no-cache``)
27
+ unless ``max_age`` says otherwise. Each answer has an ``ETag``, and a
28
+ request whose ``If-None-Match`` names it gets 304 without a body.
29
+ """
30
+
31
+ import hashlib
32
+ import inspect
33
+ import json
34
+ from collections.abc import Awaitable, Callable, Sequence
35
+ from functools import update_wrapper
36
+ from typing import Any, Final, get_type_hints
37
+
38
+ from fastapi import Request, Response, status
39
+ from pydantic_core import to_json
40
+
41
+ from domainkit.cache.core import DEFAULT_TTL, TTL, adapter
42
+ from domainkit.cache.handler import caches
43
+ from domainkit.conf import DEFAULT_CACHE_ALIAS
44
+ from domainkit.context import current_request
45
+ from domainkit.i18n.translation import get_language
46
+ from domainkit.utils.headers import none_match, patch_vary
47
+
48
+ REQUEST: Final = "_cache_page_request"
49
+ RESPONSE: Final = "_cache_page_response"
50
+ """Parameters added to the endpoint: FastAPI passes the request and response."""
51
+ CACHEABLE_METHODS: Final = frozenset({"GET", "HEAD"})
52
+
53
+ type Endpoint = Callable[..., Awaitable[Any]]
54
+
55
+
56
+ def cache_page(
57
+ ttl: TTL = DEFAULT_TTL,
58
+ *,
59
+ cache: str = DEFAULT_CACHE_ALIAS,
60
+ vary: Sequence[str] = (),
61
+ vary_on_user: bool = False,
62
+ max_age: int = 0,
63
+ name: str | None = None,
64
+ ) -> Callable[[Endpoint], Endpoint]:
65
+ """Cache what an async GET endpoint returns, for ``ttl``, in ``cache``.
66
+
67
+ ``vary`` lists request headers the answer depends on (``X-Tenant``).
68
+ ``vary_on_user`` caches answers to users too, one entry per actor.
69
+ ``max_age`` lets clients reuse an answer that long without asking
70
+ again. ``name`` (default: the endpoint's import path) starts every key.
71
+
72
+ Raises:
73
+ TypeError: The endpoint is not async, or returns a ``Response``.
74
+ ValueError: ``max_age`` is negative.
75
+ """
76
+ if max_age < 0:
77
+ raise ValueError("max_age cannot be negative.")
78
+ headers = tuple(header.lower() for header in vary)
79
+
80
+ def decorate(endpoint: Endpoint) -> Endpoint:
81
+ if not inspect.iscoroutinefunction(endpoint):
82
+ raise TypeError("@cache_page decorates async endpoints.")
83
+ hints = get_type_hints(endpoint, include_extras=True)
84
+ returns = hints.get("return", Any)
85
+ if inspect.isclass(returns) and issubclass(returns, Response):
86
+ raise TypeError(
87
+ "@cache_page caches values FastAPI serializes, not Response objects."
88
+ )
89
+ prefix = name or f"{endpoint.__module__}.{endpoint.__qualname__}"
90
+ signature = _signature(endpoint, hints)
91
+ request_name = _parameter_of(signature, Request)
92
+ response_name = _parameter_of(signature, Response)
93
+
94
+ async def page(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 - the endpoint's.
95
+ request: Request = kwargs[request_name]
96
+ response: Response = kwargs[response_name]
97
+ for added in (REQUEST, RESPONSE): # Ours, not the endpoint's.
98
+ kwargs.pop(added, None)
99
+ user = _user(request)
100
+ if (
101
+ request.method not in CACHEABLE_METHODS
102
+ or user == _UNCHECKED
103
+ or (user is not None and not vary_on_user)
104
+ ):
105
+ return await endpoint(*args, **kwargs)
106
+ key = _key(prefix, request, headers, user)
107
+
108
+ async def compute() -> Any: # noqa: ANN401 - the endpoint's.
109
+ # Typed now, so fields the return type leaves out are not cached.
110
+ result = await endpoint(*args, **kwargs)
111
+ return adapter(returns).validate_python(result, from_attributes=True)
112
+
113
+ value = await caches[cache].get_or_set(key, compute, returns, ttl)
114
+ etag = f'W/"{hashlib.sha256(to_json(value)).hexdigest()[:32]}"'
115
+ control = "private" if vary_on_user else "public"
116
+ answer = {
117
+ "ETag": etag,
118
+ "Cache-Control": f"{control}, "
119
+ + (f"max-age={max_age}" if max_age else "no-cache"),
120
+ }
121
+ if _matches(request.headers.get("if-none-match"), etag):
122
+ unchanged = Response(status_code=status.HTTP_304_NOT_MODIFIED)
123
+ unchanged.headers.update(answer)
124
+ patch_vary(unchanged.headers, ["Authorization", *vary])
125
+ return unchanged
126
+ response.headers.update(answer)
127
+ patch_vary(response.headers, ["Authorization", *vary])
128
+ return value
129
+
130
+ update_wrapper(page, endpoint)
131
+ page.__signature__ = signature # type: ignore[attr-defined]
132
+ return page
133
+
134
+ return decorate
135
+
136
+
137
+ _UNCHECKED: Final = ""
138
+ """Credentials no middleware accepted: whose they are is unknown."""
139
+
140
+
141
+ def _user(request: Request) -> str | None:
142
+ """The actor, ``""`` for credentials without one, or None for anonymous callers.
143
+
144
+ Without an actor (no ``RequestContextMiddleware``), credentials that
145
+ middleware accepted (``request.auth``) still count, by their user's id:
146
+ a cookie or a client certificate sends no ``Authorization`` header.
147
+ """
148
+ context = current_request()
149
+ if context is not None and context.actor:
150
+ return context.actor
151
+ credentials = request.scope.get("auth")
152
+ if credentials is not None:
153
+ user_id = getattr(getattr(credentials, "user", None), "id", None)
154
+ return _UNCHECKED if user_id is None else str(user_id)
155
+ return _UNCHECKED if "authorization" in request.headers else None
156
+
157
+
158
+ def _key(
159
+ prefix: str, request: Request, headers: Sequence[str], user: str | None
160
+ ) -> str:
161
+ """The entry for this request: URL, query, language, headers, and user.
162
+
163
+ The URL has the scheme and the host, so tenants chosen by ``Host``, and
164
+ answers that link to themselves, never share an entry.
165
+ """
166
+ query = sorted(request.query_params.multi_items(), key=lambda item: item[0])
167
+ url = request.url
168
+ parts = [
169
+ f"{url.scheme}://{url.netloc}{url.path}",
170
+ query,
171
+ get_language(),
172
+ [request.headers.get(header) for header in headers],
173
+ user,
174
+ ]
175
+ canonical = json.dumps(parts, separators=(",", ":"))
176
+ return f"page:{prefix}:{hashlib.sha256(canonical.encode()).hexdigest()[:32]}"
177
+
178
+
179
+ def _matches(if_none_match: str | None, etag: str) -> bool:
180
+ """Whether ``If-None-Match`` names ``etag`` (or ``*``), compared weakly.
181
+
182
+ Resources compare their tags the same way (``utils.headers.none_match``).
183
+ """
184
+ return none_match(if_none_match, etag)
185
+
186
+
187
+ def _signature(endpoint: Endpoint, hints: dict[str, Any]) -> inspect.Signature:
188
+ """The endpoint's signature, annotations resolved, with the request and response.
189
+
190
+ FastAPI reads parameters from it. It would evaluate annotations written
191
+ as strings (``from __future__ import annotations``) in this module,
192
+ where the endpoint's names are unknown, so they are resolved here.
193
+ FastAPI passes one ``Request`` and one ``Response`` parameter, so the
194
+ endpoint's own are used when it has them.
195
+ """
196
+ signature = inspect.signature(endpoint)
197
+ parameters = [
198
+ parameter.replace(annotation=hints.get(name, parameter.annotation))
199
+ for name, parameter in signature.parameters.items()
200
+ ]
201
+ if {REQUEST, RESPONSE} & set(signature.parameters):
202
+ raise TypeError(f"@cache_page needs the parameter names {REQUEST}, {RESPONSE}.")
203
+ keyword = inspect.Parameter.KEYWORD_ONLY
204
+ added = [
205
+ inspect.Parameter(name, keyword, annotation=kind)
206
+ for name, kind in ((REQUEST, Request), (RESPONSE, Response))
207
+ if not any(_is(parameter.annotation, kind) for parameter in parameters)
208
+ ]
209
+ variadic = [p for p in parameters if p.kind is inspect.Parameter.VAR_KEYWORD]
210
+ fixed = [p for p in parameters if p.kind is not inspect.Parameter.VAR_KEYWORD]
211
+ return signature.replace(
212
+ parameters=[*fixed, *added, *variadic],
213
+ return_annotation=hints.get("return", signature.return_annotation),
214
+ )
215
+
216
+
217
+ def _parameter_of(signature: inspect.Signature, kind: type) -> str:
218
+ """The name of the parameter FastAPI passes a ``kind`` (request or response)."""
219
+ return next(
220
+ name
221
+ for name, parameter in signature.parameters.items()
222
+ if _is(parameter.annotation, kind)
223
+ )
224
+
225
+
226
+ def _is(annotation: object, kind: type) -> bool:
227
+ """Whether a parameter annotated ``annotation`` receives a ``kind``."""
228
+ return inspect.isclass(annotation) and issubclass(annotation, kind)