domainkit 0.6.0__tar.gz → 0.6.2__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.0 → domainkit-0.6.2}/PKG-INFO +5 -4
- {domainkit-0.6.0 → domainkit-0.6.2}/README.md +4 -3
- {domainkit-0.6.0 → domainkit-0.6.2}/pyproject.toml +1 -1
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/conf/env.py +20 -9
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/conf/global_settings.py +58 -8
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/context.py +6 -1
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/management/commands/auditverify.py +2 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/personal.py +4 -2
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/recorder.py +9 -5
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/seals.py +51 -14
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/settings.py +1 -1
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/__init__.py +2 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/access.py +101 -29
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/admin.py +34 -2
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/api.py +65 -11
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/apps.py +6 -2
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/authentication.py +16 -2
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/backends.py +12 -7
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/events.py +7 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/exceptions.py +5 -1
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/hashers.py +6 -2
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/middleware.py +65 -1
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/repositories.py +32 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/settings.py +3 -3
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/tokens.py +8 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/evidence/model.py +2 -3
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/evidence/settings.py +1 -1
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/evidence/store.py +20 -3
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/idempotency/middleware.py +51 -19
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/idempotency/settings.py +8 -1
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/registry.py +12 -6
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/settings.py +1 -1
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/worker.py +36 -3
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/connections.py +60 -15
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/drivers.py +14 -3
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/migrations.py +14 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/snapshots.py +16 -6
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/types.py +183 -33
- domainkit-0.6.2/src/domainkit/ddd/personal.py +94 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/http/errors.py +55 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/http/health.py +24 -6
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/http/metrics.py +5 -1
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/http/middleware.py +136 -21
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/http/ratelimit.py +56 -27
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/makemigrations.py +4 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/startdomain.py +22 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/src/config/settings/production.py-tpl +9 -3
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/testing.py +16 -5
- domainkit-0.6.0/src/domainkit/ddd/personal.py +0 -46
- {domainkit-0.6.0 → domainkit-0.6.2}/LICENSE +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/apps/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/apps/config.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/apps/registry.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/architecture.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/broker/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/broker/base.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/broker/handler.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/broker/memory.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/broker/rabbitmq.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/cache/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/cache/base.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/cache/core.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/cache/decorators.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/cache/handler.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/cache/memory.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/cache/metrics.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/cache/redis.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/conf/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/conf/lazy.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/apps.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/management/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/management/commands/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/management/commands/auditforget.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/management/commands/auditlog.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/management/commands/auditseal.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/protection.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/tables.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/tasks.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/common-passwords.NOTICE +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/common-passwords.txt.gz +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/management/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/management/commands/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/management/commands/changepassword.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/management/commands/cleartokens.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/management/commands/createsuperuser.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/management/commands/createtoken.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/models.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/permissions.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/tables.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/tasks.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/uow.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/users.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/auth/validators.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/contenttypes/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/contenttypes/apps.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/contenttypes/models.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/contenttypes/registry.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/contenttypes/sync.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/contenttypes/tables.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/evidence/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/evidence/apps.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/evidence/management/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/evidence/management/commands/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/evidence/management/commands/evidence.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/evidence/tasks.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/idempotency/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/idempotency/apps.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/idempotency/operations.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/idempotency/tables.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/idempotency/tasks.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/admin.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/apps.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/dispatch.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/management/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/management/commands/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/management/commands/messages.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/management/commands/runworker.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/metrics.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/outbox.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/messaging/tables.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/errors.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/migration_env/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/migration_env/env.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/migration_env/script.py.mako +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/orm.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/partitioning.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/repository.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/routing.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/db/uow.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ddd/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ddd/_dataclass.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ddd/events.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ddd/exceptions.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ddd/messages.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ddd/model.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ddd/pagination.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ddd/repository.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ddd/uow.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/deprecation.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/exceptions.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/http/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/http/application.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/http/pagination.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ledger/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ledger/base.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ledger/handler.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ledger/immudb.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/ledger/memory.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/lifecycle.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/log.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/base.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/broker.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/cache.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/check.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/init.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/migrate.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/partitions.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/runserver.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/shell.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/commands/showmigrations.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/api/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/api/dependencies.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/api/routes.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/api/schemas.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/application/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/application/commands.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/application/handlers.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/application/services.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/application/unit_of_work.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/apps.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/domain/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/domain/events.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/domain/exceptions.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/domain/model.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/domain/repositories.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/infrastructure/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/infrastructure/orm.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/infrastructure/repositories.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/infrastructure/unit_of_work.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/management/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/management/commands/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/management/commands/{{ aggregates }}.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/package/settings.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/tests/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/tests/fakes.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/tests/test_api.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/tests/test_model.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/tests/test_services.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/domain/tests/test_settings.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/dot-env-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/dot-gitignore-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/manage.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/pyproject.toml-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/src/config/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/src/config/asgi.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/src/config/settings/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/src/config/settings/base.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/src/config/settings/local.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/src/config/settings/test.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/src/config/urls.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/src/domains/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/tests/__init__.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/management/templates/project/tests/test_health.py-tpl +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/py.typed +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/reporting.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/tracing.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/utils/__init__.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/utils/closing.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/utils/formatting.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/utils/module_loading.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/utils/rates.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/utils/readiness.py +0 -0
- {domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/utils/text.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: domainkit
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: A Django-style framework for domain-driven FastAPI services.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -124,9 +124,10 @@ billing/
|
|
|
124
124
|
tests/test_health.py
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
The production settings read `BILLING_API_ALLOWED_HOSTS
|
|
128
|
-
`BILLING_API_DATABASE_URL
|
|
129
|
-
|
|
127
|
+
The production settings read `BILLING_API_ALLOWED_HOSTS`,
|
|
128
|
+
`BILLING_API_DATABASE_URL`, and `BILLING_API_TRUSTED_PROXIES` (the load
|
|
129
|
+
balancers' networks, or empty) from the environment, and the deployment
|
|
130
|
+
checks refuse to start without them, or without verified TLS to the database. They
|
|
130
131
|
also turn the API docs off, send HSTS for a year, and limit each caller to
|
|
131
132
|
300 requests a minute; `BILLING_API_API_DOCS`,
|
|
132
133
|
`BILLING_API_SECURE_HSTS_SECONDS`, and `BILLING_API_RATE_LIMIT` change that.
|
|
@@ -73,9 +73,10 @@ billing/
|
|
|
73
73
|
tests/test_health.py
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
The production settings read `BILLING_API_ALLOWED_HOSTS
|
|
77
|
-
`BILLING_API_DATABASE_URL
|
|
78
|
-
|
|
76
|
+
The production settings read `BILLING_API_ALLOWED_HOSTS`,
|
|
77
|
+
`BILLING_API_DATABASE_URL`, and `BILLING_API_TRUSTED_PROXIES` (the load
|
|
78
|
+
balancers' networks, or empty) from the environment, and the deployment
|
|
79
|
+
checks refuse to start without them, or without verified TLS to the database. They
|
|
79
80
|
also turn the API docs off, send HSTS for a year, and limit each caller to
|
|
80
81
|
300 requests a minute; `BILLING_API_API_DOCS`,
|
|
81
82
|
`BILLING_API_SECURE_HSTS_SECONDS`, and `BILLING_API_RATE_LIMIT` change that.
|
|
@@ -18,6 +18,7 @@ is not expanded.
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import builtins
|
|
21
|
+
import math
|
|
21
22
|
import os
|
|
22
23
|
import re
|
|
23
24
|
import tomllib
|
|
@@ -188,7 +189,7 @@ class Env:
|
|
|
188
189
|
text = self.raw(name)
|
|
189
190
|
if text is None:
|
|
190
191
|
return self._default(name, default)
|
|
191
|
-
return self._convert(name, text,
|
|
192
|
+
return self._convert(name, text, _finite, "a finite number")
|
|
192
193
|
|
|
193
194
|
def bool[D](
|
|
194
195
|
self, name: builtins.str, default: D | _Missing = MISSING
|
|
@@ -202,7 +203,7 @@ class Env:
|
|
|
202
203
|
return True
|
|
203
204
|
if normalized in _FALSE:
|
|
204
205
|
return False
|
|
205
|
-
raise self._invalid(name,
|
|
206
|
+
raise self._invalid(name, "a boolean (true/false)")
|
|
206
207
|
|
|
207
208
|
def list[D](
|
|
208
209
|
self, name: builtins.str, default: D | _Missing = MISSING
|
|
@@ -243,16 +244,26 @@ class Env:
|
|
|
243
244
|
"""Convert with ``cast`` or explain which variable is invalid."""
|
|
244
245
|
try:
|
|
245
246
|
return cast(text)
|
|
246
|
-
except ValueError
|
|
247
|
-
|
|
247
|
+
except ValueError:
|
|
248
|
+
# "from None": the ValueError would quote the value, maybe a secret.
|
|
249
|
+
raise self._invalid(name, expected) from None
|
|
248
250
|
|
|
249
251
|
def _invalid(
|
|
250
|
-
self, name: builtins.str,
|
|
252
|
+
self, name: builtins.str, expected: builtins.str
|
|
251
253
|
) -> ImproperlyConfigured:
|
|
252
|
-
"""Build the error for a value that cannot be converted.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
"""Build the error for a value that cannot be converted.
|
|
255
|
+
|
|
256
|
+
The value is not quoted: it may be a secret read from ``<NAME>_FILE``.
|
|
257
|
+
"""
|
|
258
|
+
return ImproperlyConfigured(f"{self.prefix}{name} must be {expected}.")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _finite(text: builtins.str) -> builtins.float:
|
|
262
|
+
"""Read a number; infinity and NaN are no setting's value."""
|
|
263
|
+
value = builtins.float(text)
|
|
264
|
+
if not math.isfinite(value):
|
|
265
|
+
raise ValueError # _convert explains it, without the value.
|
|
266
|
+
return value
|
|
256
267
|
|
|
257
268
|
|
|
258
269
|
def _from_root(path: Path) -> Path:
|
|
@@ -6,10 +6,10 @@ the framework does not declare must belong to an installed domain (see
|
|
|
6
6
|
so a misspelled setting fails at startup instead of being silently ignored.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from collections.abc import Callable
|
|
9
|
+
from collections.abc import Callable, Iterator
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Final, Literal, Self
|
|
12
|
-
from urllib.parse import parse_qs, urlsplit
|
|
12
|
+
from urllib.parse import SplitResult, parse_qs, urlsplit
|
|
13
13
|
|
|
14
14
|
from pydantic import (
|
|
15
15
|
BaseModel,
|
|
@@ -17,6 +17,8 @@ from pydantic import (
|
|
|
17
17
|
Field,
|
|
18
18
|
IPvAnyNetwork,
|
|
19
19
|
SecretStr,
|
|
20
|
+
SerializerFunctionWrapHandler,
|
|
21
|
+
model_serializer,
|
|
20
22
|
model_validator,
|
|
21
23
|
)
|
|
22
24
|
|
|
@@ -24,6 +26,8 @@ from domainkit.conf.env import project_root
|
|
|
24
26
|
from domainkit.utils.rates import parse_rate
|
|
25
27
|
|
|
26
28
|
DEFAULT_DB_ALIAS: Final = "default"
|
|
29
|
+
HIDDEN: Final = "**********"
|
|
30
|
+
"""What ``repr`` and dumps of the settings show for domain settings."""
|
|
27
31
|
|
|
28
32
|
Environment = Literal["local", "test", "staging", "production"]
|
|
29
33
|
DEPLOYED_ENVIRONMENTS: Final = frozenset({"staging", "production"})
|
|
@@ -61,7 +65,7 @@ IMMUDB_PORT: Final = 3322
|
|
|
61
65
|
class DatabaseConfig(BaseModel):
|
|
62
66
|
"""One ``DATABASES`` entry: an SQLAlchemy async URL plus connection options."""
|
|
63
67
|
|
|
64
|
-
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
68
|
+
model_config = ConfigDict(frozen=True, extra="forbid", allow_inf_nan=False)
|
|
65
69
|
|
|
66
70
|
URL: SecretStr
|
|
67
71
|
"""``dialect+driver://user:password@host/name``; hidden in reprs and errors."""
|
|
@@ -111,6 +115,21 @@ class DatabaseConfig(BaseModel):
|
|
|
111
115
|
return self
|
|
112
116
|
|
|
113
117
|
|
|
118
|
+
def _check_port(parts: SplitResult) -> None:
|
|
119
|
+
"""Refuse a URL whose port is not a number, without quoting it.
|
|
120
|
+
|
|
121
|
+
A ``/`` in the password ends the host early, so the port Python reads is
|
|
122
|
+
part of the password; its own error would quote it.
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
parts.port # noqa: B018 - reading it parses it.
|
|
126
|
+
except ValueError:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"the URL's port is not a number; percent-encode / ? # @ and : in "
|
|
129
|
+
"the user name and password"
|
|
130
|
+
) from None
|
|
131
|
+
|
|
132
|
+
|
|
114
133
|
class LedgerConfig(BaseModel):
|
|
115
134
|
"""One ``LEDGERS`` entry: write once, read many, verified on the client.
|
|
116
135
|
|
|
@@ -118,7 +137,7 @@ class LedgerConfig(BaseModel):
|
|
|
118
137
|
migrated and never take part in a unit of work.
|
|
119
138
|
"""
|
|
120
139
|
|
|
121
|
-
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
140
|
+
model_config = ConfigDict(frozen=True, extra="forbid", allow_inf_nan=False)
|
|
122
141
|
|
|
123
142
|
URL: SecretStr
|
|
124
143
|
"""``immudb://user:password@host:3322/database``, or ``memory://`` for tests."""
|
|
@@ -135,6 +154,7 @@ class LedgerConfig(BaseModel):
|
|
|
135
154
|
parts = urlsplit(self.URL.get_secret_value())
|
|
136
155
|
if parts.scheme not in LEDGER_SCHEMES:
|
|
137
156
|
raise ValueError("a ledger URL starts with immudb:// or memory://")
|
|
157
|
+
_check_port(parts)
|
|
138
158
|
if parts.scheme == "immudb" and not parts.hostname:
|
|
139
159
|
raise ValueError("an immudb URL needs a host: immudb://user:pw@host/db")
|
|
140
160
|
return self
|
|
@@ -152,7 +172,7 @@ class ShardConfig(BaseModel):
|
|
|
152
172
|
to one database. Moving buckets between databases is how a group grows.
|
|
153
173
|
"""
|
|
154
174
|
|
|
155
|
-
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
175
|
+
model_config = ConfigDict(frozen=True, extra="forbid", allow_inf_nan=False)
|
|
156
176
|
|
|
157
177
|
DATABASES: tuple[str, ...] = Field(min_length=1)
|
|
158
178
|
"""The group's SQL aliases; ``makemigrations`` compares against the first."""
|
|
@@ -224,7 +244,7 @@ class CacheConfig(BaseModel):
|
|
|
224
244
|
``get_or_set`` and ``@cached`` compute values instead (see ADR 0012).
|
|
225
245
|
"""
|
|
226
246
|
|
|
227
|
-
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
247
|
+
model_config = ConfigDict(frozen=True, extra="forbid", allow_inf_nan=False)
|
|
228
248
|
|
|
229
249
|
URL: SecretStr = SecretStr("memory://")
|
|
230
250
|
"""``memory://`` (this process), ``redis://``/``rediss://`` (Redis, Valkey), ``dummy://``."""
|
|
@@ -264,6 +284,7 @@ class CacheConfig(BaseModel):
|
|
|
264
284
|
raise ValueError(
|
|
265
285
|
"a cache URL starts with memory://, redis://, rediss://, or dummy://"
|
|
266
286
|
)
|
|
287
|
+
_check_port(parts)
|
|
267
288
|
if parts.scheme in REDIS_SCHEMES:
|
|
268
289
|
if not parts.hostname:
|
|
269
290
|
raise ValueError("a Redis URL needs a host: redis://host:6379/0")
|
|
@@ -318,7 +339,7 @@ class BrokerConfig(BaseModel):
|
|
|
318
339
|
never drops them (see ADR 0014).
|
|
319
340
|
"""
|
|
320
341
|
|
|
321
|
-
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
342
|
+
model_config = ConfigDict(frozen=True, extra="forbid", allow_inf_nan=False)
|
|
322
343
|
|
|
323
344
|
URL: SecretStr
|
|
324
345
|
"""``amqps://user:password@host:5671/vhost`` (RabbitMQ), or ``memory://``."""
|
|
@@ -340,6 +361,7 @@ class BrokerConfig(BaseModel):
|
|
|
340
361
|
parts = urlsplit(self.URL.get_secret_value())
|
|
341
362
|
if parts.scheme not in BROKER_SCHEMES:
|
|
342
363
|
raise ValueError("a broker URL starts with amqp://, amqps://, or memory://")
|
|
364
|
+
_check_port(parts)
|
|
343
365
|
if parts.scheme != "memory":
|
|
344
366
|
if not parts.hostname:
|
|
345
367
|
raise ValueError("an AMQP URL needs a host: amqps://user:pw@host/vhost")
|
|
@@ -380,7 +402,7 @@ class BrokerConfig(BaseModel):
|
|
|
380
402
|
class Settings(BaseModel):
|
|
381
403
|
"""Validated, read-only settings; extra names are checked at ``setup()``."""
|
|
382
404
|
|
|
383
|
-
model_config = ConfigDict(frozen=True, extra="allow")
|
|
405
|
+
model_config = ConfigDict(frozen=True, extra="allow", allow_inf_nan=False)
|
|
384
406
|
|
|
385
407
|
SETTINGS_MODULE: str = ""
|
|
386
408
|
"""Set by the loader to the module that produced these settings."""
|
|
@@ -466,6 +488,23 @@ class Settings(BaseModel):
|
|
|
466
488
|
)
|
|
467
489
|
CORS_ALLOW_CREDENTIALS: bool = False
|
|
468
490
|
|
|
491
|
+
def __repr_args__(self) -> Iterator[tuple[str | None, Any]]:
|
|
492
|
+
"""Show declared settings; domain settings, which may hold keys, hidden."""
|
|
493
|
+
domain = self.model_extra or {}
|
|
494
|
+
for name, value in super().__repr_args__():
|
|
495
|
+
yield name, HIDDEN if name in domain else value
|
|
496
|
+
|
|
497
|
+
@model_serializer(mode="wrap")
|
|
498
|
+
def _hide_domain_settings(
|
|
499
|
+
self, handler: SerializerFunctionWrapHandler
|
|
500
|
+
) -> dict[str, Any]:
|
|
501
|
+
"""Dump domain settings hidden: they are checked, and typed, elsewhere."""
|
|
502
|
+
dumped: dict[str, Any] = handler(self)
|
|
503
|
+
return {
|
|
504
|
+
name: HIDDEN if name in (self.model_extra or {}) else value
|
|
505
|
+
for name, value in dumped.items()
|
|
506
|
+
}
|
|
507
|
+
|
|
469
508
|
@model_validator(mode="after")
|
|
470
509
|
def check_consistency(self) -> Self:
|
|
471
510
|
"""Reject combinations that are unsafe or would fail later at runtime."""
|
|
@@ -534,6 +573,12 @@ class Settings(BaseModel):
|
|
|
534
573
|
f"Requests are not rate limited; set RATE_LIMIT and list "
|
|
535
574
|
f"{RATE_LIMIT_MIDDLEWARE} in MIDDLEWARE."
|
|
536
575
|
)
|
|
576
|
+
elif not self.TRUSTED_PROXIES:
|
|
577
|
+
warnings.append(
|
|
578
|
+
"TRUSTED_PROXIES is empty, so callers are rate limited by the "
|
|
579
|
+
"address they connect from: behind a load balancer or proxy, "
|
|
580
|
+
"they all share its count. List its networks in TRUSTED_PROXIES."
|
|
581
|
+
)
|
|
537
582
|
return warnings
|
|
538
583
|
|
|
539
584
|
def _check_deployment(self) -> None:
|
|
@@ -543,6 +588,11 @@ class Settings(BaseModel):
|
|
|
543
588
|
raise ValueError(f"DEBUG must be False {where}")
|
|
544
589
|
if "*" in self.ALLOWED_HOSTS:
|
|
545
590
|
raise ValueError(f"ALLOWED_HOSTS must list real host names {where}")
|
|
591
|
+
if any(network.prefixlen == 0 for network in self.TRUSTED_PROXIES):
|
|
592
|
+
raise ValueError(
|
|
593
|
+
f"TRUSTED_PROXIES must list the proxies' networks, not every "
|
|
594
|
+
f"address {where}: any caller could choose its own address"
|
|
595
|
+
)
|
|
546
596
|
rules: dict[str, Callable[[DatabaseConfig], bool]] = {
|
|
547
597
|
"ECHO logs every statement; disable it": lambda db: db.ECHO,
|
|
548
598
|
"LOG_PARAMETERS exposes bound values; disable it": lambda db: (
|
|
@@ -8,6 +8,10 @@ from collections.abc import Iterator
|
|
|
8
8
|
from contextlib import contextmanager
|
|
9
9
|
from contextvars import ContextVar
|
|
10
10
|
from dataclasses import dataclass
|
|
11
|
+
from typing import Final
|
|
12
|
+
|
|
13
|
+
SYSTEM_ACTOR: Final = "system"
|
|
14
|
+
"""The actor of work no caller asked for: periodic tasks, commands' messages."""
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
@dataclass(frozen=True, slots=True)
|
|
@@ -15,7 +19,8 @@ class RequestContext:
|
|
|
15
19
|
"""Facts about the request or job being served; no personal data.
|
|
16
20
|
|
|
17
21
|
``actor`` names who is acting. Authentication sets it; None means an
|
|
18
|
-
anonymous caller, and background work carries the actor that queued it
|
|
22
|
+
anonymous caller, and background work carries the actor that queued it,
|
|
23
|
+
or ``SYSTEM_ACTOR`` when no request did.
|
|
19
24
|
"""
|
|
20
25
|
|
|
21
26
|
request_id: str
|
{domainkit-0.6.0 → domainkit-0.6.2}/src/domainkit/contrib/audit/management/commands/auditverify.py
RENAMED
|
@@ -52,6 +52,8 @@ def _summary(report: Report) -> str:
|
|
|
52
52
|
):
|
|
53
53
|
if found:
|
|
54
54
|
lines.append(f" {name}: {', '.join(str(each) for each in found)}")
|
|
55
|
+
if report.unanchored:
|
|
56
|
+
lines.append(f" seals left unanchored: {report.unanchored}")
|
|
55
57
|
if report.unsealed:
|
|
56
58
|
lines.append(f" unsealed rows: {report.unsealed}")
|
|
57
59
|
return "\n".join(lines)
|
|
@@ -52,7 +52,8 @@ def pseudonym(key: bytes, field: str, value: object) -> str:
|
|
|
52
52
|
"""``value``, the ``field`` of a subject, as a pseudonym under their ``key``.
|
|
53
53
|
|
|
54
54
|
The field's name is part of it, so equal values in different fields do
|
|
55
|
-
not give away that they are equal.
|
|
55
|
+
not give away that they are equal. Lone surrogates in strings are
|
|
56
|
+
encoded as they are, so every value has a pseudonym.
|
|
56
57
|
"""
|
|
57
58
|
data = json.dumps(
|
|
58
59
|
to_jsonable_python(value),
|
|
@@ -60,7 +61,8 @@ def pseudonym(key: bytes, field: str, value: object) -> str:
|
|
|
60
61
|
separators=(",", ":"),
|
|
61
62
|
ensure_ascii=False,
|
|
62
63
|
)
|
|
63
|
-
|
|
64
|
+
message = f"{field}\0{data}".encode("utf-8", "surrogatepass")
|
|
65
|
+
digest = hmac.new(key, message, hashlib.sha256).hexdigest()
|
|
64
66
|
return f"{PREFIX}{digest[:DIGITS]}"
|
|
65
67
|
|
|
66
68
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Building and writing audit rows.
|
|
2
2
|
|
|
3
3
|
Who acted comes from the request context: the authenticated actor,
|
|
4
|
-
``anonymous`` for unauthenticated requests, ``system`` outside requests
|
|
4
|
+
``anonymous`` for unauthenticated requests, ``system`` outside requests and
|
|
5
|
+
in work no request queued (periodic tasks).
|
|
5
6
|
Never put secrets in event fields or ``details``: they are kept for good.
|
|
6
7
|
|
|
7
8
|
Personal values are written as pseudonyms (see ``personal``): the client's
|
|
@@ -22,7 +23,7 @@ from sqlalchemy import insert
|
|
|
22
23
|
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession
|
|
23
24
|
|
|
24
25
|
from domainkit.apps import registry
|
|
25
|
-
from domainkit.context import current_request
|
|
26
|
+
from domainkit.context import SYSTEM_ACTOR, current_request
|
|
26
27
|
from domainkit.db import connections
|
|
27
28
|
from domainkit.ddd import DomainEvent
|
|
28
29
|
from domainkit.ddd.personal import personal_fields
|
|
@@ -47,7 +48,7 @@ def _actor() -> str:
|
|
|
47
48
|
"""Return who is acting in the current context."""
|
|
48
49
|
request = current_request()
|
|
49
50
|
if request is None:
|
|
50
|
-
return
|
|
51
|
+
return SYSTEM_ACTOR
|
|
51
52
|
return request.actor or "anonymous"
|
|
52
53
|
|
|
53
54
|
|
|
@@ -110,8 +111,11 @@ def audit_row(
|
|
|
110
111
|
"""
|
|
111
112
|
request = current_request()
|
|
112
113
|
occurred_at = occurred_at or datetime.now(UTC)
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
actor = request.actor if request else None
|
|
115
|
+
caller = (
|
|
116
|
+
actor
|
|
117
|
+
if actor not in {None, SYSTEM_ACTOR}
|
|
118
|
+
else anonymous_subject(occurred_at.astimezone(UTC).date())
|
|
115
119
|
)
|
|
116
120
|
row = {
|
|
117
121
|
"id": uuid.uuid4(),
|
|
@@ -10,7 +10,8 @@ verifies what it returns: a chain rewritten after a head was anchored no
|
|
|
10
10
|
longer matches it.
|
|
11
11
|
|
|
12
12
|
``verify`` reports changed and missing rows, links that do not follow,
|
|
13
|
-
anchors the chain no longer matches, and rows left
|
|
13
|
+
anchors the chain no longer matches, seals left unanchored, and rows left
|
|
14
|
+
unsealed.
|
|
14
15
|
|
|
15
16
|
Rows are sealed after they commit, by the worker every ``AUDIT.SEAL_EVERY``
|
|
16
17
|
or by ``auditseal``, so a row can be rewritten unnoticed until it is sealed
|
|
@@ -114,6 +115,9 @@ class Report:
|
|
|
114
115
|
"""Seals whose number or link does not follow the one before."""
|
|
115
116
|
rewritten: list[int] = field(default_factory=list)
|
|
116
117
|
"""Anchored heads the chain no longer has."""
|
|
118
|
+
unanchored: int = 0
|
|
119
|
+
"""Seals after the newest anchor, sealed more than ``ANCHOR_EVERY`` and
|
|
120
|
+
``LOOKBACK`` ago: anchoring stopped (checked with a ledger only)."""
|
|
117
121
|
unsealed: int = 0
|
|
118
122
|
"""Rows older than ``LOOKBACK`` without a seal."""
|
|
119
123
|
|
|
@@ -121,7 +125,7 @@ class Report:
|
|
|
121
125
|
def intact(self) -> bool:
|
|
122
126
|
"""Whether nothing was found wrong."""
|
|
123
127
|
problems = (self.changed, self.missing, self.broken, self.rewritten)
|
|
124
|
-
return not (any(problems) or self.unsealed)
|
|
128
|
+
return not (any(problems) or self.unanchored or self.unsealed)
|
|
125
129
|
|
|
126
130
|
|
|
127
131
|
def digest(row: Mapping[Any, Any]) -> str:
|
|
@@ -163,7 +167,8 @@ async def anchor(
|
|
|
163
167
|
) -> Anchor | None:
|
|
164
168
|
"""Write ``database``'s head to ``ledger``; None when there is nothing new.
|
|
165
169
|
|
|
166
|
-
A head is anchored at most once per ``every``, and never twice.
|
|
170
|
+
A head is anchored at most once per ``every``, and never twice. An anchor
|
|
171
|
+
dated after ``now`` (written by a clock running ahead) holds none back.
|
|
167
172
|
"""
|
|
168
173
|
now = now or datetime.now(UTC)
|
|
169
174
|
async with connections.engine(database).connect() as connection:
|
|
@@ -175,7 +180,7 @@ async def anchor(
|
|
|
175
180
|
except LedgerKeyNotFound:
|
|
176
181
|
latest = None
|
|
177
182
|
if latest is not None and (
|
|
178
|
-
latest.seq == head.seq or now - latest.anchored_at < every
|
|
183
|
+
latest.seq == head.seq or timedelta(0) <= now - latest.anchored_at < every
|
|
179
184
|
):
|
|
180
185
|
return None
|
|
181
186
|
written = Anchor(head.seq, head.link, now)
|
|
@@ -199,12 +204,27 @@ async def seal_all() -> dict[str, tuple[int, Anchor | None]]:
|
|
|
199
204
|
|
|
200
205
|
|
|
201
206
|
async def verify(
|
|
202
|
-
database: str,
|
|
207
|
+
database: str,
|
|
208
|
+
ledger: Ledger | None = None,
|
|
209
|
+
*,
|
|
210
|
+
now: datetime | None = None,
|
|
211
|
+
every: timedelta | None = None,
|
|
203
212
|
) -> Report:
|
|
204
|
-
"""Check ``database``'s chain against its rows, and against ``ledger``.
|
|
213
|
+
"""Check ``database``'s chain against its rows, and against ``ledger``.
|
|
214
|
+
|
|
215
|
+
With a ledger, seals sealed more than ``every`` (default:
|
|
216
|
+
``AUDIT.ANCHOR_EVERY``) and ``LOOKBACK`` ago must be anchored.
|
|
217
|
+
"""
|
|
218
|
+
now = now or datetime.now(UTC)
|
|
205
219
|
report = Report(database)
|
|
206
220
|
anchored = await _anchors(database, ledger) if ledger is not None else {}
|
|
207
221
|
report.anchors = len(anchored)
|
|
222
|
+
newest_anchored = max(anchored, default=0)
|
|
223
|
+
overdue = None # Sealed before this, a seal must be anchored.
|
|
224
|
+
if ledger is not None:
|
|
225
|
+
if every is None:
|
|
226
|
+
every = registry.settings(AuditSettings).ANCHOR_EVERY
|
|
227
|
+
overdue = now - every - LOOKBACK
|
|
208
228
|
previous = Head(0, GENESIS)
|
|
209
229
|
query = (
|
|
210
230
|
select(audit_seals, *(audit_log.c[name] for name in COLUMNS))
|
|
@@ -225,16 +245,23 @@ async def verify(
|
|
|
225
245
|
report.changed.append(seal_row["entry_id"])
|
|
226
246
|
if anchored.pop(seal_row["seq"], seal_row["link"]) != seal_row["link"]:
|
|
227
247
|
report.rewritten.append(seal_row["seq"])
|
|
248
|
+
if (
|
|
249
|
+
overdue is not None
|
|
250
|
+
and seal_row["seq"] > newest_anchored
|
|
251
|
+
and seal_row["sealed_at"] < overdue
|
|
252
|
+
):
|
|
253
|
+
report.unanchored += 1
|
|
228
254
|
previous = Head(seal_row["seq"], seal_row["link"])
|
|
229
255
|
report.rewritten.extend(sorted(anchored)) # Anchored seals now gone.
|
|
230
|
-
report.unsealed = await _unsealed(connection, now
|
|
256
|
+
report.unsealed = await _unsealed(connection, now)
|
|
231
257
|
return report
|
|
232
258
|
|
|
233
259
|
|
|
234
260
|
async def _seal_batch(connection: AsyncConnection, batch: int) -> int:
|
|
235
261
|
"""Seal up to ``batch`` rows after the head, oldest first."""
|
|
236
262
|
head = await _head(connection)
|
|
237
|
-
|
|
263
|
+
sealed_at = datetime.now(UTC)
|
|
264
|
+
horizon = await _horizon(connection, head, sealed_at)
|
|
238
265
|
unsealed = (
|
|
239
266
|
select(*(audit_log.c[name] for name in COLUMNS))
|
|
240
267
|
.outerjoin(audit_seals, audit_seals.c.entry_id == audit_log.c.id)
|
|
@@ -246,7 +273,6 @@ async def _seal_batch(connection: AsyncConnection, batch: int) -> int:
|
|
|
246
273
|
unsealed = unsealed.where(audit_log.c.occurred_at >= horizon)
|
|
247
274
|
rows = (await connection.execute(unsealed)).mappings().all()
|
|
248
275
|
previous = head or Head(0, GENESIS)
|
|
249
|
-
sealed_at = datetime.now(UTC)
|
|
250
276
|
seals = []
|
|
251
277
|
for row in rows:
|
|
252
278
|
row_digest = digest(row)
|
|
@@ -277,8 +303,14 @@ async def _head(connection: AsyncConnection) -> Head | None:
|
|
|
277
303
|
return None if found is None else Head(found.seq, found.link)
|
|
278
304
|
|
|
279
305
|
|
|
280
|
-
async def _horizon(
|
|
281
|
-
|
|
306
|
+
async def _horizon(
|
|
307
|
+
connection: AsyncConnection, head: Head | None, now: datetime
|
|
308
|
+
) -> datetime | None:
|
|
309
|
+
"""Where to look for unsealed rows: ``LOOKBACK`` before the head's row.
|
|
310
|
+
|
|
311
|
+
A head's row dated after ``now`` (a clock running ahead) counts as
|
|
312
|
+
``now``, so it cannot leave the rows written before it unsealable.
|
|
313
|
+
"""
|
|
282
314
|
if head is None:
|
|
283
315
|
return None
|
|
284
316
|
newest = await connection.scalar(
|
|
@@ -286,7 +318,7 @@ async def _horizon(connection: AsyncConnection, head: Head | None) -> datetime |
|
|
|
286
318
|
.join(audit_seals, audit_seals.c.entry_id == audit_log.c.id)
|
|
287
319
|
.where(audit_seals.c.seq == head.seq)
|
|
288
320
|
)
|
|
289
|
-
return None if newest is None else newest - LOOKBACK
|
|
321
|
+
return None if newest is None else min(newest, now) - LOOKBACK
|
|
290
322
|
|
|
291
323
|
|
|
292
324
|
async def _unsealed(connection: AsyncConnection, now: datetime) -> int:
|
|
@@ -327,7 +359,12 @@ def _plain(value: object) -> object:
|
|
|
327
359
|
|
|
328
360
|
|
|
329
361
|
def _canonical(document: object) -> bytes:
|
|
330
|
-
"""``document`` as compact JSON with sorted keys.
|
|
362
|
+
"""``document`` as compact JSON with sorted keys.
|
|
363
|
+
|
|
364
|
+
Strings can hold lone surrogates (JSON can escape half a surrogate
|
|
365
|
+
pair); they are encoded as they are, so every row has a digest, and the
|
|
366
|
+
bytes of every other string stay plain UTF-8.
|
|
367
|
+
"""
|
|
331
368
|
return json.dumps(
|
|
332
369
|
document, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
333
|
-
).encode()
|
|
370
|
+
).encode("utf-8", "surrogatepass")
|
|
@@ -10,7 +10,7 @@ from domainkit.conf import DEFAULT_DB_ALIAS
|
|
|
10
10
|
class AuditSettings(BaseModel):
|
|
11
11
|
"""What to record; every key is optional."""
|
|
12
12
|
|
|
13
|
-
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
13
|
+
model_config = ConfigDict(frozen=True, extra="forbid", allow_inf_nan=False)
|
|
14
14
|
|
|
15
15
|
DATABASE: str = DEFAULT_DB_ALIAS
|
|
16
16
|
"""Alias holding the audit trail; event audits need units of work on it."""
|
|
@@ -46,6 +46,7 @@ from .access import (
|
|
|
46
46
|
Require,
|
|
47
47
|
check_object_permissions,
|
|
48
48
|
current_user,
|
|
49
|
+
mark_public,
|
|
49
50
|
open_routes,
|
|
50
51
|
)
|
|
51
52
|
from .authentication import (
|
|
@@ -165,6 +166,7 @@ __all__ = [
|
|
|
165
166
|
"has_perm",
|
|
166
167
|
"has_perms",
|
|
167
168
|
"make_password",
|
|
169
|
+
"mark_public",
|
|
168
170
|
"open_routes",
|
|
169
171
|
"password_help_texts",
|
|
170
172
|
"validate_password",
|