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