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.
Files changed (124) hide show
  1. domainkit-0.1.0/LICENSE +21 -0
  2. domainkit-0.1.0/PKG-INFO +188 -0
  3. domainkit-0.1.0/README.md +149 -0
  4. domainkit-0.1.0/pyproject.toml +125 -0
  5. domainkit-0.1.0/src/domainkit/__init__.py +40 -0
  6. domainkit-0.1.0/src/domainkit/apps/__init__.py +6 -0
  7. domainkit-0.1.0/src/domainkit/apps/config.py +118 -0
  8. domainkit-0.1.0/src/domainkit/apps/registry.py +135 -0
  9. domainkit-0.1.0/src/domainkit/architecture.py +193 -0
  10. domainkit-0.1.0/src/domainkit/conf/__init__.py +58 -0
  11. domainkit-0.1.0/src/domainkit/conf/env.py +260 -0
  12. domainkit-0.1.0/src/domainkit/conf/global_settings.py +205 -0
  13. domainkit-0.1.0/src/domainkit/conf/lazy.py +165 -0
  14. domainkit-0.1.0/src/domainkit/context.py +43 -0
  15. domainkit-0.1.0/src/domainkit/contrib/__init__.py +7 -0
  16. domainkit-0.1.0/src/domainkit/contrib/audit/__init__.py +16 -0
  17. domainkit-0.1.0/src/domainkit/contrib/audit/apps.py +32 -0
  18. domainkit-0.1.0/src/domainkit/contrib/audit/management/__init__.py +1 -0
  19. domainkit-0.1.0/src/domainkit/contrib/audit/management/commands/__init__.py +1 -0
  20. domainkit-0.1.0/src/domainkit/contrib/audit/management/commands/auditlog.py +41 -0
  21. domainkit-0.1.0/src/domainkit/contrib/audit/protection.py +79 -0
  22. domainkit-0.1.0/src/domainkit/contrib/audit/recorder.py +133 -0
  23. domainkit-0.1.0/src/domainkit/contrib/audit/settings.py +18 -0
  24. domainkit-0.1.0/src/domainkit/contrib/audit/tables.py +24 -0
  25. domainkit-0.1.0/src/domainkit/contrib/idempotency/__init__.py +11 -0
  26. domainkit-0.1.0/src/domainkit/contrib/idempotency/apps.py +22 -0
  27. domainkit-0.1.0/src/domainkit/contrib/idempotency/middleware.py +238 -0
  28. domainkit-0.1.0/src/domainkit/contrib/idempotency/settings.py +18 -0
  29. domainkit-0.1.0/src/domainkit/contrib/idempotency/tables.py +19 -0
  30. domainkit-0.1.0/src/domainkit/contrib/idempotency/tasks.py +24 -0
  31. domainkit-0.1.0/src/domainkit/contrib/messaging/__init__.py +26 -0
  32. domainkit-0.1.0/src/domainkit/contrib/messaging/admin.py +54 -0
  33. domainkit-0.1.0/src/domainkit/contrib/messaging/apps.py +52 -0
  34. domainkit-0.1.0/src/domainkit/contrib/messaging/management/__init__.py +1 -0
  35. domainkit-0.1.0/src/domainkit/contrib/messaging/management/commands/__init__.py +1 -0
  36. domainkit-0.1.0/src/domainkit/contrib/messaging/management/commands/messages.py +46 -0
  37. domainkit-0.1.0/src/domainkit/contrib/messaging/management/commands/runworker.py +59 -0
  38. domainkit-0.1.0/src/domainkit/contrib/messaging/metrics.py +38 -0
  39. domainkit-0.1.0/src/domainkit/contrib/messaging/outbox.py +155 -0
  40. domainkit-0.1.0/src/domainkit/contrib/messaging/registry.py +174 -0
  41. domainkit-0.1.0/src/domainkit/contrib/messaging/settings.py +39 -0
  42. domainkit-0.1.0/src/domainkit/contrib/messaging/tables.py +34 -0
  43. domainkit-0.1.0/src/domainkit/contrib/messaging/worker.py +320 -0
  44. domainkit-0.1.0/src/domainkit/db/__init__.py +19 -0
  45. domainkit-0.1.0/src/domainkit/db/connections.py +117 -0
  46. domainkit-0.1.0/src/domainkit/db/drivers.py +100 -0
  47. domainkit-0.1.0/src/domainkit/db/migration_env/__init__.py +1 -0
  48. domainkit-0.1.0/src/domainkit/db/migration_env/env.py +5 -0
  49. domainkit-0.1.0/src/domainkit/db/migration_env/script.py.mako +26 -0
  50. domainkit-0.1.0/src/domainkit/db/migrations.py +276 -0
  51. domainkit-0.1.0/src/domainkit/db/orm.py +44 -0
  52. domainkit-0.1.0/src/domainkit/db/repository.py +33 -0
  53. domainkit-0.1.0/src/domainkit/db/types.py +39 -0
  54. domainkit-0.1.0/src/domainkit/db/uow.py +91 -0
  55. domainkit-0.1.0/src/domainkit/ddd/__init__.py +44 -0
  56. domainkit-0.1.0/src/domainkit/ddd/_dataclass.py +47 -0
  57. domainkit-0.1.0/src/domainkit/ddd/events.py +127 -0
  58. domainkit-0.1.0/src/domainkit/ddd/exceptions.py +48 -0
  59. domainkit-0.1.0/src/domainkit/ddd/messages.py +16 -0
  60. domainkit-0.1.0/src/domainkit/ddd/model.py +79 -0
  61. domainkit-0.1.0/src/domainkit/ddd/pagination.py +24 -0
  62. domainkit-0.1.0/src/domainkit/ddd/repository.py +94 -0
  63. domainkit-0.1.0/src/domainkit/ddd/uow.py +114 -0
  64. domainkit-0.1.0/src/domainkit/deprecation.py +21 -0
  65. domainkit-0.1.0/src/domainkit/exceptions.py +8 -0
  66. domainkit-0.1.0/src/domainkit/http/__init__.py +24 -0
  67. domainkit-0.1.0/src/domainkit/http/application.py +73 -0
  68. domainkit-0.1.0/src/domainkit/http/errors.py +179 -0
  69. domainkit-0.1.0/src/domainkit/http/health.py +52 -0
  70. domainkit-0.1.0/src/domainkit/http/metrics.py +150 -0
  71. domainkit-0.1.0/src/domainkit/http/middleware.py +378 -0
  72. domainkit-0.1.0/src/domainkit/http/pagination.py +71 -0
  73. domainkit-0.1.0/src/domainkit/http/ratelimit.py +150 -0
  74. domainkit-0.1.0/src/domainkit/log.py +119 -0
  75. domainkit-0.1.0/src/domainkit/management/__init__.py +129 -0
  76. domainkit-0.1.0/src/domainkit/management/base.py +87 -0
  77. domainkit-0.1.0/src/domainkit/management/commands/__init__.py +1 -0
  78. domainkit-0.1.0/src/domainkit/management/commands/check.py +37 -0
  79. domainkit-0.1.0/src/domainkit/management/commands/makemigrations.py +61 -0
  80. domainkit-0.1.0/src/domainkit/management/commands/migrate.py +64 -0
  81. domainkit-0.1.0/src/domainkit/management/commands/runserver.py +60 -0
  82. domainkit-0.1.0/src/domainkit/management/commands/shell.py +27 -0
  83. domainkit-0.1.0/src/domainkit/management/commands/showmigrations.py +26 -0
  84. domainkit-0.1.0/src/domainkit/management/commands/startdomain.py +165 -0
  85. domainkit-0.1.0/src/domainkit/management/templates/domain/package/__init__.py-tpl +11 -0
  86. domainkit-0.1.0/src/domainkit/management/templates/domain/package/api/__init__.py-tpl +1 -0
  87. domainkit-0.1.0/src/domainkit/management/templates/domain/package/api/dependencies.py-tpl +47 -0
  88. domainkit-0.1.0/src/domainkit/management/templates/domain/package/api/routes.py-tpl +74 -0
  89. domainkit-0.1.0/src/domainkit/management/templates/domain/package/api/schemas.py-tpl +65 -0
  90. domainkit-0.1.0/src/domainkit/management/templates/domain/package/application/__init__.py-tpl +1 -0
  91. domainkit-0.1.0/src/domainkit/management/templates/domain/package/application/commands.py-tpl +24 -0
  92. domainkit-0.1.0/src/domainkit/management/templates/domain/package/application/handlers.py-tpl +18 -0
  93. domainkit-0.1.0/src/domainkit/management/templates/domain/package/application/services.py-tpl +63 -0
  94. domainkit-0.1.0/src/domainkit/management/templates/domain/package/application/unit_of_work.py-tpl +12 -0
  95. domainkit-0.1.0/src/domainkit/management/templates/domain/package/apps.py-tpl +22 -0
  96. domainkit-0.1.0/src/domainkit/management/templates/domain/package/domain/__init__.py-tpl +1 -0
  97. domainkit-0.1.0/src/domainkit/management/templates/domain/package/domain/events.py-tpl +28 -0
  98. domainkit-0.1.0/src/domainkit/management/templates/domain/package/domain/exceptions.py-tpl +15 -0
  99. domainkit-0.1.0/src/domainkit/management/templates/domain/package/domain/model.py-tpl +71 -0
  100. domainkit-0.1.0/src/domainkit/management/templates/domain/package/domain/repositories.py-tpl +33 -0
  101. domainkit-0.1.0/src/domainkit/management/templates/domain/package/infrastructure/__init__.py-tpl +1 -0
  102. domainkit-0.1.0/src/domainkit/management/templates/domain/package/infrastructure/orm.py-tpl +44 -0
  103. domainkit-0.1.0/src/domainkit/management/templates/domain/package/infrastructure/repositories.py-tpl +26 -0
  104. domainkit-0.1.0/src/domainkit/management/templates/domain/package/infrastructure/unit_of_work.py-tpl +15 -0
  105. domainkit-0.1.0/src/domainkit/management/templates/domain/package/management/__init__.py-tpl +1 -0
  106. domainkit-0.1.0/src/domainkit/management/templates/domain/package/management/commands/__init__.py-tpl +1 -0
  107. domainkit-0.1.0/src/domainkit/management/templates/domain/package/management/commands/{{ aggregates }}.py-tpl +25 -0
  108. domainkit-0.1.0/src/domainkit/management/templates/domain/package/settings.py-tpl +30 -0
  109. domainkit-0.1.0/src/domainkit/management/templates/domain/tests/__init__.py-tpl +1 -0
  110. domainkit-0.1.0/src/domainkit/management/templates/domain/tests/fakes.py-tpl +47 -0
  111. domainkit-0.1.0/src/domainkit/management/templates/domain/tests/test_api.py-tpl +104 -0
  112. domainkit-0.1.0/src/domainkit/management/templates/domain/tests/test_model.py-tpl +67 -0
  113. domainkit-0.1.0/src/domainkit/management/templates/domain/tests/test_services.py-tpl +71 -0
  114. domainkit-0.1.0/src/domainkit/management/templates/domain/tests/test_settings.py-tpl +20 -0
  115. domainkit-0.1.0/src/domainkit/py.typed +0 -0
  116. domainkit-0.1.0/src/domainkit/reporting.py +50 -0
  117. domainkit-0.1.0/src/domainkit/testing.py +83 -0
  118. domainkit-0.1.0/src/domainkit/tracing.py +42 -0
  119. domainkit-0.1.0/src/domainkit/utils/__init__.py +1 -0
  120. domainkit-0.1.0/src/domainkit/utils/formatting.py +26 -0
  121. domainkit-0.1.0/src/domainkit/utils/module_loading.py +22 -0
  122. domainkit-0.1.0/src/domainkit/utils/rates.py +17 -0
  123. domainkit-0.1.0/src/domainkit/utils/readiness.py +31 -0
  124. domainkit-0.1.0/src/domainkit/utils/text.py +25 -0
@@ -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.
@@ -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,6 @@
1
+ """Installed domains (``django.apps``): configuration classes and the registry."""
2
+
3
+ from domainkit.apps.config import DomainConfig
4
+ from domainkit.apps.registry import DomainRegistry, registry
5
+
6
+ __all__ = ["DomainConfig", "DomainRegistry", "registry"]
@@ -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