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.
- {domainkit-0.6.2 → domainkit-0.7.0}/PKG-INFO +45 -8
- {domainkit-0.6.2 → domainkit-0.7.0}/README.md +35 -7
- {domainkit-0.6.2 → domainkit-0.7.0}/pyproject.toml +14 -1
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/apps/config.py +23 -1
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/architecture.py +4 -4
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/__init__.py +6 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/core.py +2 -2
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/memory.py +2 -2
- domainkit-0.7.0/src/domainkit/cache/pages.py +228 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/redis.py +135 -15
- domainkit-0.7.0/src/domainkit/clock.py +198 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/conf/__init__.py +2 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/conf/global_settings.py +206 -22
- domainkit-0.7.0/src/domainkit/contrib/admin/__init__.py +62 -0
- domainkit-0.7.0/src/domainkit/contrib/admin/sites.py +588 -0
- domainkit-0.7.0/src/domainkit/contrib/audit/api.py +148 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/apps.py +8 -2
- domainkit-0.7.0/src/domainkit/contrib/audit/history.py +206 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/management/commands/auditforget.py +3 -2
- domainkit-0.7.0/src/domainkit/contrib/audit/management/commands/auditlog.py +101 -0
- domainkit-0.7.0/src/domainkit/contrib/audit/models.py +21 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/personal.py +3 -2
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/recorder.py +2 -1
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/seals.py +4 -3
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/tables.py +7 -3
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/__init__.py +6 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/access.py +62 -12
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/admin.py +67 -95
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/api.py +80 -92
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/apps.py +153 -7
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/authentication.py +49 -11
- domainkit-0.7.0/src/domainkit/contrib/auth/certificates.py +188 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/cookies.py +384 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/erasure.py +44 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/events.py +59 -1
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/exceptions.py +4 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/management/commands/addcertificate.py +51 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/commands/createsuperuser.py +7 -2
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/commands/createtoken.py +41 -5
- domainkit-0.7.0/src/domainkit/contrib/auth/management/commands/resetmfa.py +42 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/mfa.py +479 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/middleware.py +14 -6
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/models.py +163 -15
- domainkit-0.7.0/src/domainkit/contrib/auth/onetime.py +119 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/personal_tokens.py +220 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/recovery.py +352 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/repositories.py +738 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/settings.py +291 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/signup.py +286 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/tables.py +75 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/tasks.py +6 -2
- domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/email_verification.subject.txt +1 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/email_verification.txt +7 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/password_reset.subject.txt +1 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/password_reset.txt +8 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/signup_confirm.subject.txt +1 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/signup_confirm.txt +8 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/signup_exists.subject.txt +1 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/templates/mail/auth/signup_exists.txt +7 -0
- domainkit-0.7.0/src/domainkit/contrib/auth/throttling.py +124 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/tokens.py +13 -2
- domainkit-0.7.0/src/domainkit/contrib/auth/totp.py +84 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/uow.py +12 -1
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/model.py +2 -1
- domainkit-0.7.0/src/domainkit/contrib/flags/__init__.py +55 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/admin.py +225 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/apps.py +32 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/dependencies.py +28 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/events.py +32 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/exceptions.py +15 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/management/__init__.py +1 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/management/commands/__init__.py +1 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/management/commands/flags.py +161 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/models.py +113 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/settings.py +18 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/store.py +208 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/tables.py +38 -0
- domainkit-0.7.0/src/domainkit/contrib/flags/uow.py +40 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/middleware.py +3 -2
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/operations.py +2 -2
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/tasks.py +3 -4
- domainkit-0.7.0/src/domainkit/contrib/mail/__init__.py +56 -0
- domainkit-0.7.0/src/domainkit/contrib/mail/apps.py +43 -0
- domainkit-0.7.0/src/domainkit/contrib/mail/backends.py +173 -0
- domainkit-0.7.0/src/domainkit/contrib/mail/delivery.py +113 -0
- domainkit-0.7.0/src/domainkit/contrib/mail/message.py +204 -0
- domainkit-0.7.0/src/domainkit/contrib/mail/settings.py +76 -0
- domainkit-0.7.0/src/domainkit/contrib/mail/templates.py +157 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/__init__.py +22 -5
- domainkit-0.7.0/src/domainkit/contrib/messaging/admin.py +191 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/apps.py +7 -8
- domainkit-0.7.0/src/domainkit/contrib/messaging/cron.py +272 -0
- domainkit-0.7.0/src/domainkit/contrib/messaging/management/commands/messages.py +125 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/metrics.py +18 -5
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/outbox.py +54 -11
- domainkit-0.7.0/src/domainkit/contrib/messaging/registry.py +399 -0
- domainkit-0.7.0/src/domainkit/contrib/messaging/retry.py +90 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/settings.py +4 -2
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/tables.py +17 -1
- domainkit-0.7.0/src/domainkit/contrib/messaging/testing.py +401 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/worker.py +230 -39
- domainkit-0.7.0/src/domainkit/contrib/notifications/__init__.py +65 -0
- domainkit-0.7.0/src/domainkit/contrib/notifications/apps.py +60 -0
- domainkit-0.7.0/src/domainkit/contrib/notifications/backends.py +107 -0
- domainkit-0.7.0/src/domainkit/contrib/notifications/delivery.py +134 -0
- domainkit-0.7.0/src/domainkit/contrib/notifications/messages.py +107 -0
- domainkit-0.7.0/src/domainkit/contrib/notifications/settings.py +24 -0
- domainkit-0.7.0/src/domainkit/contrib/objectpermissions/__init__.py +63 -0
- domainkit-0.7.0/src/domainkit/contrib/objectpermissions/apps.py +25 -0
- domainkit-0.7.0/src/domainkit/contrib/objectpermissions/backends.py +34 -0
- domainkit-0.7.0/src/domainkit/contrib/objectpermissions/criteria.py +178 -0
- domainkit-0.7.0/src/domainkit/contrib/objectpermissions/deletions.py +47 -0
- domainkit-0.7.0/src/domainkit/contrib/objectpermissions/models.py +146 -0
- domainkit-0.7.0/src/domainkit/contrib/objectpermissions/services.py +185 -0
- domainkit-0.7.0/src/domainkit/contrib/objectpermissions/tables.py +50 -0
- domainkit-0.7.0/src/domainkit/contrib/objectpermissions/uow.py +88 -0
- domainkit-0.7.0/src/domainkit/contrib/privacy/__init__.py +55 -0
- domainkit-0.7.0/src/domainkit/contrib/privacy/apps.py +38 -0
- domainkit-0.7.0/src/domainkit/contrib/privacy/events.py +18 -0
- domainkit-0.7.0/src/domainkit/contrib/privacy/management/__init__.py +1 -0
- domainkit-0.7.0/src/domainkit/contrib/privacy/management/commands/__init__.py +1 -0
- domainkit-0.7.0/src/domainkit/contrib/privacy/management/commands/privacy.py +121 -0
- domainkit-0.7.0/src/domainkit/contrib/privacy/models.py +334 -0
- domainkit-0.7.0/src/domainkit/contrib/privacy/operations.py +423 -0
- domainkit-0.7.0/src/domainkit/contrib/privacy/retention.py +53 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/__init__.py +35 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/admin.py +403 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/apps.py +67 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/client.py +133 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/delivery.py +529 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/guard.py +165 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/management/__init__.py +1 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/management/commands/__init__.py +1 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/management/commands/webhooks.py +133 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/models.py +240 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/settings.py +50 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/signing.py +114 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/tables.py +89 -0
- domainkit-0.7.0/src/domainkit/contrib/webhooks/uow.py +43 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/__init__.py +19 -1
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/connections.py +14 -3
- domainkit-0.7.0/src/domainkit/db/derive.py +282 -0
- domainkit-0.7.0/src/domainkit/db/errors.py +302 -0
- domainkit-0.7.0/src/domainkit/db/fixtures.py +392 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/migrations.py +131 -7
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/orm.py +30 -1
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/partitioning.py +2 -3
- domainkit-0.7.0/src/domainkit/db/query.py +232 -0
- domainkit-0.7.0/src/domainkit/db/repository.py +182 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/routing.py +31 -1
- domainkit-0.7.0/src/domainkit/db/squashing.py +358 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/uow.py +52 -4
- domainkit-0.7.0/src/domainkit/ddd/__init__.py +80 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/events.py +3 -2
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/exceptions.py +10 -0
- domainkit-0.7.0/src/domainkit/ddd/fields.py +341 -0
- domainkit-0.7.0/src/domainkit/ddd/messages.py +69 -0
- domainkit-0.7.0/src/domainkit/ddd/pagination.py +37 -0
- domainkit-0.7.0/src/domainkit/ddd/preconditions.py +111 -0
- domainkit-0.7.0/src/domainkit/ddd/query.py +804 -0
- domainkit-0.7.0/src/domainkit/ddd/repository.py +610 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/uow.py +101 -4
- domainkit-0.7.0/src/domainkit/debug/__init__.py +11 -0
- domainkit-0.7.0/src/domainkit/debug/middleware.py +123 -0
- domainkit-0.7.0/src/domainkit/debug/queries.py +117 -0
- domainkit-0.7.0/src/domainkit/encryption.py +90 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/__init__.py +20 -1
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/application.py +19 -2
- domainkit-0.7.0/src/domainkit/http/errors.py +464 -0
- domainkit-0.7.0/src/domainkit/http/filters.py +430 -0
- domainkit-0.7.0/src/domainkit/http/locale.py +51 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/middleware.py +14 -8
- domainkit-0.7.0/src/domainkit/http/openapi.py +551 -0
- domainkit-0.7.0/src/domainkit/http/pagination.py +561 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/ratelimit.py +193 -34
- domainkit-0.7.0/src/domainkit/http/uploads.py +298 -0
- domainkit-0.7.0/src/domainkit/http/versioning.py +204 -0
- domainkit-0.7.0/src/domainkit/i18n/__init__.py +68 -0
- domainkit-0.7.0/src/domainkit/i18n/catalogs.py +293 -0
- domainkit-0.7.0/src/domainkit/i18n/formats.py +308 -0
- domainkit-0.7.0/src/domainkit/i18n/locale/ar/LC_MESSAGES/messages.mo +0 -0
- domainkit-0.7.0/src/domainkit/i18n/locale/ar/LC_MESSAGES/messages.po +626 -0
- domainkit-0.7.0/src/domainkit/i18n/locale/en/LC_MESSAGES/messages.mo +0 -0
- domainkit-0.7.0/src/domainkit/i18n/locale/en/LC_MESSAGES/messages.po +620 -0
- domainkit-0.7.0/src/domainkit/i18n/timezone.py +90 -0
- domainkit-0.7.0/src/domainkit/i18n/translation.py +271 -0
- domainkit-0.7.0/src/domainkit/i18n/validation.py +54 -0
- domainkit-0.7.0/src/domainkit/management/commands/addcommand.py +358 -0
- domainkit-0.7.0/src/domainkit/management/commands/addevent.py +197 -0
- domainkit-0.7.0/src/domainkit/management/commands/addquery.py +289 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/check.py +3 -0
- domainkit-0.7.0/src/domainkit/management/commands/compilemessages.py +101 -0
- domainkit-0.7.0/src/domainkit/management/commands/dbshell.py +140 -0
- domainkit-0.7.0/src/domainkit/management/commands/diffsettings.py +67 -0
- domainkit-0.7.0/src/domainkit/management/commands/dumpdata.py +86 -0
- domainkit-0.7.0/src/domainkit/management/commands/flush.py +57 -0
- domainkit-0.7.0/src/domainkit/management/commands/loaddata.py +90 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/makemigrations.py +25 -1
- domainkit-0.7.0/src/domainkit/management/commands/migrate.py +147 -0
- domainkit-0.7.0/src/domainkit/management/commands/openapi.py +111 -0
- domainkit-0.7.0/src/domainkit/management/commands/seed.py +39 -0
- domainkit-0.7.0/src/domainkit/management/commands/shell.py +147 -0
- domainkit-0.7.0/src/domainkit/management/commands/squashmigrations.py +57 -0
- domainkit-0.7.0/src/domainkit/management/commands/startaggregate.py +399 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/startdomain.py +40 -52
- domainkit-0.7.0/src/domainkit/management/generators.py +832 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/__init__.py-tpl +2 -2
- domainkit-0.7.0/src/domainkit/management/templates/domain/package/api/routes.py-tpl +89 -0
- domainkit-0.7.0/src/domainkit/management/templates/domain/package/application/commands.py-tpl +38 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/application/handlers.py-tpl +0 -1
- domainkit-0.7.0/src/domainkit/management/templates/domain/package/application/services.py-tpl +62 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/application/unit_of_work.py-tpl +1 -1
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/apps.py-tpl +0 -3
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/domain/events.py-tpl +13 -0
- domainkit-0.7.0/src/domainkit/management/templates/domain/package/domain/model.py-tpl +122 -0
- domainkit-0.7.0/src/domainkit/management/templates/domain/package/infrastructure/orm.py-tpl +44 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/infrastructure/unit_of_work.py-tpl +11 -3
- domainkit-0.7.0/src/domainkit/management/templates/domain/tests/fakes.py-tpl +54 -0
- domainkit-0.7.0/src/domainkit/management/templates/domain/tests/test_api.py-tpl +110 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/tests/test_model.py-tpl +52 -11
- domainkit-0.7.0/src/domainkit/management/templates/domain/tests/test_services.py-tpl +98 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/pyproject.toml-tpl +2 -1
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/settings/base.py-tpl +9 -0
- domainkit-0.7.0/src/domainkit/management/templates/project/src/config/settings/local.py-tpl +16 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/settings/production.py-tpl +12 -6
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/settings/test.py-tpl +1 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/tests/test_health.py-tpl +9 -1
- domainkit-0.7.0/src/domainkit/resources/__init__.py +59 -0
- domainkit-0.7.0/src/domainkit/resources/access.py +102 -0
- domainkit-0.7.0/src/domainkit/resources/batches.py +121 -0
- domainkit-0.7.0/src/domainkit/resources/conditional.py +64 -0
- domainkit-0.7.0/src/domainkit/resources/events.py +34 -0
- domainkit-0.7.0/src/domainkit/resources/policies.py +168 -0
- domainkit-0.7.0/src/domainkit/resources/renderers.py +174 -0
- domainkit-0.7.0/src/domainkit/resources/routing.py +1768 -0
- domainkit-0.7.0/src/domainkit/resources/schemas.py +337 -0
- domainkit-0.7.0/src/domainkit/signing.py +314 -0
- domainkit-0.7.0/src/domainkit/storage/__init__.py +43 -0
- domainkit-0.7.0/src/domainkit/storage/base.py +226 -0
- domainkit-0.7.0/src/domainkit/storage/filesystem.py +271 -0
- domainkit-0.7.0/src/domainkit/storage/handler.py +60 -0
- domainkit-0.7.0/src/domainkit/storage/memory.py +63 -0
- domainkit-0.7.0/src/domainkit/testing/__init__.py +215 -0
- domainkit-0.7.0/src/domainkit/testing/clients.py +274 -0
- domainkit-0.7.0/src/domainkit/testing/databases.py +259 -0
- domainkit-0.7.0/src/domainkit/testing/factories.py +313 -0
- domainkit-0.7.0/src/domainkit/testing/queries.py +124 -0
- domainkit-0.7.0/src/domainkit/tracing.py +150 -0
- domainkit-0.7.0/src/domainkit/utils/headers.py +85 -0
- domainkit-0.7.0/src/domainkit/utils/redis_urls.py +143 -0
- domainkit-0.6.2/src/domainkit/contrib/audit/management/commands/auditlog.py +0 -53
- domainkit-0.6.2/src/domainkit/contrib/auth/repositories.py +0 -361
- domainkit-0.6.2/src/domainkit/contrib/auth/settings.py +0 -137
- domainkit-0.6.2/src/domainkit/contrib/messaging/admin.py +0 -62
- domainkit-0.6.2/src/domainkit/contrib/messaging/management/commands/messages.py +0 -58
- domainkit-0.6.2/src/domainkit/contrib/messaging/registry.py +0 -215
- domainkit-0.6.2/src/domainkit/db/errors.py +0 -112
- domainkit-0.6.2/src/domainkit/db/repository.py +0 -33
- domainkit-0.6.2/src/domainkit/ddd/__init__.py +0 -48
- domainkit-0.6.2/src/domainkit/ddd/messages.py +0 -16
- domainkit-0.6.2/src/domainkit/ddd/pagination.py +0 -24
- domainkit-0.6.2/src/domainkit/ddd/repository.py +0 -94
- domainkit-0.6.2/src/domainkit/http/errors.py +0 -234
- domainkit-0.6.2/src/domainkit/http/pagination.py +0 -71
- domainkit-0.6.2/src/domainkit/management/commands/migrate.py +0 -88
- domainkit-0.6.2/src/domainkit/management/commands/shell.py +0 -27
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/api/dependencies.py-tpl +0 -47
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/api/routes.py-tpl +0 -102
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/api/schemas.py-tpl +0 -65
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/application/commands.py-tpl +0 -24
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/application/services.py-tpl +0 -63
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/domain/exceptions.py-tpl +0 -15
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/domain/model.py-tpl +0 -71
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/domain/repositories.py-tpl +0 -33
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/infrastructure/orm.py-tpl +0 -46
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/infrastructure/repositories.py-tpl +0 -26
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/management/__init__.py-tpl +0 -1
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/management/commands/__init__.py-tpl +0 -1
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/management/commands/{{ aggregates }}.py-tpl +0 -25
- domainkit-0.6.2/src/domainkit/management/templates/domain/package/settings.py-tpl +0 -30
- domainkit-0.6.2/src/domainkit/management/templates/domain/tests/fakes.py-tpl +0 -47
- domainkit-0.6.2/src/domainkit/management/templates/domain/tests/test_api.py-tpl +0 -121
- domainkit-0.6.2/src/domainkit/management/templates/domain/tests/test_services.py-tpl +0 -71
- domainkit-0.6.2/src/domainkit/management/templates/domain/tests/test_settings.py-tpl +0 -20
- domainkit-0.6.2/src/domainkit/management/templates/project/src/config/settings/local.py-tpl +0 -11
- domainkit-0.6.2/src/domainkit/testing.py +0 -179
- domainkit-0.6.2/src/domainkit/tracing.py +0 -42
- {domainkit-0.6.2 → domainkit-0.7.0}/LICENSE +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/apps/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/apps/registry.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/broker/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/broker/base.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/broker/handler.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/broker/memory.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/broker/rabbitmq.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/base.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/decorators.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/handler.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/cache/metrics.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/conf/env.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/conf/lazy.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/context.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/management/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/management/commands/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/management/commands/auditseal.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/management/commands/auditverify.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/protection.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/settings.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/audit/tasks.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/backends.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/common-passwords.NOTICE +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/common-passwords.txt.gz +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/hashers.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/commands/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/commands/changepassword.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/management/commands/cleartokens.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/permissions.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/users.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/auth/validators.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/apps.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/models.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/registry.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/sync.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/contenttypes/tables.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/apps.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/management/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/management/commands/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/management/commands/evidence.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/settings.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/store.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/evidence/tasks.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/apps.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/settings.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/idempotency/tables.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/dispatch.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/management/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/management/commands/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/contrib/messaging/management/commands/runworker.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/drivers.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/migration_env/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/migration_env/env.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/migration_env/script.py.mako +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/snapshots.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/db/types.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/_dataclass.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/model.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ddd/personal.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/deprecation.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/exceptions.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/health.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/http/metrics.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ledger/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ledger/base.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ledger/handler.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ledger/immudb.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/ledger/memory.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/lifecycle.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/log.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/base.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/broker.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/cache.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/init.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/partitions.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/runserver.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/commands/showmigrations.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/api/__init__.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/application/__init__.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/domain/__init__.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/package/infrastructure/__init__.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/domain/tests/__init__.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/dot-env-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/dot-gitignore-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/manage.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/__init__.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/asgi.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/settings/__init__.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/config/urls.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/src/domains/__init__.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/management/templates/project/tests/__init__.py-tpl +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/py.typed +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/reporting.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/__init__.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/closing.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/formatting.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/module_loading.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/rates.py +0 -0
- {domainkit-0.6.2 → domainkit-0.7.0}/src/domainkit/utils/readiness.py +0 -0
- {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.
|
|
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
|
|
62
|
-
rate limits shared by every
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 #
|
|
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
|
|
11
|
-
rate limits shared by every
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 #
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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] =
|
|
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)
|