domainkit 0.1.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.1.0/LICENSE +21 -0
- domainkit-0.1.0/PKG-INFO +188 -0
- domainkit-0.1.0/README.md +149 -0
- domainkit-0.1.0/pyproject.toml +125 -0
- domainkit-0.1.0/src/domainkit/__init__.py +40 -0
- domainkit-0.1.0/src/domainkit/apps/__init__.py +6 -0
- domainkit-0.1.0/src/domainkit/apps/config.py +118 -0
- domainkit-0.1.0/src/domainkit/apps/registry.py +135 -0
- domainkit-0.1.0/src/domainkit/architecture.py +193 -0
- domainkit-0.1.0/src/domainkit/conf/__init__.py +58 -0
- domainkit-0.1.0/src/domainkit/conf/env.py +260 -0
- domainkit-0.1.0/src/domainkit/conf/global_settings.py +205 -0
- domainkit-0.1.0/src/domainkit/conf/lazy.py +165 -0
- domainkit-0.1.0/src/domainkit/context.py +43 -0
- domainkit-0.1.0/src/domainkit/contrib/__init__.py +7 -0
- domainkit-0.1.0/src/domainkit/contrib/audit/__init__.py +16 -0
- domainkit-0.1.0/src/domainkit/contrib/audit/apps.py +32 -0
- domainkit-0.1.0/src/domainkit/contrib/audit/management/__init__.py +1 -0
- domainkit-0.1.0/src/domainkit/contrib/audit/management/commands/__init__.py +1 -0
- domainkit-0.1.0/src/domainkit/contrib/audit/management/commands/auditlog.py +41 -0
- domainkit-0.1.0/src/domainkit/contrib/audit/protection.py +79 -0
- domainkit-0.1.0/src/domainkit/contrib/audit/recorder.py +133 -0
- domainkit-0.1.0/src/domainkit/contrib/audit/settings.py +18 -0
- domainkit-0.1.0/src/domainkit/contrib/audit/tables.py +24 -0
- domainkit-0.1.0/src/domainkit/contrib/idempotency/__init__.py +11 -0
- domainkit-0.1.0/src/domainkit/contrib/idempotency/apps.py +22 -0
- domainkit-0.1.0/src/domainkit/contrib/idempotency/middleware.py +238 -0
- domainkit-0.1.0/src/domainkit/contrib/idempotency/settings.py +18 -0
- domainkit-0.1.0/src/domainkit/contrib/idempotency/tables.py +19 -0
- domainkit-0.1.0/src/domainkit/contrib/idempotency/tasks.py +24 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/__init__.py +26 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/admin.py +54 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/apps.py +52 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/management/__init__.py +1 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/management/commands/__init__.py +1 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/management/commands/messages.py +46 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/management/commands/runworker.py +59 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/metrics.py +38 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/outbox.py +155 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/registry.py +174 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/settings.py +39 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/tables.py +34 -0
- domainkit-0.1.0/src/domainkit/contrib/messaging/worker.py +320 -0
- domainkit-0.1.0/src/domainkit/db/__init__.py +19 -0
- domainkit-0.1.0/src/domainkit/db/connections.py +117 -0
- domainkit-0.1.0/src/domainkit/db/drivers.py +100 -0
- domainkit-0.1.0/src/domainkit/db/migration_env/__init__.py +1 -0
- domainkit-0.1.0/src/domainkit/db/migration_env/env.py +5 -0
- domainkit-0.1.0/src/domainkit/db/migration_env/script.py.mako +26 -0
- domainkit-0.1.0/src/domainkit/db/migrations.py +276 -0
- domainkit-0.1.0/src/domainkit/db/orm.py +44 -0
- domainkit-0.1.0/src/domainkit/db/repository.py +33 -0
- domainkit-0.1.0/src/domainkit/db/types.py +39 -0
- domainkit-0.1.0/src/domainkit/db/uow.py +91 -0
- domainkit-0.1.0/src/domainkit/ddd/__init__.py +44 -0
- domainkit-0.1.0/src/domainkit/ddd/_dataclass.py +47 -0
- domainkit-0.1.0/src/domainkit/ddd/events.py +127 -0
- domainkit-0.1.0/src/domainkit/ddd/exceptions.py +48 -0
- domainkit-0.1.0/src/domainkit/ddd/messages.py +16 -0
- domainkit-0.1.0/src/domainkit/ddd/model.py +79 -0
- domainkit-0.1.0/src/domainkit/ddd/pagination.py +24 -0
- domainkit-0.1.0/src/domainkit/ddd/repository.py +94 -0
- domainkit-0.1.0/src/domainkit/ddd/uow.py +114 -0
- domainkit-0.1.0/src/domainkit/deprecation.py +21 -0
- domainkit-0.1.0/src/domainkit/exceptions.py +8 -0
- domainkit-0.1.0/src/domainkit/http/__init__.py +24 -0
- domainkit-0.1.0/src/domainkit/http/application.py +73 -0
- domainkit-0.1.0/src/domainkit/http/errors.py +179 -0
- domainkit-0.1.0/src/domainkit/http/health.py +52 -0
- domainkit-0.1.0/src/domainkit/http/metrics.py +150 -0
- domainkit-0.1.0/src/domainkit/http/middleware.py +378 -0
- domainkit-0.1.0/src/domainkit/http/pagination.py +71 -0
- domainkit-0.1.0/src/domainkit/http/ratelimit.py +150 -0
- domainkit-0.1.0/src/domainkit/log.py +119 -0
- domainkit-0.1.0/src/domainkit/management/__init__.py +129 -0
- domainkit-0.1.0/src/domainkit/management/base.py +87 -0
- domainkit-0.1.0/src/domainkit/management/commands/__init__.py +1 -0
- domainkit-0.1.0/src/domainkit/management/commands/check.py +37 -0
- domainkit-0.1.0/src/domainkit/management/commands/makemigrations.py +61 -0
- domainkit-0.1.0/src/domainkit/management/commands/migrate.py +64 -0
- domainkit-0.1.0/src/domainkit/management/commands/runserver.py +60 -0
- domainkit-0.1.0/src/domainkit/management/commands/shell.py +27 -0
- domainkit-0.1.0/src/domainkit/management/commands/showmigrations.py +26 -0
- domainkit-0.1.0/src/domainkit/management/commands/startdomain.py +165 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/__init__.py-tpl +11 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/api/__init__.py-tpl +1 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/api/dependencies.py-tpl +47 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/api/routes.py-tpl +74 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/api/schemas.py-tpl +65 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/application/__init__.py-tpl +1 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/application/commands.py-tpl +24 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/application/handlers.py-tpl +18 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/application/services.py-tpl +63 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/application/unit_of_work.py-tpl +12 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/apps.py-tpl +22 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/domain/__init__.py-tpl +1 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/domain/events.py-tpl +28 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/domain/exceptions.py-tpl +15 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/domain/model.py-tpl +71 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/domain/repositories.py-tpl +33 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/infrastructure/__init__.py-tpl +1 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/infrastructure/orm.py-tpl +44 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/infrastructure/repositories.py-tpl +26 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/infrastructure/unit_of_work.py-tpl +15 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/management/__init__.py-tpl +1 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/management/commands/__init__.py-tpl +1 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/management/commands/{{ aggregates }}.py-tpl +25 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/package/settings.py-tpl +30 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/tests/__init__.py-tpl +1 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/tests/fakes.py-tpl +47 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/tests/test_api.py-tpl +104 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/tests/test_model.py-tpl +67 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/tests/test_services.py-tpl +71 -0
- domainkit-0.1.0/src/domainkit/management/templates/domain/tests/test_settings.py-tpl +20 -0
- domainkit-0.1.0/src/domainkit/py.typed +0 -0
- domainkit-0.1.0/src/domainkit/reporting.py +50 -0
- domainkit-0.1.0/src/domainkit/testing.py +83 -0
- domainkit-0.1.0/src/domainkit/tracing.py +42 -0
- domainkit-0.1.0/src/domainkit/utils/__init__.py +1 -0
- domainkit-0.1.0/src/domainkit/utils/formatting.py +26 -0
- domainkit-0.1.0/src/domainkit/utils/module_loading.py +22 -0
- domainkit-0.1.0/src/domainkit/utils/rates.py +17 -0
- domainkit-0.1.0/src/domainkit/utils/readiness.py +31 -0
- domainkit-0.1.0/src/domainkit/utils/text.py +25 -0
domainkit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 karim magdy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
domainkit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: domainkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Django-style framework for domain-driven FastAPI services.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: ddd,domain-driven-design,fastapi,sqlalchemy,outbox,framework
|
|
8
|
+
Author: karim magdy
|
|
9
|
+
Author-email: karimhady111@gmail.com
|
|
10
|
+
Requires-Python: >=3.13,<4
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Provides-Extra: metrics
|
|
20
|
+
Provides-Extra: postgres
|
|
21
|
+
Provides-Extra: sqlite
|
|
22
|
+
Provides-Extra: tracing
|
|
23
|
+
Requires-Dist: aiosqlite (>=0.22.1,<0.23.0) ; extra == "sqlite"
|
|
24
|
+
Requires-Dist: alembic (>=1.20.0,<2.0.0)
|
|
25
|
+
Requires-Dist: asyncpg (>=0.31.0,<0.32.0) ; extra == "postgres"
|
|
26
|
+
Requires-Dist: fastapi (>=0.141.1,<0.142.0)
|
|
27
|
+
Requires-Dist: opentelemetry-api (>=1.45.1,<2.0.0) ; extra == "tracing"
|
|
28
|
+
Requires-Dist: prometheus-client (>=0.26.0,<0.27.0) ; extra == "metrics"
|
|
29
|
+
Requires-Dist: pydantic (>=2.14.1,<3.0.0)
|
|
30
|
+
Requires-Dist: python-dotenv (>=1.2.4,<2.0.0)
|
|
31
|
+
Requires-Dist: sqlalchemy[asyncio] (>=2.0.54,<2.1)
|
|
32
|
+
Requires-Dist: uvicorn[standard] (>=0.54.0,<0.55.0)
|
|
33
|
+
Project-URL: Changelog, https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md
|
|
34
|
+
Project-URL: Documentation, https://github.com/karimmattar/domainkit/blob/main/docs/reference.md
|
|
35
|
+
Project-URL: Homepage, https://github.com/karimmattar/domainkit
|
|
36
|
+
Project-URL: Issues, https://github.com/karimmattar/domainkit/issues
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# domainkit
|
|
40
|
+
|
|
41
|
+
A Django-style framework for domain-driven FastAPI services. It provides:
|
|
42
|
+
|
|
43
|
+
- settings modules and installed domains
|
|
44
|
+
- `manage.py` commands and migrations
|
|
45
|
+
- DDD building blocks, with the layer rules between them enforced
|
|
46
|
+
- optional apps for reliable messaging, audit trails, and idempotent requests
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install "domainkit[postgres]" # extras: postgres, sqlite, metrics, tracing
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Documentation
|
|
53
|
+
|
|
54
|
+
- [docs/reference.md](https://github.com/karimmattar/domainkit/blob/main/docs/reference.md): settings, middleware, errors,
|
|
55
|
+
observability, testing, commands, and extension points
|
|
56
|
+
- [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging, audit, and idempotency
|
|
57
|
+
- [docs/versioning.md](https://github.com/karimmattar/domainkit/blob/main/docs/versioning.md): compatibility, deprecations, and releases
|
|
58
|
+
- [docs/adr/](https://github.com/karimmattar/domainkit/tree/main/docs/adr): the decisions behind the design
|
|
59
|
+
- [CHANGELOG.md](https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md) and [SECURITY.md](https://github.com/karimmattar/domainkit/blob/main/SECURITY.md)
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
A project is a `pyproject.toml`, a `manage.py`, a `config` package, and a
|
|
64
|
+
`domains` package for its bounded contexts. The project's name sets the
|
|
65
|
+
environment variable prefix: `billingAPI` reads `BILLING_API_*`.
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
billing/
|
|
69
|
+
pyproject.toml
|
|
70
|
+
manage.py
|
|
71
|
+
src/config/__init__.py
|
|
72
|
+
src/config/settings/__init__.py
|
|
73
|
+
src/config/settings/local.py
|
|
74
|
+
src/config/settings/test.py
|
|
75
|
+
src/config/urls.py
|
|
76
|
+
src/config/asgi.py
|
|
77
|
+
src/domains/__init__.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```toml
|
|
81
|
+
# pyproject.toml
|
|
82
|
+
[project]
|
|
83
|
+
name = "billingAPI"
|
|
84
|
+
version = "0.1.0"
|
|
85
|
+
requires-python = ">=3.13"
|
|
86
|
+
dependencies = ["domainkit[sqlite]"]
|
|
87
|
+
|
|
88
|
+
[tool.pytest.ini_options]
|
|
89
|
+
pythonpath = ["src"]
|
|
90
|
+
addopts = "-p domainkit.testing" # the database and client fixtures
|
|
91
|
+
domainkit_settings = "config.settings.test"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# manage.py
|
|
96
|
+
import sys
|
|
97
|
+
from pathlib import Path
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))
|
|
101
|
+
from domainkit.management import execute_from_command_line
|
|
102
|
+
|
|
103
|
+
execute_from_command_line(sys.argv)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# src/config/settings/local.py
|
|
108
|
+
from pathlib import Path
|
|
109
|
+
|
|
110
|
+
from domainkit.conf.env import Env
|
|
111
|
+
|
|
112
|
+
env = Env()
|
|
113
|
+
BASE_DIR = Path(__file__).resolve().parents[3]
|
|
114
|
+
|
|
115
|
+
DEBUG = True
|
|
116
|
+
INSTALLED_DOMAINS = ["domainkit.contrib.messaging"]
|
|
117
|
+
ROOT_ROUTER = "config.urls.router"
|
|
118
|
+
ASGI_APPLICATION = "config.asgi:application"
|
|
119
|
+
DATABASES = {
|
|
120
|
+
"default": {
|
|
121
|
+
"URL": env.str("DATABASE_URL", f"sqlite+aiosqlite:///{BASE_DIR / 'db.sqlite3'}")
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# src/config/settings/test.py
|
|
128
|
+
from config.settings.local import *
|
|
129
|
+
|
|
130
|
+
DEBUG = False
|
|
131
|
+
ALLOWED_HOSTS = ["testserver"]
|
|
132
|
+
DATABASES = {"default": {"URL": "sqlite+aiosqlite:///:memory:"}}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# src/config/urls.py
|
|
137
|
+
from fastapi import APIRouter
|
|
138
|
+
|
|
139
|
+
from domainkit.http.health import router as health_router
|
|
140
|
+
|
|
141
|
+
router = APIRouter()
|
|
142
|
+
router.include_router(health_router)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# src/config/asgi.py
|
|
147
|
+
from domainkit.http import get_asgi_application
|
|
148
|
+
|
|
149
|
+
application = get_asgi_application()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Then:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
export BILLING_API_SETTINGS_MODULE=config.settings.local # or put it in .env
|
|
156
|
+
python manage.py check
|
|
157
|
+
python manage.py startdomain invoices --aggregate Invoice # prints how to install it
|
|
158
|
+
python manage.py makemigrations -m "create invoices" && python manage.py migrate
|
|
159
|
+
pytest # the generated tests
|
|
160
|
+
python manage.py runserver # http://127.0.0.1:8000/docs
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Developing domainkit
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
poetry install --all-extras # Python 3.13
|
|
167
|
+
pre-commit install # after: pipx install pre-commit
|
|
168
|
+
|
|
169
|
+
poetry run ruff check . && poetry run ruff format --check . && poetry run mypy
|
|
170
|
+
poetry run pytest --cov # random order; coverage gate in pyproject.toml
|
|
171
|
+
poetry run mutmut run "domainkit.http*" && poetry run mutmut results
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
CI (`.github/workflows/ci.yml`) runs the same checks on every push and pull
|
|
175
|
+
request:
|
|
176
|
+
|
|
177
|
+
- lint and strict typing
|
|
178
|
+
- the tests on Python 3.13 and 3.14
|
|
179
|
+
- a wheel build, installed and imported in a clean environment
|
|
180
|
+
- pip-audit, and a gitleaks scan of the whole history
|
|
181
|
+
|
|
182
|
+
A full mutation testing run happens weekly. Releases are published to PyPI
|
|
183
|
+
from a version tag; see [docs/versioning.md](https://github.com/karimmattar/domainkit/blob/main/docs/versioning.md).
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT; see [LICENSE](https://github.com/karimmattar/domainkit/blob/main/LICENSE).
|
|
188
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# domainkit
|
|
2
|
+
|
|
3
|
+
A Django-style framework for domain-driven FastAPI services. It provides:
|
|
4
|
+
|
|
5
|
+
- settings modules and installed domains
|
|
6
|
+
- `manage.py` commands and migrations
|
|
7
|
+
- DDD building blocks, with the layer rules between them enforced
|
|
8
|
+
- optional apps for reliable messaging, audit trails, and idempotent requests
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install "domainkit[postgres]" # extras: postgres, sqlite, metrics, tracing
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Documentation
|
|
15
|
+
|
|
16
|
+
- [docs/reference.md](https://github.com/karimmattar/domainkit/blob/main/docs/reference.md): settings, middleware, errors,
|
|
17
|
+
observability, testing, commands, and extension points
|
|
18
|
+
- [docs/contrib.md](https://github.com/karimmattar/domainkit/blob/main/docs/contrib.md): messaging, audit, and idempotency
|
|
19
|
+
- [docs/versioning.md](https://github.com/karimmattar/domainkit/blob/main/docs/versioning.md): compatibility, deprecations, and releases
|
|
20
|
+
- [docs/adr/](https://github.com/karimmattar/domainkit/tree/main/docs/adr): the decisions behind the design
|
|
21
|
+
- [CHANGELOG.md](https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md) and [SECURITY.md](https://github.com/karimmattar/domainkit/blob/main/SECURITY.md)
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
A project is a `pyproject.toml`, a `manage.py`, a `config` package, and a
|
|
26
|
+
`domains` package for its bounded contexts. The project's name sets the
|
|
27
|
+
environment variable prefix: `billingAPI` reads `BILLING_API_*`.
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
billing/
|
|
31
|
+
pyproject.toml
|
|
32
|
+
manage.py
|
|
33
|
+
src/config/__init__.py
|
|
34
|
+
src/config/settings/__init__.py
|
|
35
|
+
src/config/settings/local.py
|
|
36
|
+
src/config/settings/test.py
|
|
37
|
+
src/config/urls.py
|
|
38
|
+
src/config/asgi.py
|
|
39
|
+
src/domains/__init__.py
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
# pyproject.toml
|
|
44
|
+
[project]
|
|
45
|
+
name = "billingAPI"
|
|
46
|
+
version = "0.1.0"
|
|
47
|
+
requires-python = ">=3.13"
|
|
48
|
+
dependencies = ["domainkit[sqlite]"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
pythonpath = ["src"]
|
|
52
|
+
addopts = "-p domainkit.testing" # the database and client fixtures
|
|
53
|
+
domainkit_settings = "config.settings.test"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# manage.py
|
|
58
|
+
import sys
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))
|
|
63
|
+
from domainkit.management import execute_from_command_line
|
|
64
|
+
|
|
65
|
+
execute_from_command_line(sys.argv)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# src/config/settings/local.py
|
|
70
|
+
from pathlib import Path
|
|
71
|
+
|
|
72
|
+
from domainkit.conf.env import Env
|
|
73
|
+
|
|
74
|
+
env = Env()
|
|
75
|
+
BASE_DIR = Path(__file__).resolve().parents[3]
|
|
76
|
+
|
|
77
|
+
DEBUG = True
|
|
78
|
+
INSTALLED_DOMAINS = ["domainkit.contrib.messaging"]
|
|
79
|
+
ROOT_ROUTER = "config.urls.router"
|
|
80
|
+
ASGI_APPLICATION = "config.asgi:application"
|
|
81
|
+
DATABASES = {
|
|
82
|
+
"default": {
|
|
83
|
+
"URL": env.str("DATABASE_URL", f"sqlite+aiosqlite:///{BASE_DIR / 'db.sqlite3'}")
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# src/config/settings/test.py
|
|
90
|
+
from config.settings.local import *
|
|
91
|
+
|
|
92
|
+
DEBUG = False
|
|
93
|
+
ALLOWED_HOSTS = ["testserver"]
|
|
94
|
+
DATABASES = {"default": {"URL": "sqlite+aiosqlite:///:memory:"}}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# src/config/urls.py
|
|
99
|
+
from fastapi import APIRouter
|
|
100
|
+
|
|
101
|
+
from domainkit.http.health import router as health_router
|
|
102
|
+
|
|
103
|
+
router = APIRouter()
|
|
104
|
+
router.include_router(health_router)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# src/config/asgi.py
|
|
109
|
+
from domainkit.http import get_asgi_application
|
|
110
|
+
|
|
111
|
+
application = get_asgi_application()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Then:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
export BILLING_API_SETTINGS_MODULE=config.settings.local # or put it in .env
|
|
118
|
+
python manage.py check
|
|
119
|
+
python manage.py startdomain invoices --aggregate Invoice # prints how to install it
|
|
120
|
+
python manage.py makemigrations -m "create invoices" && python manage.py migrate
|
|
121
|
+
pytest # the generated tests
|
|
122
|
+
python manage.py runserver # http://127.0.0.1:8000/docs
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Developing domainkit
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
poetry install --all-extras # Python 3.13
|
|
129
|
+
pre-commit install # after: pipx install pre-commit
|
|
130
|
+
|
|
131
|
+
poetry run ruff check . && poetry run ruff format --check . && poetry run mypy
|
|
132
|
+
poetry run pytest --cov # random order; coverage gate in pyproject.toml
|
|
133
|
+
poetry run mutmut run "domainkit.http*" && poetry run mutmut results
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
CI (`.github/workflows/ci.yml`) runs the same checks on every push and pull
|
|
137
|
+
request:
|
|
138
|
+
|
|
139
|
+
- lint and strict typing
|
|
140
|
+
- the tests on Python 3.13 and 3.14
|
|
141
|
+
- a wheel build, installed and imported in a clean environment
|
|
142
|
+
- pip-audit, and a gitleaks scan of the whole history
|
|
143
|
+
|
|
144
|
+
A full mutation testing run happens weekly. Releases are published to PyPI
|
|
145
|
+
from a version tag; see [docs/versioning.md](https://github.com/karimmattar/domainkit/blob/main/docs/versioning.md).
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT; see [LICENSE](https://github.com/karimmattar/domainkit/blob/main/LICENSE).
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "domainkit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A Django-style framework for domain-driven FastAPI services."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.13,<4"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "karim magdy", email = "karimhady111@gmail.com"},
|
|
11
|
+
]
|
|
12
|
+
keywords = ["ddd", "domain-driven-design", "fastapi", "sqlalchemy", "outbox", "framework"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Framework :: FastAPI",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
# Everything domainkit imports. Database drivers are extras: projects pick.
|
|
24
|
+
dependencies = [
|
|
25
|
+
"fastapi (>=0.141.1,<0.142.0)",
|
|
26
|
+
"uvicorn[standard] (>=0.54.0,<0.55.0)",
|
|
27
|
+
"pydantic (>=2.14.1,<3.0.0)",
|
|
28
|
+
"sqlalchemy[asyncio] (>=2.0.54,<2.1)",
|
|
29
|
+
"alembic (>=1.20.0,<2.0.0)",
|
|
30
|
+
"python-dotenv (>=1.2.4,<2.0.0)",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
postgres = ["asyncpg (>=0.31.0,<0.32.0)"]
|
|
35
|
+
sqlite = ["aiosqlite (>=0.22.1,<0.23.0)"]
|
|
36
|
+
metrics = ["prometheus-client (>=0.26.0,<0.27.0)"]
|
|
37
|
+
tracing = ["opentelemetry-api (>=1.45.1,<2.0.0)"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/karimmattar/domainkit"
|
|
41
|
+
Documentation = "https://github.com/karimmattar/domainkit/blob/main/docs/reference.md"
|
|
42
|
+
Changelog = "https://github.com/karimmattar/domainkit/blob/main/CHANGELOG.md"
|
|
43
|
+
Issues = "https://github.com/karimmattar/domainkit/issues"
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
# 2.2.1 is the version verified to write PEP 639 license metadata.
|
|
47
|
+
requires = ["poetry-core>=2.2.1,<3.0.0"]
|
|
48
|
+
build-backend = "poetry.core.masonry.api"
|
|
49
|
+
|
|
50
|
+
[tool.poetry]
|
|
51
|
+
packages = [{ include = "domainkit", from = "src" }]
|
|
52
|
+
|
|
53
|
+
# poetry install --all-extras: the tests exercise every driver and integration.
|
|
54
|
+
[dependency-groups]
|
|
55
|
+
dev = [
|
|
56
|
+
"pytest (>=9.1.1,<10.0.0)",
|
|
57
|
+
"pytest-cov (>=7.1.0,<8.0.0)",
|
|
58
|
+
"ruff (>=0.16.8,<0.17.0)",
|
|
59
|
+
"mypy (>=2.3.1,<3.0.0)",
|
|
60
|
+
"pip-audit (>=2.9.0,<3.0.0)",
|
|
61
|
+
# Starlette's TestClient (it deprecates plain httpx).
|
|
62
|
+
"httpx2 (>=2.13.0,<3.0.0)",
|
|
63
|
+
# In-memory span exporter for the tracing tests.
|
|
64
|
+
"opentelemetry-sdk (>=1.45.1,<2.0.0)",
|
|
65
|
+
# Random test order, property-based tests, mutation testing.
|
|
66
|
+
"pytest-randomly (>=5.0.0,<6.0.0)",
|
|
67
|
+
"hypothesis (>=6.169.0,<7.0.0)",
|
|
68
|
+
"mutmut (>=3.8.0,<4.0.0)",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.ruff]
|
|
72
|
+
target-version = "py313"
|
|
73
|
+
line-length = 88
|
|
74
|
+
src = ["src", "."]
|
|
75
|
+
extend-exclude = ["src/domainkit/management/templates", "**/mutants"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
# S: security (bandit), ASYNC: blocking calls in async code, BLE: blind except,
|
|
79
|
+
# G/LOG: logging misuse, DTZ: naive datetimes, T20: stray prints.
|
|
80
|
+
select = [
|
|
81
|
+
"E", "F", "I", "UP", "B", "SIM", "D", "ANN", "RUF",
|
|
82
|
+
"S", "ASYNC", "BLE", "G", "LOG", "DTZ", "T20",
|
|
83
|
+
"PT", "C4", "RET", "PIE", "PERF", "PTH", "ERA", "ISC",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint.pydocstyle]
|
|
87
|
+
convention = "google"
|
|
88
|
+
|
|
89
|
+
[tool.ruff.lint.per-file-ignores]
|
|
90
|
+
# Tests assert, and use throwaway credentials and server-side identifiers.
|
|
91
|
+
"domainkit_tests/**" = ["S101", "S105", "S106"]
|
|
92
|
+
|
|
93
|
+
[tool.mypy]
|
|
94
|
+
python_version = "3.13"
|
|
95
|
+
strict = true
|
|
96
|
+
plugins = ["pydantic.mypy"]
|
|
97
|
+
mypy_path = "src:."
|
|
98
|
+
files = ["src", "domainkit_tests"]
|
|
99
|
+
exclude = ["/management/templates/", "/mutants/"]
|
|
100
|
+
|
|
101
|
+
[tool.pytest.ini_options]
|
|
102
|
+
testpaths = ["domainkit_tests"]
|
|
103
|
+
pythonpath = ["."]
|
|
104
|
+
# pytester: the testing plugin is checked by running a pytest session in a test.
|
|
105
|
+
addopts = "-ra --strict-markers --strict-config -p pytester"
|
|
106
|
+
filterwarnings = ["error"]
|
|
107
|
+
|
|
108
|
+
[tool.coverage.run]
|
|
109
|
+
branch = true
|
|
110
|
+
concurrency = ["thread", "greenlet"]
|
|
111
|
+
source = ["src"]
|
|
112
|
+
|
|
113
|
+
[tool.coverage.report]
|
|
114
|
+
show_missing = true
|
|
115
|
+
skip_empty = true
|
|
116
|
+
fail_under = 98
|
|
117
|
+
precision = 2
|
|
118
|
+
exclude_also = ["if TYPE_CHECKING:", "raise NotImplementedError"]
|
|
119
|
+
|
|
120
|
+
# Mutation testing: "mutmut run 'domainkit.utils*'" (one area at a time; a
|
|
121
|
+
# full run takes a while), then "mutmut results".
|
|
122
|
+
[tool.mutmut]
|
|
123
|
+
source_paths = ["src"]
|
|
124
|
+
also_copy = ["domainkit_tests/", "docs/"]
|
|
125
|
+
pytest_add_cli_args_test_selection = ["domainkit_tests/", "-p", "no:randomly"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""domainkit: a Django-style framework for domain-driven FastAPI services.
|
|
2
|
+
|
|
3
|
+
Layers, lowest first; a module may import only from its own layer or below:
|
|
4
|
+
|
|
5
|
+
1. ``exceptions``, ``utils``, ``ddd``: no settings, no I/O. ``ddd`` holds the
|
|
6
|
+
domain building blocks (entities, value objects, events, repositories).
|
|
7
|
+
2. ``conf``: settings loading and validation.
|
|
8
|
+
3. ``apps``, ``context``, ``log``, ``reporting``: the domain registry and
|
|
9
|
+
observability.
|
|
10
|
+
4. ``db``, ``http``: SQLAlchemy and FastAPI integration.
|
|
11
|
+
5. ``management``: the ``manage.py`` commands.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
__version__ = version("domainkit")
|
|
18
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree
|
|
19
|
+
__version__ = "0.0.0+unknown"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def setup() -> None:
|
|
23
|
+
"""Configure logging and load the installed domains (``django.setup()``).
|
|
24
|
+
|
|
25
|
+
Safe to call more than once; later calls do nothing.
|
|
26
|
+
"""
|
|
27
|
+
from domainkit.apps import registry
|
|
28
|
+
from domainkit.conf import settings
|
|
29
|
+
from domainkit.ddd import event_bus
|
|
30
|
+
from domainkit.log import configure_logging
|
|
31
|
+
from domainkit.reporting import report_error, reporters
|
|
32
|
+
|
|
33
|
+
if registry.ready:
|
|
34
|
+
return
|
|
35
|
+
configure_logging(settings.LOGGING)
|
|
36
|
+
reporters()
|
|
37
|
+
event_bus.error_hook = lambda exc, event: report_error(
|
|
38
|
+
exc, event=type(event).__qualname__, event_id=str(event.event_id)
|
|
39
|
+
)
|
|
40
|
+
registry.populate(settings.INSTALLED_DOMAINS)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""``DomainConfig``: Django's ``AppConfig`` for a bounded context."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from domainkit.exceptions import ImproperlyConfigured
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from domainkit.ddd import DeferredTask, DomainEvent, UnitOfWork
|
|
15
|
+
|
|
16
|
+
CONFIG_MODULE = "apps"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DomainConfig:
|
|
20
|
+
"""Describe one installed domain and its lifecycle hooks.
|
|
21
|
+
|
|
22
|
+
Each domain package may define exactly one subclass in its ``apps``
|
|
23
|
+
module; a package without one gets this base class. Override only the
|
|
24
|
+
hooks the domain needs.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
label: str = ""
|
|
28
|
+
"""Unique short name; defaults to the last part of the package path."""
|
|
29
|
+
verbose_name: str = ""
|
|
30
|
+
settings_class: type[BaseModel] | None = None
|
|
31
|
+
"""Schema for this domain's settings, read from ``settings_name``."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, name: str, module: ModuleType) -> None:
|
|
34
|
+
"""Bind the config to the domain package ``name``."""
|
|
35
|
+
self.name = name
|
|
36
|
+
self.module = module
|
|
37
|
+
self.label = self.label or name.rpartition(".")[2]
|
|
38
|
+
if not self.label.isidentifier():
|
|
39
|
+
raise ImproperlyConfigured(
|
|
40
|
+
f"Domain label {self.label!r} is not a valid Python identifier."
|
|
41
|
+
)
|
|
42
|
+
self.verbose_name = self.verbose_name or self.label.replace("_", " ").title()
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
"""Show the class and the domain label."""
|
|
46
|
+
return f"<{type(self).__name__}: {self.label}>"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def settings_name(self) -> str:
|
|
50
|
+
"""Settings-module name holding this domain's settings, e.g. ``BILLING``."""
|
|
51
|
+
return self.label.upper()
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def path(self) -> Path:
|
|
55
|
+
"""Directory of the domain package."""
|
|
56
|
+
return Path(next(iter(self.module.__path__)))
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def create(cls, name: str) -> "DomainConfig":
|
|
60
|
+
"""Import package ``name`` and instantiate its ``DomainConfig``."""
|
|
61
|
+
module = _import_domain(name)
|
|
62
|
+
config_module_name = f"{name}.{CONFIG_MODULE}"
|
|
63
|
+
try:
|
|
64
|
+
config_module = import_module(config_module_name)
|
|
65
|
+
except ModuleNotFoundError as exc:
|
|
66
|
+
if exc.name != config_module_name:
|
|
67
|
+
raise
|
|
68
|
+
return cls(name, module)
|
|
69
|
+
configs = [
|
|
70
|
+
value
|
|
71
|
+
for value in vars(config_module).values()
|
|
72
|
+
if isinstance(value, type)
|
|
73
|
+
and issubclass(value, DomainConfig)
|
|
74
|
+
and value.__module__ == config_module_name
|
|
75
|
+
]
|
|
76
|
+
if len(configs) != 1:
|
|
77
|
+
raise ImproperlyConfigured(
|
|
78
|
+
f"{config_module_name!r} must define exactly one DomainConfig "
|
|
79
|
+
f"subclass, found {len(configs)}."
|
|
80
|
+
)
|
|
81
|
+
return configs[0](name, module)
|
|
82
|
+
|
|
83
|
+
def ready(self) -> None:
|
|
84
|
+
"""Wire the domain once settings are loaded: ORM mappers, event handlers."""
|
|
85
|
+
|
|
86
|
+
async def startup(self) -> None:
|
|
87
|
+
"""Open process resources such as clients and pools."""
|
|
88
|
+
|
|
89
|
+
async def shutdown(self) -> None:
|
|
90
|
+
"""Release resources opened by ``startup``."""
|
|
91
|
+
|
|
92
|
+
async def check(self) -> None:
|
|
93
|
+
"""Raise when the domain cannot serve traffic (readiness probe)."""
|
|
94
|
+
|
|
95
|
+
async def stage(
|
|
96
|
+
self,
|
|
97
|
+
uow: "UnitOfWork",
|
|
98
|
+
events: Sequence["DomainEvent"],
|
|
99
|
+
tasks: Sequence["DeferredTask"],
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Add rows to a committing SQL unit of work (outbox, audit trail)."""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _import_domain(name: str) -> ModuleType:
|
|
105
|
+
"""Import a domain package, explaining a missing one."""
|
|
106
|
+
try:
|
|
107
|
+
module = import_module(name)
|
|
108
|
+
except ModuleNotFoundError as exc:
|
|
109
|
+
if exc.name is not None and (
|
|
110
|
+
name == exc.name or name.startswith(f"{exc.name}.")
|
|
111
|
+
):
|
|
112
|
+
raise ImproperlyConfigured(
|
|
113
|
+
f"Installed domain {name!r} cannot be imported."
|
|
114
|
+
) from exc
|
|
115
|
+
raise
|
|
116
|
+
if not hasattr(module, "__path__"):
|
|
117
|
+
raise ImproperlyConfigured(f"Installed domain {name!r} must be a package.")
|
|
118
|
+
return module
|