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