domainkit 0.2.0__tar.gz → 0.4.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 (169) hide show
  1. {domainkit-0.2.0 → domainkit-0.4.0}/PKG-INFO +34 -11
  2. {domainkit-0.2.0 → domainkit-0.4.0}/README.md +29 -10
  3. {domainkit-0.2.0 → domainkit-0.4.0}/pyproject.toml +14 -1
  4. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/apps/config.py +10 -0
  5. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/apps/registry.py +7 -0
  6. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/architecture.py +1 -0
  7. domainkit-0.4.0/src/domainkit/cache/__init__.py +39 -0
  8. domainkit-0.4.0/src/domainkit/cache/base.py +63 -0
  9. domainkit-0.4.0/src/domainkit/cache/core.py +400 -0
  10. domainkit-0.4.0/src/domainkit/cache/decorators.py +126 -0
  11. domainkit-0.4.0/src/domainkit/cache/handler.py +122 -0
  12. domainkit-0.4.0/src/domainkit/cache/memory.py +155 -0
  13. domainkit-0.4.0/src/domainkit/cache/metrics.py +24 -0
  14. domainkit-0.4.0/src/domainkit/cache/redis.py +176 -0
  15. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/conf/__init__.py +8 -0
  16. domainkit-0.4.0/src/domainkit/conf/global_settings.py +482 -0
  17. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/audit/apps.py +12 -2
  18. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/audit/management/commands/auditlog.py +14 -2
  19. domainkit-0.4.0/src/domainkit/contrib/evidence/__init__.py +60 -0
  20. domainkit-0.4.0/src/domainkit/contrib/evidence/apps.py +29 -0
  21. domainkit-0.4.0/src/domainkit/contrib/evidence/management/__init__.py +1 -0
  22. domainkit-0.4.0/src/domainkit/contrib/evidence/management/commands/__init__.py +1 -0
  23. domainkit-0.4.0/src/domainkit/contrib/evidence/management/commands/evidence.py +50 -0
  24. domainkit-0.4.0/src/domainkit/contrib/evidence/model.py +166 -0
  25. domainkit-0.4.0/src/domainkit/contrib/evidence/settings.py +16 -0
  26. domainkit-0.4.0/src/domainkit/contrib/evidence/store.py +131 -0
  27. domainkit-0.4.0/src/domainkit/contrib/evidence/tasks.py +32 -0
  28. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/idempotency/apps.py +8 -1
  29. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/admin.py +15 -7
  30. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/apps.py +8 -9
  31. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/management/commands/messages.py +16 -4
  32. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/management/commands/runworker.py +20 -4
  33. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/worker.py +20 -4
  34. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/connections.py +24 -1
  35. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/migrations.py +81 -38
  36. domainkit-0.4.0/src/domainkit/db/partitioning.py +344 -0
  37. domainkit-0.4.0/src/domainkit/db/routing.py +216 -0
  38. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/uow.py +35 -7
  39. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/http/application.py +5 -1
  40. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/http/health.py +4 -0
  41. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/http/ratelimit.py +60 -2
  42. domainkit-0.4.0/src/domainkit/ledger/__init__.py +37 -0
  43. domainkit-0.4.0/src/domainkit/ledger/base.py +75 -0
  44. domainkit-0.4.0/src/domainkit/ledger/handler.py +76 -0
  45. domainkit-0.4.0/src/domainkit/ledger/immudb.py +233 -0
  46. domainkit-0.4.0/src/domainkit/ledger/memory.py +42 -0
  47. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/base.py +2 -0
  48. domainkit-0.4.0/src/domainkit/management/commands/cache.py +48 -0
  49. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/commands/check.py +6 -5
  50. domainkit-0.4.0/src/domainkit/management/commands/makemigrations.py +89 -0
  51. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/commands/migrate.py +31 -10
  52. domainkit-0.4.0/src/domainkit/management/commands/partitions.py +55 -0
  53. domainkit-0.4.0/src/domainkit/management/commands/showmigrations.py +35 -0
  54. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/infrastructure/orm.py-tpl +2 -0
  55. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/testing.py +24 -10
  56. domainkit-0.2.0/src/domainkit/conf/global_settings.py +0 -205
  57. domainkit-0.2.0/src/domainkit/management/commands/makemigrations.py +0 -61
  58. domainkit-0.2.0/src/domainkit/management/commands/showmigrations.py +0 -26
  59. {domainkit-0.2.0 → domainkit-0.4.0}/LICENSE +0 -0
  60. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/__init__.py +0 -0
  61. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/apps/__init__.py +0 -0
  62. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/conf/env.py +0 -0
  63. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/conf/lazy.py +0 -0
  64. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/context.py +0 -0
  65. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/__init__.py +0 -0
  66. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/audit/__init__.py +0 -0
  67. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/audit/management/__init__.py +0 -0
  68. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/audit/management/commands/__init__.py +0 -0
  69. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/audit/protection.py +0 -0
  70. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/audit/recorder.py +0 -0
  71. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/audit/settings.py +0 -0
  72. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/audit/tables.py +0 -0
  73. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/idempotency/__init__.py +0 -0
  74. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/idempotency/middleware.py +0 -0
  75. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/idempotency/settings.py +0 -0
  76. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/idempotency/tables.py +0 -0
  77. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/idempotency/tasks.py +0 -0
  78. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/__init__.py +0 -0
  79. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/management/__init__.py +0 -0
  80. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/management/commands/__init__.py +0 -0
  81. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/metrics.py +0 -0
  82. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/outbox.py +0 -0
  83. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/registry.py +0 -0
  84. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/settings.py +0 -0
  85. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/contrib/messaging/tables.py +0 -0
  86. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/__init__.py +0 -0
  87. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/drivers.py +0 -0
  88. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/migration_env/__init__.py +0 -0
  89. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/migration_env/env.py +0 -0
  90. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/migration_env/script.py.mako +0 -0
  91. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/orm.py +0 -0
  92. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/repository.py +0 -0
  93. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/db/types.py +0 -0
  94. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/ddd/__init__.py +0 -0
  95. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/ddd/_dataclass.py +0 -0
  96. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/ddd/events.py +0 -0
  97. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/ddd/exceptions.py +0 -0
  98. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/ddd/messages.py +0 -0
  99. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/ddd/model.py +0 -0
  100. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/ddd/pagination.py +0 -0
  101. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/ddd/repository.py +0 -0
  102. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/ddd/uow.py +0 -0
  103. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/deprecation.py +0 -0
  104. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/exceptions.py +0 -0
  105. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/http/__init__.py +0 -0
  106. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/http/errors.py +0 -0
  107. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/http/metrics.py +0 -0
  108. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/http/middleware.py +0 -0
  109. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/http/pagination.py +0 -0
  110. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/log.py +0 -0
  111. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/__init__.py +0 -0
  112. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/commands/__init__.py +0 -0
  113. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/commands/init.py +0 -0
  114. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/commands/runserver.py +0 -0
  115. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/commands/shell.py +0 -0
  116. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/commands/startdomain.py +0 -0
  117. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/__init__.py-tpl +0 -0
  118. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/api/__init__.py-tpl +0 -0
  119. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/api/dependencies.py-tpl +0 -0
  120. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/api/routes.py-tpl +0 -0
  121. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/api/schemas.py-tpl +0 -0
  122. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/application/__init__.py-tpl +0 -0
  123. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/application/commands.py-tpl +0 -0
  124. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/application/handlers.py-tpl +0 -0
  125. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/application/services.py-tpl +0 -0
  126. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/application/unit_of_work.py-tpl +0 -0
  127. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/apps.py-tpl +0 -0
  128. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/domain/__init__.py-tpl +0 -0
  129. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/domain/events.py-tpl +0 -0
  130. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/domain/exceptions.py-tpl +0 -0
  131. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/domain/model.py-tpl +0 -0
  132. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/domain/repositories.py-tpl +0 -0
  133. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/infrastructure/__init__.py-tpl +0 -0
  134. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/infrastructure/repositories.py-tpl +0 -0
  135. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/infrastructure/unit_of_work.py-tpl +0 -0
  136. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/management/__init__.py-tpl +0 -0
  137. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/management/commands/__init__.py-tpl +0 -0
  138. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/management/commands/{{ aggregates }}.py-tpl +0 -0
  139. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/package/settings.py-tpl +0 -0
  140. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/tests/__init__.py-tpl +0 -0
  141. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/tests/fakes.py-tpl +0 -0
  142. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/tests/test_api.py-tpl +0 -0
  143. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/tests/test_model.py-tpl +0 -0
  144. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/tests/test_services.py-tpl +0 -0
  145. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/domain/tests/test_settings.py-tpl +0 -0
  146. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/dot-env-tpl +0 -0
  147. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/dot-gitignore-tpl +0 -0
  148. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/manage.py-tpl +0 -0
  149. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/pyproject.toml-tpl +0 -0
  150. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/src/config/__init__.py-tpl +0 -0
  151. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/src/config/asgi.py-tpl +0 -0
  152. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/src/config/settings/__init__.py-tpl +0 -0
  153. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/src/config/settings/base.py-tpl +0 -0
  154. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/src/config/settings/local.py-tpl +0 -0
  155. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/src/config/settings/production.py-tpl +0 -0
  156. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/src/config/settings/test.py-tpl +0 -0
  157. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/src/config/urls.py-tpl +0 -0
  158. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/src/domains/__init__.py-tpl +0 -0
  159. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/tests/__init__.py-tpl +0 -0
  160. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/management/templates/project/tests/test_health.py-tpl +0 -0
  161. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/py.typed +0 -0
  162. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/reporting.py +0 -0
  163. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/tracing.py +0 -0
  164. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/utils/__init__.py +0 -0
  165. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/utils/formatting.py +0 -0
  166. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/utils/module_loading.py +0 -0
  167. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/utils/rates.py +0 -0
  168. {domainkit-0.2.0 → domainkit-0.4.0}/src/domainkit/utils/readiness.py +0 -0
  169. {domainkit-0.2.0 → domainkit-0.4.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.2.0
3
+ Version: 0.4.0
4
4
  Summary: A Django-style framework for domain-driven FastAPI services.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -16,8 +16,10 @@ 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: immudb
19
20
  Provides-Extra: metrics
20
21
  Provides-Extra: postgres
22
+ Provides-Extra: redis
21
23
  Provides-Extra: sqlite
22
24
  Provides-Extra: testing
23
25
  Provides-Extra: tracing
@@ -26,11 +28,13 @@ Requires-Dist: alembic (>=1.20.0,<2.0.0)
26
28
  Requires-Dist: asyncpg (>=0.31.0,<0.32.0) ; extra == "postgres"
27
29
  Requires-Dist: fastapi (>=0.141.1,<0.142.0)
28
30
  Requires-Dist: httpx2 (>=2.13.0,<3.0.0) ; extra == "testing"
31
+ Requires-Dist: immudb-py (>=1.5.0,<1.6.0) ; extra == "immudb"
29
32
  Requires-Dist: opentelemetry-api (>=1.45.1,<2.0.0) ; extra == "tracing"
30
33
  Requires-Dist: prometheus-client (>=0.26.0,<0.27.0) ; extra == "metrics"
31
34
  Requires-Dist: pydantic (>=2.14.1,<3.0.0)
32
35
  Requires-Dist: pytest (>=9.1.1,<10.0.0) ; extra == "testing"
33
36
  Requires-Dist: python-dotenv (>=1.2.4,<2.0.0)
37
+ Requires-Dist: redis (>=8.1.0,<9.0.0) ; extra == "redis"
34
38
  Requires-Dist: sqlalchemy[asyncio] (>=2.0.54,<2.1)
35
39
  Requires-Dist: uvicorn[standard] (>=0.54.0,<0.55.0)
36
40
  Project-URL: Changelog, https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md
@@ -46,17 +50,26 @@ A Django-style framework for domain-driven FastAPI services. It provides:
46
50
  - settings modules and installed domains
47
51
  - `manage.py` commands and migrations
48
52
  - DDD building blocks, with the layer rules between them enforced
49
- - optional apps for reliable messaging, audit trails, and idempotent requests
53
+ - several databases: domains routed to their own, sharding by key,
54
+ PostgreSQL range partitioning, and immudb ledgers for evidence
55
+ - caches in memory or in Redis and Valkey, with stampede protection, and
56
+ rate limits shared by every replica
57
+ - optional apps for reliable messaging, audit trails, idempotent requests,
58
+ and tamper-evident evidence
50
59
 
51
60
  ```bash
52
- pip install "domainkit[postgres]" # extras: postgres, sqlite, metrics, tracing, testing
61
+ pip install "domainkit[postgres]" # extras: postgres, sqlite, immudb, redis, metrics, tracing, testing
53
62
  ```
54
63
 
55
64
  ## Documentation
56
65
 
57
66
  - [docs/reference.md](https://github.com/karimmattar/domainkit/blob/main/docs/reference.md): settings, middleware, errors,
58
67
  observability, testing, commands, and extension points
59
- - [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging, audit, and idempotency
68
+ - [docs/databases.md](https://github.com/karimmattar/domainkit/blob/main/docs/databases.md): several databases, routing,
69
+ sharding, partitioning, and ledgers
70
+ - [docs/cache.md](https://github.com/karimmattar/domainkit/blob/main/docs/cache.md): caches in memory, Redis, and Valkey,
71
+ and shared rate limits
72
+ - [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging, audit, idempotency, and evidence
60
73
  - [docs/versioning.md](https://github.com/karimmattar/domainkit/blob/main/docs/versioning.md): compatibility, deprecations, and releases
61
74
  - [docs/adr/](https://github.com/karimmattar/domainkit/tree/main/docs/adr): the decisions behind the design
62
75
  - [CHANGELOG.md](https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md) and [SECURITY.md](https://github.com/karimmattar/domainkit/blob/main/SECURITY.md)
@@ -105,24 +118,34 @@ poetry run pytest --cov # random order; coverage gate in pyprojec
105
118
  poetry run mutmut run "domainkit.http*" && poetry run mutmut results
106
119
  ```
107
120
 
108
- The database tests use SQLite unless `DOMAINKIT_TEST_POSTGRES_URL` names a
109
- PostgreSQL server; each test then creates a database there and drops it.
110
- Use a throwaway server:
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
124
+ them; each test cleans up after itself:
111
125
 
112
126
  ```bash
113
127
  docker run -d --rm --name domainkit-postgres -e POSTGRES_PASSWORD=postgres \
114
128
  -p 127.0.0.1:55432:5432 postgres:18-alpine
129
+ docker run -d --rm --name domainkit-redis -p 127.0.0.1:56379:6379 redis:8-alpine
115
130
  DOMAINKIT_TEST_POSTGRES_URL=postgresql+asyncpg://postgres:postgres@127.0.0.1:55432/postgres \
131
+ DOMAINKIT_TEST_REDIS_URL=redis://127.0.0.1:56379/0 \
116
132
  poetry run pytest
117
133
  ```
118
134
 
119
- CI (`.github/workflows/ci.yml`) runs the same checks on every push and pull
120
- request:
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
137
+ 98%; CI also combines the runs with and without servers, which must cover
138
+ 100%. To check that locally, run `pytest --cov` once without and once with
139
+ the servers, each with its own `COVERAGE_FILE`, then `coverage combine` and
140
+ `coverage report --fail-under=100`.
141
+
142
+ CI (`.github/workflows/ci.yml`) runs on every push and pull request:
121
143
 
122
144
  - lint and strict typing
123
145
  - the tests on Python 3.13 and 3.14
124
- - the tests again with PostgreSQL 18, including row locking, the audit
125
- triggers, and server-side timeouts
146
+ - 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
148
+ - coverage of both runs combined, which must be 100%
126
149
  - a wheel build, installed and imported in a clean environment
127
150
  - pip-audit, and a gitleaks scan of the whole history
128
151
 
@@ -5,17 +5,26 @@ A Django-style framework for domain-driven FastAPI services. It provides:
5
5
  - settings modules and installed domains
6
6
  - `manage.py` commands and migrations
7
7
  - DDD building blocks, with the layer rules between them enforced
8
- - optional apps for reliable messaging, audit trails, and idempotent requests
8
+ - several databases: domains routed to their own, sharding by key,
9
+ 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
12
+ - optional apps for reliable messaging, audit trails, idempotent requests,
13
+ and tamper-evident evidence
9
14
 
10
15
  ```bash
11
- pip install "domainkit[postgres]" # extras: postgres, sqlite, metrics, tracing, testing
16
+ pip install "domainkit[postgres]" # extras: postgres, sqlite, immudb, redis, metrics, tracing, testing
12
17
  ```
13
18
 
14
19
  ## Documentation
15
20
 
16
21
  - [docs/reference.md](https://github.com/karimmattar/domainkit/blob/main/docs/reference.md): settings, middleware, errors,
17
22
  observability, testing, commands, and extension points
18
- - [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging, audit, and idempotency
23
+ - [docs/databases.md](https://github.com/karimmattar/domainkit/blob/main/docs/databases.md): several databases, routing,
24
+ sharding, partitioning, and ledgers
25
+ - [docs/cache.md](https://github.com/karimmattar/domainkit/blob/main/docs/cache.md): caches in memory, Redis, and Valkey,
26
+ and shared rate limits
27
+ - [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging, audit, idempotency, and evidence
19
28
  - [docs/versioning.md](https://github.com/karimmattar/domainkit/blob/main/docs/versioning.md): compatibility, deprecations, and releases
20
29
  - [docs/adr/](https://github.com/karimmattar/domainkit/tree/main/docs/adr): the decisions behind the design
21
30
  - [CHANGELOG.md](https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md) and [SECURITY.md](https://github.com/karimmattar/domainkit/blob/main/SECURITY.md)
@@ -64,24 +73,34 @@ poetry run pytest --cov # random order; coverage gate in pyprojec
64
73
  poetry run mutmut run "domainkit.http*" && poetry run mutmut results
65
74
  ```
66
75
 
67
- The database tests use SQLite unless `DOMAINKIT_TEST_POSTGRES_URL` names a
68
- PostgreSQL server; each test then creates a database there and drops it.
69
- Use a throwaway server:
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
79
+ them; each test cleans up after itself:
70
80
 
71
81
  ```bash
72
82
  docker run -d --rm --name domainkit-postgres -e POSTGRES_PASSWORD=postgres \
73
83
  -p 127.0.0.1:55432:5432 postgres:18-alpine
84
+ docker run -d --rm --name domainkit-redis -p 127.0.0.1:56379:6379 redis:8-alpine
74
85
  DOMAINKIT_TEST_POSTGRES_URL=postgresql+asyncpg://postgres:postgres@127.0.0.1:55432/postgres \
86
+ DOMAINKIT_TEST_REDIS_URL=redis://127.0.0.1:56379/0 \
75
87
  poetry run pytest
76
88
  ```
77
89
 
78
- CI (`.github/workflows/ci.yml`) runs the same checks on every push and pull
79
- request:
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
92
+ 98%; CI also combines the runs with and without servers, which must cover
93
+ 100%. To check that locally, run `pytest --cov` once without and once with
94
+ the servers, each with its own `COVERAGE_FILE`, then `coverage combine` and
95
+ `coverage report --fail-under=100`.
96
+
97
+ CI (`.github/workflows/ci.yml`) runs on every push and pull request:
80
98
 
81
99
  - lint and strict typing
82
100
  - the tests on Python 3.13 and 3.14
83
- - the tests again with PostgreSQL 18, including row locking, the audit
84
- triggers, and server-side timeouts
101
+ - 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
103
+ - coverage of both runs combined, which must be 100%
85
104
  - a wheel build, installed and imported in a clean environment
86
105
  - pip-audit, and a gitleaks scan of the whole history
87
106
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "domainkit"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "A Django-style framework for domain-driven FastAPI services."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -35,6 +35,10 @@ 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
+ # Ledgers for evidence (WORM): immudb's SDK, with its verified operations.
39
+ immudb = ["immudb-py (>=1.5.0,<1.6.0)"]
40
+ # Shared caches and rate limits: Redis or Valkey, through redis-py.
41
+ redis = ["redis (>=8.1.0,<9.0.0)"]
38
42
  # The domainkit.testing pytest plugin; Starlette's TestClient needs httpx2.
39
43
  testing = ["pytest (>=9.1.1,<10.0.0)", "httpx2 (>=2.13.0,<3.0.0)"]
40
44
 
@@ -66,6 +70,8 @@ dev = [
66
70
  "pip-audit (>=2.9.0,<3.0.0)",
67
71
  # Starlette's TestClient (it deprecates plain httpx).
68
72
  "httpx2 (>=2.13.0,<3.0.0)",
73
+ # An in-memory Redis, with Lua, so the Redis tests also run without a server.
74
+ "fakeredis[lua] (>=2.38.0,<3.0.0)",
69
75
  # In-memory span exporter for the tracing tests.
70
76
  "opentelemetry-sdk (>=1.45.1,<2.0.0)",
71
77
  # Random test order, property-based tests, mutation testing.
@@ -104,6 +110,11 @@ mypy_path = "src:."
104
110
  files = ["src", "domainkit_tests"]
105
111
  exclude = ["/management/templates/", "/mutants/"]
106
112
 
113
+ # immudb-py and grpcio ship no type information.
114
+ [[tool.mypy.overrides]]
115
+ module = ["immudb", "immudb.*", "grpc", "grpc.*", "ecdsa", "ecdsa.*"]
116
+ ignore_missing_imports = true
117
+
107
118
  [tool.pytest.ini_options]
108
119
  testpaths = ["domainkit_tests"]
109
120
  pythonpath = ["."]
@@ -119,6 +130,8 @@ source = ["src"]
119
130
  [tool.coverage.report]
120
131
  show_missing = true
121
132
  skip_empty = true
133
+ # Every run, with or without servers; CI's coverage job also requires 100%
134
+ # of all runs combined.
122
135
  fail_under = 98
123
136
  precision = 2
124
137
  exclude_also = ["if TYPE_CHECKING:", "raise NotImplementedError"]
@@ -11,6 +11,7 @@ from pydantic import BaseModel
11
11
  from domainkit.exceptions import ImproperlyConfigured
12
12
 
13
13
  if TYPE_CHECKING:
14
+ from domainkit.db.routing import DatabaseRouter
14
15
  from domainkit.ddd import DeferredTask, DomainEvent, UnitOfWork
15
16
 
16
17
  CONFIG_MODULE = "apps"
@@ -100,6 +101,15 @@ class DomainConfig:
100
101
  ) -> None:
101
102
  """Add rows to a committing SQL unit of work (outbox, audit trail)."""
102
103
 
104
+ def databases(self, router: "DatabaseRouter") -> frozenset[str]:
105
+ """The targets that hold this domain's tables (see ``DATABASE_ROUTING``).
106
+
107
+ By default, the domain's routing target. Apps whose rows must commit
108
+ with every domain's data (the outbox, the audit trail) return every
109
+ hosting target; apps without tables return none.
110
+ """
111
+ return frozenset({router.target_for(self.label)})
112
+
103
113
 
104
114
  def _import_domain(name: str) -> ModuleType:
105
115
  """Import a domain package, explaining a missing one."""
@@ -123,6 +123,13 @@ class DomainRegistry:
123
123
  + ", ".join(_describe_unknown(name, known) for name in unknown)
124
124
  + ". Declare them in a domain's settings_class or SETTINGS_CLASS."
125
125
  )
126
+ labels = list(self._configs)
127
+ if unrouted := sorted(set(current.DATABASE_ROUTING) - set(labels)):
128
+ raise ImproperlyConfigured(
129
+ "DATABASE_ROUTING names domains that are not installed: "
130
+ + ", ".join(_describe_unknown(label, labels) for label in unrouted)
131
+ + "."
132
+ )
126
133
 
127
134
 
128
135
  def _describe_unknown(name: str, known: list[str]) -> str:
@@ -35,6 +35,7 @@ FORBIDDEN_LIBRARIES: Final = {
35
35
  "starlette",
36
36
  "sqlalchemy",
37
37
  "alembic",
38
+ "redis",
38
39
  "domainkit.db",
39
40
  "domainkit.http",
40
41
  ),
@@ -0,0 +1,39 @@
1
+ """Caches: in process memory, or in Redis or Valkey shared by every replica.
2
+
3
+ Configure them in ``CACHES`` (``default`` is a memory cache unless set),
4
+ then use them anywhere in the application layer::
5
+
6
+ from domainkit.cache import cached, caches
7
+
8
+ summary = await caches["default"].get_or_set(
9
+ f"invoice:{invoice_id}", lambda: load_summary(invoice_id), InvoiceSummary
10
+ )
11
+
12
+ @cached(ttl=60)
13
+ async def invoice_summary(invoice_id: UUID) -> InvoiceSummary: ...
14
+
15
+ Values are JSON; a failing cache is skipped, not fatal. See ADR 0012 and
16
+ ``docs/cache.md``.
17
+ """
18
+
19
+ from domainkit.cache.base import CacheError, CacheUnavailable, Store
20
+ from domainkit.cache.core import DEFAULT_TTL, TTL, Cache
21
+ from domainkit.cache.decorators import CachedFunction, cached, invalidate_on
22
+ from domainkit.cache.handler import CacheHandler, caches
23
+ from domainkit.cache.memory import DummyStore, MemoryStore
24
+
25
+ __all__ = [
26
+ "DEFAULT_TTL",
27
+ "TTL",
28
+ "Cache",
29
+ "CacheError",
30
+ "CacheHandler",
31
+ "CacheUnavailable",
32
+ "CachedFunction",
33
+ "DummyStore",
34
+ "MemoryStore",
35
+ "Store",
36
+ "cached",
37
+ "caches",
38
+ "invalidate_on",
39
+ ]
@@ -0,0 +1,63 @@
1
+ """The store port: what a cache backend implements.
2
+
3
+ A store keeps bytes under full keys, with expiry in milliseconds. ``Cache``
4
+ (``domainkit.cache.core``) builds keys, encodes values, and handles failures
5
+ on top of it, so a new backend implements only these operations.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from collections.abc import Mapping, Sequence
10
+
11
+
12
+ class CacheError(Exception):
13
+ """Base class of cache errors."""
14
+
15
+
16
+ class CacheUnavailable(CacheError):
17
+ """The cache could not be reached in time, or is skipped after a failure."""
18
+
19
+
20
+ class Store(ABC):
21
+ """Bytes under full keys; ``ttl`` is in milliseconds, None keeps the entry.
22
+
23
+ Raise ``CacheUnavailable`` when the backend cannot be reached, and
24
+ ``CacheError`` when it refuses an operation.
25
+ """
26
+
27
+ @abstractmethod
28
+ async def get_many(self, keys: Sequence[str]) -> list[bytes | None]:
29
+ """Return the value of each key, or None where it is missing or expired."""
30
+
31
+ @abstractmethod
32
+ async def set_many(self, items: Mapping[str, bytes], ttl: int | None) -> None:
33
+ """Store every item, replacing existing values."""
34
+
35
+ @abstractmethod
36
+ async def add(self, key: str, value: bytes, ttl: int | None) -> bool:
37
+ """Store ``value`` only if ``key`` is missing; report whether it was."""
38
+
39
+ @abstractmethod
40
+ async def delete_many(self, keys: Sequence[str]) -> int:
41
+ """Delete the keys; return how many existed."""
42
+
43
+ @abstractmethod
44
+ async def incr(self, key: str, delta: int, ttl: int | None) -> int:
45
+ """Add ``delta`` to an integer value and return it.
46
+
47
+ A missing key starts at 0 and gets ``ttl``; an existing key keeps its
48
+ expiry. Raise ``CacheError`` if the value is not an integer.
49
+ """
50
+
51
+ @abstractmethod
52
+ async def touch(self, key: str, ttl: int | None) -> bool:
53
+ """Give ``key`` a new expiry (None: none); report whether it exists."""
54
+
55
+ @abstractmethod
56
+ async def clear(self, prefix: str) -> int:
57
+ """Delete every key starting with ``prefix``; return how many."""
58
+
59
+ async def check(self) -> None: # noqa: B027 - optional; most stores have it.
60
+ """Raise if the backend cannot be reached (readiness checks)."""
61
+
62
+ async def close(self) -> None: # noqa: B027 - optional, for connections.
63
+ """Release connections; the store is not used afterwards."""