domainkit 0.1.0__tar.gz → 0.2.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 (140) hide show
  1. {domainkit-0.1.0 → domainkit-0.2.0}/PKG-INFO +42 -95
  2. {domainkit-0.1.0 → domainkit-0.2.0}/README.md +38 -94
  3. {domainkit-0.1.0 → domainkit-0.2.0}/pyproject.toml +7 -1
  4. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/audit/protection.py +3 -2
  5. domainkit-0.2.0/src/domainkit/management/commands/init.py +108 -0
  6. domainkit-0.2.0/src/domainkit/management/templates/project/dot-env-tpl +3 -0
  7. domainkit-0.2.0/src/domainkit/management/templates/project/dot-gitignore-tpl +11 -0
  8. domainkit-0.2.0/src/domainkit/management/templates/project/manage.py-tpl +15 -0
  9. domainkit-0.2.0/src/domainkit/management/templates/project/pyproject.toml-tpl +17 -0
  10. domainkit-0.2.0/src/domainkit/management/templates/project/src/config/__init__.py-tpl +1 -0
  11. domainkit-0.2.0/src/domainkit/management/templates/project/src/config/asgi.py-tpl +5 -0
  12. domainkit-0.2.0/src/domainkit/management/templates/project/src/config/settings/__init__.py-tpl +1 -0
  13. domainkit-0.2.0/src/domainkit/management/templates/project/src/config/settings/base.py-tpl +13 -0
  14. domainkit-0.2.0/src/domainkit/management/templates/project/src/config/settings/local.py-tpl +11 -0
  15. domainkit-0.2.0/src/domainkit/management/templates/project/src/config/settings/production.py-tpl +16 -0
  16. domainkit-0.2.0/src/domainkit/management/templates/project/src/config/settings/test.py-tpl +7 -0
  17. domainkit-0.2.0/src/domainkit/management/templates/project/src/config/urls.py-tpl +8 -0
  18. domainkit-0.2.0/src/domainkit/management/templates/project/src/domains/__init__.py-tpl +1 -0
  19. domainkit-0.2.0/src/domainkit/management/templates/project/tests/__init__.py-tpl +1 -0
  20. domainkit-0.2.0/src/domainkit/management/templates/project/tests/test_health.py-tpl +8 -0
  21. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/utils/formatting.py +3 -2
  22. {domainkit-0.1.0 → domainkit-0.2.0}/LICENSE +0 -0
  23. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/__init__.py +0 -0
  24. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/apps/__init__.py +0 -0
  25. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/apps/config.py +0 -0
  26. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/apps/registry.py +0 -0
  27. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/architecture.py +0 -0
  28. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/conf/__init__.py +0 -0
  29. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/conf/env.py +0 -0
  30. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/conf/global_settings.py +0 -0
  31. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/conf/lazy.py +0 -0
  32. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/context.py +0 -0
  33. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/__init__.py +0 -0
  34. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/audit/__init__.py +0 -0
  35. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/audit/apps.py +0 -0
  36. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/audit/management/__init__.py +0 -0
  37. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/audit/management/commands/__init__.py +0 -0
  38. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/audit/management/commands/auditlog.py +0 -0
  39. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/audit/recorder.py +0 -0
  40. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/audit/settings.py +0 -0
  41. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/audit/tables.py +0 -0
  42. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/idempotency/__init__.py +0 -0
  43. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/idempotency/apps.py +0 -0
  44. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/idempotency/middleware.py +0 -0
  45. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/idempotency/settings.py +0 -0
  46. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/idempotency/tables.py +0 -0
  47. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/idempotency/tasks.py +0 -0
  48. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/__init__.py +0 -0
  49. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/admin.py +0 -0
  50. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/apps.py +0 -0
  51. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/management/__init__.py +0 -0
  52. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/management/commands/__init__.py +0 -0
  53. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/management/commands/messages.py +0 -0
  54. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/management/commands/runworker.py +0 -0
  55. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/metrics.py +0 -0
  56. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/outbox.py +0 -0
  57. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/registry.py +0 -0
  58. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/settings.py +0 -0
  59. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/tables.py +0 -0
  60. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/contrib/messaging/worker.py +0 -0
  61. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/__init__.py +0 -0
  62. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/connections.py +0 -0
  63. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/drivers.py +0 -0
  64. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/migration_env/__init__.py +0 -0
  65. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/migration_env/env.py +0 -0
  66. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/migration_env/script.py.mako +0 -0
  67. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/migrations.py +0 -0
  68. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/orm.py +0 -0
  69. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/repository.py +0 -0
  70. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/types.py +0 -0
  71. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/db/uow.py +0 -0
  72. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/ddd/__init__.py +0 -0
  73. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/ddd/_dataclass.py +0 -0
  74. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/ddd/events.py +0 -0
  75. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/ddd/exceptions.py +0 -0
  76. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/ddd/messages.py +0 -0
  77. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/ddd/model.py +0 -0
  78. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/ddd/pagination.py +0 -0
  79. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/ddd/repository.py +0 -0
  80. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/ddd/uow.py +0 -0
  81. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/deprecation.py +0 -0
  82. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/exceptions.py +0 -0
  83. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/http/__init__.py +0 -0
  84. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/http/application.py +0 -0
  85. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/http/errors.py +0 -0
  86. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/http/health.py +0 -0
  87. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/http/metrics.py +0 -0
  88. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/http/middleware.py +0 -0
  89. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/http/pagination.py +0 -0
  90. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/http/ratelimit.py +0 -0
  91. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/log.py +0 -0
  92. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/__init__.py +0 -0
  93. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/base.py +0 -0
  94. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/commands/__init__.py +0 -0
  95. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/commands/check.py +0 -0
  96. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/commands/makemigrations.py +0 -0
  97. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/commands/migrate.py +0 -0
  98. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/commands/runserver.py +0 -0
  99. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/commands/shell.py +0 -0
  100. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/commands/showmigrations.py +0 -0
  101. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/commands/startdomain.py +0 -0
  102. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/__init__.py-tpl +0 -0
  103. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/api/__init__.py-tpl +0 -0
  104. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/api/dependencies.py-tpl +0 -0
  105. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/api/routes.py-tpl +0 -0
  106. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/api/schemas.py-tpl +0 -0
  107. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/application/__init__.py-tpl +0 -0
  108. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/application/commands.py-tpl +0 -0
  109. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/application/handlers.py-tpl +0 -0
  110. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/application/services.py-tpl +0 -0
  111. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/application/unit_of_work.py-tpl +0 -0
  112. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/apps.py-tpl +0 -0
  113. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/domain/__init__.py-tpl +0 -0
  114. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/domain/events.py-tpl +0 -0
  115. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/domain/exceptions.py-tpl +0 -0
  116. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/domain/model.py-tpl +0 -0
  117. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/domain/repositories.py-tpl +0 -0
  118. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/infrastructure/__init__.py-tpl +0 -0
  119. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/infrastructure/orm.py-tpl +0 -0
  120. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/infrastructure/repositories.py-tpl +0 -0
  121. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/infrastructure/unit_of_work.py-tpl +0 -0
  122. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/management/__init__.py-tpl +0 -0
  123. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/management/commands/__init__.py-tpl +0 -0
  124. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/management/commands/{{ aggregates }}.py-tpl +0 -0
  125. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/package/settings.py-tpl +0 -0
  126. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/tests/__init__.py-tpl +0 -0
  127. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/tests/fakes.py-tpl +0 -0
  128. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/tests/test_api.py-tpl +0 -0
  129. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/tests/test_model.py-tpl +0 -0
  130. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/tests/test_services.py-tpl +0 -0
  131. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/management/templates/domain/tests/test_settings.py-tpl +0 -0
  132. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/py.typed +0 -0
  133. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/reporting.py +0 -0
  134. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/testing.py +0 -0
  135. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/tracing.py +0 -0
  136. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/utils/__init__.py +0 -0
  137. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/utils/module_loading.py +0 -0
  138. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/utils/rates.py +0 -0
  139. {domainkit-0.1.0 → domainkit-0.2.0}/src/domainkit/utils/readiness.py +0 -0
  140. {domainkit-0.1.0 → domainkit-0.2.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.1.0
3
+ Version: 0.2.0
4
4
  Summary: A Django-style framework for domain-driven FastAPI services.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -19,14 +19,17 @@ Classifier: Typing :: Typed
19
19
  Provides-Extra: metrics
20
20
  Provides-Extra: postgres
21
21
  Provides-Extra: sqlite
22
+ Provides-Extra: testing
22
23
  Provides-Extra: tracing
23
24
  Requires-Dist: aiosqlite (>=0.22.1,<0.23.0) ; extra == "sqlite"
24
25
  Requires-Dist: alembic (>=1.20.0,<2.0.0)
25
26
  Requires-Dist: asyncpg (>=0.31.0,<0.32.0) ; extra == "postgres"
26
27
  Requires-Dist: fastapi (>=0.141.1,<0.142.0)
28
+ Requires-Dist: httpx2 (>=2.13.0,<3.0.0) ; extra == "testing"
27
29
  Requires-Dist: opentelemetry-api (>=1.45.1,<2.0.0) ; extra == "tracing"
28
30
  Requires-Dist: prometheus-client (>=0.26.0,<0.27.0) ; extra == "metrics"
29
31
  Requires-Dist: pydantic (>=2.14.1,<3.0.0)
32
+ Requires-Dist: pytest (>=9.1.1,<10.0.0) ; extra == "testing"
30
33
  Requires-Dist: python-dotenv (>=1.2.4,<2.0.0)
31
34
  Requires-Dist: sqlalchemy[asyncio] (>=2.0.54,<2.1)
32
35
  Requires-Dist: uvicorn[standard] (>=0.54.0,<0.55.0)
@@ -46,7 +49,7 @@ A Django-style framework for domain-driven FastAPI services. It provides:
46
49
  - optional apps for reliable messaging, audit trails, and idempotent requests
47
50
 
48
51
  ```bash
49
- pip install "domainkit[postgres]" # extras: postgres, sqlite, metrics, tracing
52
+ pip install "domainkit[postgres]" # extras: postgres, sqlite, metrics, tracing, testing
50
53
  ```
51
54
 
52
55
  ## Documentation
@@ -60,105 +63,36 @@ pip install "domainkit[postgres]" # extras: postgres, sqlite, metrics, traci
60
63
 
61
64
  ## Quick start
62
65
 
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
+ ```bash
67
+ python3.13 -m venv .venv && . .venv/bin/activate
68
+ pip install "domainkit[postgres,sqlite,testing]"
69
+ domainkit init billingAPI billing # the name sets the variable prefix: BILLING_API_*
70
+ cd billing
71
+ python manage.py check
72
+ python manage.py startdomain invoices --aggregate Invoice # prints how to install it
73
+ python manage.py makemigrations -m "create invoices" && python manage.py migrate
74
+ pytest
75
+ python manage.py runserver # http://127.0.0.1:8000/docs
76
+ ```
77
+
78
+ `domainkit init` creates a project that passes `check` and its tests as it is:
66
79
 
67
80
  ```text
68
81
  billing/
69
- pyproject.toml
82
+ pyproject.toml the name, dependencies, and the pytest plugin
70
83
  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"
84
+ .env selects config.settings.local on this machine
85
+ .gitignore
86
+ src/config/settings/ base, local, test, and production settings
87
+ src/config/urls.py the root router: mount each domain's router here
88
+ src/config/asgi.py the application, for uvicorn
89
+ src/domains/ the bounded contexts
90
+ tests/test_health.py
92
91
  ```
93
92
 
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
- ```
93
+ The production settings read `BILLING_API_ALLOWED_HOSTS` and
94
+ `BILLING_API_DATABASE_URL` from the environment, and the deployment checks
95
+ refuse to start without them, or without verified TLS to the database.
162
96
 
163
97
  ## Developing domainkit
164
98
 
@@ -171,11 +105,24 @@ poetry run pytest --cov # random order; coverage gate in pyprojec
171
105
  poetry run mutmut run "domainkit.http*" && poetry run mutmut results
172
106
  ```
173
107
 
108
+ The database tests use SQLite unless `DOMAINKIT_TEST_POSTGRES_URL` names a
109
+ PostgreSQL server; each test then creates a database there and drops it.
110
+ Use a throwaway server:
111
+
112
+ ```bash
113
+ docker run -d --rm --name domainkit-postgres -e POSTGRES_PASSWORD=postgres \
114
+ -p 127.0.0.1:55432:5432 postgres:18-alpine
115
+ DOMAINKIT_TEST_POSTGRES_URL=postgresql+asyncpg://postgres:postgres@127.0.0.1:55432/postgres \
116
+ poetry run pytest
117
+ ```
118
+
174
119
  CI (`.github/workflows/ci.yml`) runs the same checks on every push and pull
175
120
  request:
176
121
 
177
122
  - lint and strict typing
178
123
  - the tests on Python 3.13 and 3.14
124
+ - the tests again with PostgreSQL 18, including row locking, the audit
125
+ triggers, and server-side timeouts
179
126
  - a wheel build, installed and imported in a clean environment
180
127
  - pip-audit, and a gitleaks scan of the whole history
181
128
 
@@ -8,7 +8,7 @@ A Django-style framework for domain-driven FastAPI services. It provides:
8
8
  - optional apps for reliable messaging, audit trails, and idempotent requests
9
9
 
10
10
  ```bash
11
- pip install "domainkit[postgres]" # extras: postgres, sqlite, metrics, tracing
11
+ pip install "domainkit[postgres]" # extras: postgres, sqlite, metrics, tracing, testing
12
12
  ```
13
13
 
14
14
  ## Documentation
@@ -22,105 +22,36 @@ pip install "domainkit[postgres]" # extras: postgres, sqlite, metrics, traci
22
22
 
23
23
  ## Quick start
24
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_*`.
25
+ ```bash
26
+ python3.13 -m venv .venv && . .venv/bin/activate
27
+ pip install "domainkit[postgres,sqlite,testing]"
28
+ domainkit init billingAPI billing # the name sets the variable prefix: BILLING_API_*
29
+ cd billing
30
+ python manage.py check
31
+ python manage.py startdomain invoices --aggregate Invoice # prints how to install it
32
+ python manage.py makemigrations -m "create invoices" && python manage.py migrate
33
+ pytest
34
+ python manage.py runserver # http://127.0.0.1:8000/docs
35
+ ```
36
+
37
+ `domainkit init` creates a project that passes `check` and its tests as it is:
28
38
 
29
39
  ```text
30
40
  billing/
31
- pyproject.toml
41
+ pyproject.toml the name, dependencies, and the pytest plugin
32
42
  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"
43
+ .env selects config.settings.local on this machine
44
+ .gitignore
45
+ src/config/settings/ base, local, test, and production settings
46
+ src/config/urls.py the root router: mount each domain's router here
47
+ src/config/asgi.py the application, for uvicorn
48
+ src/domains/ the bounded contexts
49
+ tests/test_health.py
54
50
  ```
55
51
 
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
- ```
52
+ The production settings read `BILLING_API_ALLOWED_HOSTS` and
53
+ `BILLING_API_DATABASE_URL` from the environment, and the deployment checks
54
+ refuse to start without them, or without verified TLS to the database.
124
55
 
125
56
  ## Developing domainkit
126
57
 
@@ -133,11 +64,24 @@ poetry run pytest --cov # random order; coverage gate in pyprojec
133
64
  poetry run mutmut run "domainkit.http*" && poetry run mutmut results
134
65
  ```
135
66
 
67
+ The database tests use SQLite unless `DOMAINKIT_TEST_POSTGRES_URL` names a
68
+ PostgreSQL server; each test then creates a database there and drops it.
69
+ Use a throwaway server:
70
+
71
+ ```bash
72
+ docker run -d --rm --name domainkit-postgres -e POSTGRES_PASSWORD=postgres \
73
+ -p 127.0.0.1:55432:5432 postgres:18-alpine
74
+ DOMAINKIT_TEST_POSTGRES_URL=postgresql+asyncpg://postgres:postgres@127.0.0.1:55432/postgres \
75
+ poetry run pytest
76
+ ```
77
+
136
78
  CI (`.github/workflows/ci.yml`) runs the same checks on every push and pull
137
79
  request:
138
80
 
139
81
  - lint and strict typing
140
82
  - the tests on Python 3.13 and 3.14
83
+ - the tests again with PostgreSQL 18, including row locking, the audit
84
+ triggers, and server-side timeouts
141
85
  - a wheel build, installed and imported in a clean environment
142
86
  - pip-audit, and a gitleaks scan of the whole history
143
87
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "domainkit"
3
- version = "0.1.0"
3
+ version = "0.2.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,12 @@ 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
+ # The domainkit.testing pytest plugin; Starlette's TestClient needs httpx2.
39
+ testing = ["pytest (>=9.1.1,<10.0.0)", "httpx2 (>=2.13.0,<3.0.0)"]
40
+
41
+ # Runs commands before a project exists: "domainkit init billingAPI".
42
+ [project.scripts]
43
+ domainkit = "domainkit.management:execute_from_command_line"
38
44
 
39
45
  [project.urls]
40
46
  Homepage = "https://github.com/karimmattar/domainkit"
@@ -2,8 +2,9 @@
2
2
 
3
3
  Revoking ``UPDATE`` and ``DELETE`` from the application's role is undone by
4
4
  the next broad ``GRANT``. These triggers refuse updates, deletes, and
5
- truncation for every role but the table owner, who alone can drop them (for
6
- example, to apply a retention policy). Call them from a project revision::
5
+ truncation for every role, the table owner included; only the owner can drop
6
+ them (for example, to apply a retention policy). Call them from a project
7
+ revision::
7
8
 
8
9
  from domainkit.contrib.audit.protection import protect, unprotect
9
10
 
@@ -0,0 +1,108 @@
1
+ """``init``: create a project from the project template (Django's ``startproject``).
2
+
3
+ It runs before any project exists, through the ``domainkit`` program the
4
+ package installs::
5
+
6
+ domainkit init billingAPI # creates ./billingAPI
7
+ domainkit init billingAPI billing # or names the directory
8
+
9
+ The project gets settings for local work, tests, and production; its routes
10
+ and ASGI application; ``manage.py``; and a first test. Its name sets the
11
+ environment variable prefix (``billingAPI``: ``BILLING_API_``).
12
+ """
13
+
14
+ import re
15
+ from argparse import ArgumentParser
16
+ from pathlib import Path
17
+ from typing import Final
18
+
19
+ from domainkit import __version__
20
+ from domainkit.conf.env import prefix_for
21
+ from domainkit.management.base import BaseCommand, CommandError
22
+ from domainkit.management.commands.startdomain import render
23
+ from domainkit.utils.formatting import format_python
24
+
25
+ TEMPLATE_DIR: Final = Path(__file__).resolve().parents[1] / "templates" / "project"
26
+ DOTFILE: Final = "dot-"
27
+ """Template files named ``dot-<name>`` become ``.<name>`` (``.env``, ``.gitignore``)."""
28
+ _PROJECT_NAME: Final = re.compile(r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?")
29
+ _RELEASE: Final = re.compile(r"(\d+)\.(\d+)\.\d+")
30
+
31
+
32
+ def requirement(version: str) -> str:
33
+ """Depend on this release up to the next one that may break it.
34
+
35
+ Before 1.0 a minor release may break (``>=0.1.1,<0.2``); after it, a
36
+ major one. A version without release numbers (a source checkout) pins
37
+ nothing.
38
+ """
39
+ extras = "domainkit[postgres,sqlite]"
40
+ match = _RELEASE.fullmatch(version)
41
+ if match is None:
42
+ return extras
43
+ major, minor = int(match[1]), int(match[2])
44
+ upper = f"0.{minor + 1}" if major == 0 else f"{major + 1}"
45
+ return f"{extras}>={version},<{upper}"
46
+
47
+
48
+ class Command(BaseCommand):
49
+ """Generate a project directory that passes ``check`` and its tests."""
50
+
51
+ help = "Create a project: settings, routes, manage.py, and a first test."
52
+ requires_setup = False
53
+
54
+ def add_arguments(self, parser: ArgumentParser) -> None:
55
+ """Accept the project name and an optional directory."""
56
+ parser.add_argument(
57
+ "name",
58
+ help="project name, as in [project].name; it sets the environment "
59
+ "variable prefix (billingAPI: BILLING_API_)",
60
+ )
61
+ parser.add_argument(
62
+ "directory",
63
+ nargs="?",
64
+ type=Path,
65
+ help="where to create it (default: ./<name>); empty or missing",
66
+ )
67
+
68
+ def handle(self, *, name: str, directory: Path | None, **options: object) -> None:
69
+ """Render the template, then print the next steps."""
70
+ if not _PROJECT_NAME.fullmatch(name):
71
+ raise CommandError(
72
+ f"{name!r} is not a valid project name: use letters, digits, '.', "
73
+ "'-', and '_', starting and ending with a letter or digit."
74
+ )
75
+ prefix = prefix_for(name)
76
+ if not prefix[0].isalpha():
77
+ raise CommandError(
78
+ f"{name!r} must start with a letter: it prefixes the project's "
79
+ f"environment variables ({prefix}...)."
80
+ )
81
+ target = (directory or Path(name)).resolve()
82
+ if target.exists() and (not target.is_dir() or any(target.iterdir())):
83
+ raise CommandError(f"{target} already exists and is not empty.")
84
+ context = {
85
+ "project": name,
86
+ "prefix": prefix,
87
+ "dependency": requirement(__version__),
88
+ }
89
+ for path in render(TEMPLATE_DIR, target, context):
90
+ if path.name.startswith(DOTFILE):
91
+ path.rename(path.with_name(f".{path.name.removeprefix(DOTFILE)}"))
92
+ format_python([target])
93
+ self.write(_next_steps(name, target))
94
+
95
+
96
+ def _next_steps(name: str, target: Path) -> str:
97
+ """Explain how to install and run the new project."""
98
+ return "\n".join(
99
+ [
100
+ f"Created {name} in {target}.",
101
+ "Next steps:",
102
+ f" cd {target.relative_to(Path.cwd(), walk_up=True)}",
103
+ ' pip install "domainkit[postgres,sqlite,testing]" # or: uv sync',
104
+ " python manage.py check",
105
+ " python manage.py startdomain <name>",
106
+ " pytest",
107
+ ]
108
+ )
@@ -0,0 +1,3 @@
1
+ # This machine's settings. Never commit or deploy this file: deployments set
2
+ # {{ prefix }}SETTINGS_MODULE themselves.
3
+ {{ prefix }}SETTINGS_MODULE=config.settings.local
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+ .coverage
8
+ htmlcov/
9
+ dist/
10
+ .env
11
+ *.sqlite3
@@ -0,0 +1,15 @@
1
+ """Run the project's commands: python manage.py <command>."""
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ ROOT = Path(__file__).resolve().parent
8
+
9
+ if __name__ == "__main__":
10
+ # Find pyproject.toml and .env whatever the working directory.
11
+ os.environ.setdefault("DOMAINKIT_PROJECT_ROOT", str(ROOT))
12
+ sys.path.insert(0, str(ROOT / "src"))
13
+ from domainkit.management import execute_from_command_line
14
+
15
+ execute_from_command_line(sys.argv)
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "{{ project }}"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.13"
5
+ dependencies = ["{{ dependency }}"]
6
+
7
+ [dependency-groups]
8
+ dev = ["domainkit[testing]"] # pytest and httpx2, for the fixtures
9
+
10
+ [tool.pytest.ini_options]
11
+ pythonpath = ["src"]
12
+ addopts = "-p domainkit.testing" # the database and client fixtures
13
+ domainkit_settings = "config.settings.test"
14
+
15
+ [tool.ruff.lint.per-file-ignores]
16
+ # Each settings module builds on base.py with a star import.
17
+ "src/config/settings/*.py" = ["F403", "F405"]
@@ -0,0 +1 @@
1
+ """Project configuration: settings, routes, and the ASGI application."""
@@ -0,0 +1,5 @@
1
+ """The ASGI application, for uvicorn: config.asgi:application."""
2
+
3
+ from domainkit.http import get_asgi_application
4
+
5
+ application = get_asgi_application()
@@ -0,0 +1 @@
1
+ """Settings modules; {{ prefix }}SETTINGS_MODULE selects one."""
@@ -0,0 +1,13 @@
1
+ """Settings shared by every environment."""
2
+
3
+ from pathlib import Path
4
+
5
+ from domainkit.conf.env import Env
6
+
7
+ env = Env()
8
+ BASE_DIR = Path(__file__).resolve().parents[3]
9
+
10
+ PROJECT_NAME = "{{ project }}"
11
+ INSTALLED_DOMAINS = ["domainkit.contrib.messaging"]
12
+ ROOT_ROUTER = "config.urls.router"
13
+ ASGI_APPLICATION = "config.asgi:application"
@@ -0,0 +1,11 @@
1
+ """Settings for development on this machine (selected by .env)."""
2
+
3
+ from config.settings.base import *
4
+ from config.settings.base import BASE_DIR, env
5
+
6
+ DEBUG = True
7
+ DATABASES = {
8
+ "default": {
9
+ "URL": env.str("DATABASE_URL", f"sqlite+aiosqlite:///{BASE_DIR / 'db.sqlite3'}")
10
+ },
11
+ }
@@ -0,0 +1,16 @@
1
+ """Settings for production: addresses and secrets come from the environment.
2
+
3
+ Set {{ prefix }}SETTINGS_MODULE=config.settings.production,
4
+ {{ prefix }}ALLOWED_HOSTS (comma-separated), and {{ prefix }}DATABASE_URL.
5
+ Startup fails when one is missing, and the deployment checks refuse DEBUG,
6
+ wildcard hosts, and network databases without verified TLS.
7
+ """
8
+
9
+ from config.settings.base import *
10
+ from config.settings.base import env
11
+
12
+ ENVIRONMENT = "production"
13
+ ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
14
+ DATABASES = {
15
+ "default": {"URL": env.str("DATABASE_URL"), "SSLMODE": "verify-full"},
16
+ }
@@ -0,0 +1,7 @@
1
+ """Settings for the tests (domainkit_settings in pyproject.toml)."""
2
+
3
+ from config.settings.base import *
4
+
5
+ ENVIRONMENT = "test"
6
+ ALLOWED_HOSTS = ["testserver"]
7
+ DATABASES = {"default": {"URL": "sqlite+aiosqlite:///:memory:"}}
@@ -0,0 +1,8 @@
1
+ """The project's routes: mount each domain's router here."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from domainkit.http.health import router as health_router
6
+
7
+ router = APIRouter()
8
+ router.include_router(health_router)
@@ -0,0 +1 @@
1
+ """The bounded contexts; add one with: python manage.py startdomain <name>."""
@@ -0,0 +1 @@
1
+ """The project's tests."""
@@ -0,0 +1,8 @@
1
+ """The application starts and answers its liveness probe."""
2
+
3
+ from fastapi.testclient import TestClient
4
+
5
+
6
+ def test_health(client: TestClient) -> None:
7
+ """GET /health answers 200."""
8
+ assert client.get("/health").status_code == 200
@@ -18,9 +18,10 @@ def format_python(paths: Iterable[Path]) -> bool:
18
18
  return False
19
19
  targets = [str(path) for path in paths]
20
20
  ruff = [sys.executable, "-m", "ruff"]
21
+ # No cache: it would land in a project that was just generated.
21
22
  for arguments in (
22
- ["check", "--fix", "--select", "I", "--quiet", *targets],
23
- ["format", "--quiet", *targets],
23
+ ["check", "--fix", "--select", "I", "--quiet", "--no-cache", *targets],
24
+ ["format", "--quiet", "--no-cache", *targets],
24
25
  ):
25
26
  subprocess.run([*ruff, *arguments], check=False) # noqa: S603 - fixed program, generated paths.
26
27
  return True
File without changes