python-ddd-framework 0.3.1__py3-none-any.whl

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 (355) hide show
  1. python_ddd_framework/__init__.py +449 -0
  2. python_ddd_framework/application/__init__.py +6 -0
  3. python_ddd_framework/application/build_spec.py +37 -0
  4. python_ddd_framework/application/builder.py +260 -0
  5. python_ddd_framework/application/composition.py +219 -0
  6. python_ddd_framework/application/runtime.py +549 -0
  7. python_ddd_framework/application/runtime_hooks.py +327 -0
  8. python_ddd_framework/application/state.py +216 -0
  9. python_ddd_framework/application_services/__init__.py +54 -0
  10. python_ddd_framework/application_services/catalog.py +500 -0
  11. python_ddd_framework/application_services/contracts.py +190 -0
  12. python_ddd_framework/application_services/dispatcher.py +423 -0
  13. python_ddd_framework/application_services/errors.py +89 -0
  14. python_ddd_framework/application_services/execution.py +199 -0
  15. python_ddd_framework/application_services/interceptors.py +44 -0
  16. python_ddd_framework/application_services/invocation.py +431 -0
  17. python_ddd_framework/application_services/policies.py +46 -0
  18. python_ddd_framework/application_services/seeding.py +72 -0
  19. python_ddd_framework/application_services/signature.py +49 -0
  20. python_ddd_framework/application_services/validation.py +84 -0
  21. python_ddd_framework/auditing/__init__.py +12 -0
  22. python_ddd_framework/auditing/contracts.py +39 -0
  23. python_ddd_framework/auditing/control.py +48 -0
  24. python_ddd_framework/auditing/sqlalchemy/__init__.py +6 -0
  25. python_ddd_framework/auditing/sqlalchemy/migrations/0001_auditing.py +45 -0
  26. python_ddd_framework/auditing/sqlalchemy/migrations/__init__.py +1 -0
  27. python_ddd_framework/auditing/sqlalchemy/models.py +31 -0
  28. python_ddd_framework/auditing/sqlalchemy/module.py +32 -0
  29. python_ddd_framework/auditing/sqlalchemy/store.py +63 -0
  30. python_ddd_framework/authorization/__init__.py +38 -0
  31. python_ddd_framework/authorization/catalog.py +63 -0
  32. python_ddd_framework/authorization/contracts.py +241 -0
  33. python_ddd_framework/authorization/definition_discovery.py +81 -0
  34. python_ddd_framework/authorization/definitions.py +43 -0
  35. python_ddd_framework/authorization/errors.py +40 -0
  36. python_ddd_framework/background_execution/__init__.py +17 -0
  37. python_ddd_framework/background_execution/application.py +41 -0
  38. python_ddd_framework/background_execution/child.py +123 -0
  39. python_ddd_framework/background_execution/contracts.py +48 -0
  40. python_ddd_framework/background_execution/lifecycle.py +22 -0
  41. python_ddd_framework/background_execution/local.py +127 -0
  42. python_ddd_framework/background_execution/locks.py +33 -0
  43. python_ddd_framework/background_execution/management.py +17 -0
  44. python_ddd_framework/background_execution/module.py +10 -0
  45. python_ddd_framework/background_execution/permissions.py +14 -0
  46. python_ddd_framework/background_execution/processes.py +331 -0
  47. python_ddd_framework/background_jobs/__init__.py +31 -0
  48. python_ddd_framework/background_jobs/catalog.py +340 -0
  49. python_ddd_framework/background_jobs/contracts.py +208 -0
  50. python_ddd_framework/background_jobs/declaration.py +67 -0
  51. python_ddd_framework/background_jobs/errors.py +28 -0
  52. python_ddd_framework/background_jobs/execution.py +203 -0
  53. python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
  54. python_ddd_framework/background_jobs/pgqueuer/__init__.py +6 -0
  55. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +86 -0
  56. python_ddd_framework/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +95 -0
  57. python_ddd_framework/background_jobs/pgqueuer/migrations/__init__.py +1 -0
  58. python_ddd_framework/background_jobs/pgqueuer/module.py +106 -0
  59. python_ddd_framework/background_jobs/pgqueuer/options.py +82 -0
  60. python_ddd_framework/background_jobs/pgqueuer/runtime.py +479 -0
  61. python_ddd_framework/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
  62. python_ddd_framework/background_jobs/pgqueuer/supervision.py +201 -0
  63. python_ddd_framework/background_workers/__init__.py +22 -0
  64. python_ddd_framework/background_workers/catalog.py +118 -0
  65. python_ddd_framework/background_workers/contracts.py +152 -0
  66. python_ddd_framework/background_workers/errors.py +20 -0
  67. python_ddd_framework/background_workers/execution.py +133 -0
  68. python_ddd_framework/background_workers/runtime.py +219 -0
  69. python_ddd_framework/caching/__init__.py +11 -0
  70. python_ddd_framework/caching/catalog.py +73 -0
  71. python_ddd_framework/caching/contracts.py +73 -0
  72. python_ddd_framework/caching/errors.py +20 -0
  73. python_ddd_framework/cli/__init__.py +105 -0
  74. python_ddd_framework/cli/development.py +85 -0
  75. python_ddd_framework/cli/errors.py +5 -0
  76. python_ddd_framework/cli/inspection.py +96 -0
  77. python_ddd_framework/cli/project.py +69 -0
  78. python_ddd_framework/cli/runtime.py +56 -0
  79. python_ddd_framework/configuration/__init__.py +21 -0
  80. python_ddd_framework/configuration/composition.py +65 -0
  81. python_ddd_framework/configuration/contracts.py +68 -0
  82. python_ddd_framework/configuration/dotenv_source.py +51 -0
  83. python_ddd_framework/configuration/environment_source.py +38 -0
  84. python_ddd_framework/configuration/immutability.py +124 -0
  85. python_ddd_framework/configuration/input_shape.py +58 -0
  86. python_ddd_framework/configuration/merge.py +103 -0
  87. python_ddd_framework/configuration/root.py +215 -0
  88. python_ddd_framework/configuration/sources.py +519 -0
  89. python_ddd_framework/configuration/values.py +427 -0
  90. python_ddd_framework/configuration/yaml_source.py +51 -0
  91. python_ddd_framework/developer_kit/__init__.py +1 -0
  92. python_ddd_framework/developer_kit/generation.py +182 -0
  93. python_ddd_framework/developer_kit/project_metadata.py +45 -0
  94. python_ddd_framework/developer_kit/source.py +53 -0
  95. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -0
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +70 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +41 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +20 -0
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +7 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +83 -0
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +84 -0
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +35 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +56 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +14 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +73 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +25 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +36 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +7 -0
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/orders.py.jinja +25 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +26 -0
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/__init__.py.jinja +1 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +49 -0
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
  131. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +6 -0
  132. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +9 -0
  133. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
  134. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
  135. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +38 -0
  136. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
  137. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +130 -0
  138. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/app.development.yaml.jinja +30 -0
  139. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
  140. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +20 -0
  141. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +84 -0
  142. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +185 -0
  143. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
  144. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
  145. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +47 -0
  146. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +37 -0
  147. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
  148. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +57 -0
  149. python_ddd_framework/developer_kit/wiring.py +162 -0
  150. python_ddd_framework/diagnostics/__init__.py +23 -0
  151. python_ddd_framework/diagnostics/journal.py +174 -0
  152. python_ddd_framework/diagnostics/model.py +83 -0
  153. python_ddd_framework/diagnostics/source.py +25 -0
  154. python_ddd_framework/distributed_lock/__init__.py +6 -0
  155. python_ddd_framework/distributed_lock/contracts.py +20 -0
  156. python_ddd_framework/distributed_lock/options.py +19 -0
  157. python_ddd_framework/domain/__init__.py +13 -0
  158. python_ddd_framework/domain/aggregates.py +64 -0
  159. python_ddd_framework/domain/entities.py +33 -0
  160. python_ddd_framework/domain/errors.py +12 -0
  161. python_ddd_framework/domain/value_objects.py +31 -0
  162. python_ddd_framework/errors/__init__.py +75 -0
  163. python_ddd_framework/errors/base.py +13 -0
  164. python_ddd_framework/errors/business.py +53 -0
  165. python_ddd_framework/errors/configuration.py +119 -0
  166. python_ddd_framework/errors/diagnostics.py +12 -0
  167. python_ddd_framework/errors/lifecycle.py +164 -0
  168. python_ddd_framework/errors/modularity.py +100 -0
  169. python_ddd_framework/errors/services.py +100 -0
  170. python_ddd_framework/events/__init__.py +25 -0
  171. python_ddd_framework/events/aggregate.py +39 -0
  172. python_ddd_framework/events/catalog.py +136 -0
  173. python_ddd_framework/events/contracts.py +39 -0
  174. python_ddd_framework/events/contribution.py +128 -0
  175. python_ddd_framework/events/discovery.py +105 -0
  176. python_ddd_framework/events/errors.py +48 -0
  177. python_ddd_framework/events/runtime.py +192 -0
  178. python_ddd_framework/fastapi/__init__.py +49 -0
  179. python_ddd_framework/fastapi/action.py +175 -0
  180. python_ddd_framework/fastapi/adapter.py +391 -0
  181. python_ddd_framework/fastapi/application_services.py +654 -0
  182. python_ddd_framework/fastapi/background.py +42 -0
  183. python_ddd_framework/fastapi/contracts.py +275 -0
  184. python_ddd_framework/fastapi/errors.py +32 -0
  185. python_ddd_framework/fastapi/filters.py +111 -0
  186. python_ddd_framework/fastapi/health.py +44 -0
  187. python_ddd_framework/fastapi/http.py +72 -0
  188. python_ddd_framework/fastapi/http_router.py +216 -0
  189. python_ddd_framework/fastapi/manual_action.py +105 -0
  190. python_ddd_framework/fastapi/middleware.py +93 -0
  191. python_ddd_framework/fastapi/parameters.py +97 -0
  192. python_ddd_framework/fastapi/realtime/__init__.py +13 -0
  193. python_ddd_framework/fastapi/realtime/authentication.py +181 -0
  194. python_ddd_framework/fastapi/realtime/connection.py +166 -0
  195. python_ddd_framework/fastapi/realtime/module.py +41 -0
  196. python_ddd_framework/fastapi/realtime/options.py +50 -0
  197. python_ddd_framework/fastapi/realtime/runtime.py +423 -0
  198. python_ddd_framework/fastapi/request_context.py +247 -0
  199. python_ddd_framework/fastapi/route_integrity.py +88 -0
  200. python_ddd_framework/fastapi/routing.py +424 -0
  201. python_ddd_framework/fastapi/server.py +182 -0
  202. python_ddd_framework/fastapi/settings.py +40 -0
  203. python_ddd_framework/fastapi/tracing.py +62 -0
  204. python_ddd_framework/fastapi/transfer.py +128 -0
  205. python_ddd_framework/fastapi/upload_limits.py +63 -0
  206. python_ddd_framework/fastapi/uploads.py +94 -0
  207. python_ddd_framework/hosted_services/__init__.py +29 -0
  208. python_ddd_framework/hosted_services/bridge.py +420 -0
  209. python_ddd_framework/hosted_services/catalog.py +265 -0
  210. python_ddd_framework/hosted_services/contracts.py +55 -0
  211. python_ddd_framework/hosted_services/errors.py +53 -0
  212. python_ddd_framework/hosted_services/options.py +17 -0
  213. python_ddd_framework/hosted_services/runtime.py +267 -0
  214. python_ddd_framework/hosted_services/state.py +40 -0
  215. python_ddd_framework/hosting/__init__.py +4 -0
  216. python_ddd_framework/hosting/instance.py +40 -0
  217. python_ddd_framework/identity/__init__.py +73 -0
  218. python_ddd_framework/identity/application.py +287 -0
  219. python_ddd_framework/identity/contracts.py +283 -0
  220. python_ddd_framework/identity/errors.py +28 -0
  221. python_ddd_framework/identity/http_api.py +90 -0
  222. python_ddd_framework/identity/module.py +49 -0
  223. python_ddd_framework/identity/passwords.py +34 -0
  224. python_ddd_framework/identity/permissions.py +14 -0
  225. python_ddd_framework/identity/services.py +108 -0
  226. python_ddd_framework/identity/sqlalchemy/__init__.py +5 -0
  227. python_ddd_framework/identity/sqlalchemy/migrations/0001_identity.py +143 -0
  228. python_ddd_framework/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
  229. python_ddd_framework/identity/sqlalchemy/migrations/__init__.py +1 -0
  230. python_ddd_framework/identity/sqlalchemy/models.py +85 -0
  231. python_ddd_framework/identity/sqlalchemy/module.py +62 -0
  232. python_ddd_framework/identity/sqlalchemy/stores.py +526 -0
  233. python_ddd_framework/identity/tokens.py +106 -0
  234. python_ddd_framework/invocation/__init__.py +3 -0
  235. python_ddd_framework/invocation/callables.py +171 -0
  236. python_ddd_framework/invocation/contracts.py +33 -0
  237. python_ddd_framework/invocation/entries.py +54 -0
  238. python_ddd_framework/invocation/function_runtime.py +48 -0
  239. python_ddd_framework/invocation/interception.py +193 -0
  240. python_ddd_framework/lifecycle/__init__.py +25 -0
  241. python_ddd_framework/lifecycle/composition.py +35 -0
  242. python_ddd_framework/lifecycle/context.py +31 -0
  243. python_ddd_framework/lifecycle/runtime.py +43 -0
  244. python_ddd_framework/lifecycle/state.py +20 -0
  245. python_ddd_framework/modularity/__init__.py +15 -0
  246. python_ddd_framework/modularity/contracts.py +91 -0
  247. python_ddd_framework/modularity/discovery.py +157 -0
  248. python_ddd_framework/modularity/graph.py +334 -0
  249. python_ddd_framework/modularity/registry.py +100 -0
  250. python_ddd_framework/modularity/selection.py +16 -0
  251. python_ddd_framework/notifications/__init__.py +19 -0
  252. python_ddd_framework/notifications/catalog.py +46 -0
  253. python_ddd_framework/notifications/contracts.py +107 -0
  254. python_ddd_framework/observability/__init__.py +5 -0
  255. python_ddd_framework/observability/context.py +43 -0
  256. python_ddd_framework/observability/export.py +73 -0
  257. python_ddd_framework/observability/formatting.py +89 -0
  258. python_ddd_framework/observability/logging.py +118 -0
  259. python_ddd_framework/observability/options.py +45 -0
  260. python_ddd_framework/observability/tracing.py +80 -0
  261. python_ddd_framework/options/__init__.py +19 -0
  262. python_ddd_framework/options/aliases.py +331 -0
  263. python_ddd_framework/options/contribution.py +62 -0
  264. python_ddd_framework/options/immutability.py +48 -0
  265. python_ddd_framework/options/input_keys.py +214 -0
  266. python_ddd_framework/options/issues.py +335 -0
  267. python_ddd_framework/options/models.py +114 -0
  268. python_ddd_framework/options/registry.py +280 -0
  269. python_ddd_framework/options/schema.py +488 -0
  270. python_ddd_framework/options/validation.py +120 -0
  271. python_ddd_framework/py.typed +0 -0
  272. python_ddd_framework/realtime/__init__.py +12 -0
  273. python_ddd_framework/realtime/contracts.py +28 -0
  274. python_ddd_framework/realtime/diagnostics.py +37 -0
  275. python_ddd_framework/realtime/messages.py +113 -0
  276. python_ddd_framework/redis/__init__.py +6 -0
  277. python_ddd_framework/redis/distributed_lock.py +212 -0
  278. python_ddd_framework/redis/lease_lock.py +36 -0
  279. python_ddd_framework/redis/module.py +50 -0
  280. python_ddd_framework/redis/notification_runtime.py +182 -0
  281. python_ddd_framework/redis/notifications.py +25 -0
  282. python_ddd_framework/redis/options.py +35 -0
  283. python_ddd_framework/redis/runtime.py +116 -0
  284. python_ddd_framework/services/__init__.py +41 -0
  285. python_ddd_framework/services/application_bindings.py +221 -0
  286. python_ddd_framework/services/arbitration.py +381 -0
  287. python_ddd_framework/services/binding.py +102 -0
  288. python_ddd_framework/services/contribution.py +438 -0
  289. python_ddd_framework/services/convention.py +301 -0
  290. python_ddd_framework/services/convention_contracts.py +101 -0
  291. python_ddd_framework/services/exposure.py +23 -0
  292. python_ddd_framework/services/fixed_lifetime.py +52 -0
  293. python_ddd_framework/services/framework_provider.py +178 -0
  294. python_ddd_framework/services/native_graph.py +201 -0
  295. python_ddd_framework/services/provider.py +204 -0
  296. python_ddd_framework/services/registration.py +101 -0
  297. python_ddd_framework/services/repository.py +38 -0
  298. python_ddd_framework/services/runtime.py +302 -0
  299. python_ddd_framework/settings/__init__.py +30 -0
  300. python_ddd_framework/settings/application.py +95 -0
  301. python_ddd_framework/settings/binding.py +33 -0
  302. python_ddd_framework/settings/catalog.py +121 -0
  303. python_ddd_framework/settings/changes.py +14 -0
  304. python_ddd_framework/settings/contracts.py +115 -0
  305. python_ddd_framework/settings/definition_discovery.py +71 -0
  306. python_ddd_framework/settings/definitions.py +41 -0
  307. python_ddd_framework/settings/errors.py +22 -0
  308. python_ddd_framework/settings/handlers.py +43 -0
  309. python_ddd_framework/settings/management.py +57 -0
  310. python_ddd_framework/settings/manager.py +89 -0
  311. python_ddd_framework/settings/module.py +10 -0
  312. python_ddd_framework/settings/notifications.py +39 -0
  313. python_ddd_framework/settings/permissions.py +14 -0
  314. python_ddd_framework/settings/provider.py +74 -0
  315. python_ddd_framework/settings/refresh.py +140 -0
  316. python_ddd_framework/settings/refresh_module.py +45 -0
  317. python_ddd_framework/settings/sqlalchemy/__init__.py +5 -0
  318. python_ddd_framework/settings/sqlalchemy/migrations/0001_settings.py +31 -0
  319. python_ddd_framework/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
  320. python_ddd_framework/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
  321. python_ddd_framework/settings/sqlalchemy/migrations/__init__.py +1 -0
  322. python_ddd_framework/settings/sqlalchemy/models.py +36 -0
  323. python_ddd_framework/settings/sqlalchemy/module.py +36 -0
  324. python_ddd_framework/settings/sqlalchemy/store.py +86 -0
  325. python_ddd_framework/settings/store.py +16 -0
  326. python_ddd_framework/settings/values.py +39 -0
  327. python_ddd_framework/sqlalchemy/__init__.py +54 -0
  328. python_ddd_framework/sqlalchemy/alembic_runtime/__init__.py +1 -0
  329. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +33 -0
  330. python_ddd_framework/sqlalchemy/alembic_runtime/script.py.mako +14 -0
  331. python_ddd_framework/sqlalchemy/auditing.py +48 -0
  332. python_ddd_framework/sqlalchemy/errors.py +85 -0
  333. python_ddd_framework/sqlalchemy/metadata.py +576 -0
  334. python_ddd_framework/sqlalchemy/migration.py +378 -0
  335. python_ddd_framework/sqlalchemy/migration_options.py +56 -0
  336. python_ddd_framework/sqlalchemy/module.py +51 -0
  337. python_ddd_framework/sqlalchemy/module_migration.py +151 -0
  338. python_ddd_framework/sqlalchemy/options.py +66 -0
  339. python_ddd_framework/sqlalchemy/repository.py +33 -0
  340. python_ddd_framework/sqlalchemy/runtime.py +85 -0
  341. python_ddd_framework/sqlalchemy/session_provider.py +45 -0
  342. python_ddd_framework/sqlalchemy/unit_of_work.py +97 -0
  343. python_ddd_framework/testing/__init__.py +5 -0
  344. python_ddd_framework/testing/runtime.py +112 -0
  345. python_ddd_framework/unit_of_work/__init__.py +14 -0
  346. python_ddd_framework/unit_of_work/contracts.py +223 -0
  347. python_ddd_framework/unit_of_work/errors.py +27 -0
  348. python_ddd_framework/unit_of_work/manager.py +210 -0
  349. python_ddd_framework/unit_of_work/options.py +92 -0
  350. python_ddd_framework-0.3.1.dist-info/METADATA +379 -0
  351. python_ddd_framework-0.3.1.dist-info/RECORD +355 -0
  352. python_ddd_framework-0.3.1.dist-info/WHEEL +4 -0
  353. python_ddd_framework-0.3.1.dist-info/entry_points.txt +9 -0
  354. python_ddd_framework-0.3.1.dist-info/licenses/LICENSE +7 -0
  355. python_ddd_framework-0.3.1.dist-info/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
@@ -0,0 +1,219 @@
1
+ """Application-owned 任务编排;目录提供任务事实,执行 owner 各自管理本地或子进程资源。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections import defaultdict
7
+ from collections.abc import Mapping
8
+
9
+ from dishka import AsyncContainer
10
+
11
+ from ..application.build_spec import _ApplicationBuildSpec
12
+ from ..application_services.invocation import _InvocationRuntime
13
+ from ..background_execution import (
14
+ BackgroundExecutionMode,
15
+ BackgroundJobOptions,
16
+ BackgroundTaskOptions,
17
+ BackgroundTaskSnapshot,
18
+ )
19
+ from ..background_execution.lifecycle import _BackgroundRuntimeFailure, _TaskExecution
20
+ from ..background_execution.local import _LocalTask
21
+ from ..background_execution.locks import _check_previous_owners
22
+ from ..background_execution.processes import _ProcessSlot
23
+ from ..background_jobs import BackgroundJobCatalog
24
+ from ..invocation import BackgroundSystemCaller
25
+ from .catalog import BackgroundWorkerCatalog
26
+ from .contracts import BackgroundExecutionOptions, BackgroundExecutionProfile
27
+ from .execution import _WorkerExecution
28
+
29
+
30
+ class _BackgroundCoordinator:
31
+ def __init__(
32
+ self,
33
+ *,
34
+ worker_catalog: BackgroundWorkerCatalog,
35
+ job_catalog: BackgroundJobCatalog,
36
+ options: BackgroundExecutionOptions,
37
+ invocation_runtime: _InvocationRuntime,
38
+ build_spec: _ApplicationBuildSpec,
39
+ ) -> None:
40
+ self._worker_catalog = worker_catalog
41
+ self._job_catalog = job_catalog
42
+ self._options = options
43
+ self._invocation_runtime = invocation_runtime
44
+ self._spec = build_spec
45
+ self._caller = BackgroundSystemCaller()
46
+ self._container: AsyncContainer | None = None
47
+ self._started = False
48
+ self._prepared = False
49
+ self._child = False
50
+ self._database_only = False
51
+ self._failures: asyncio.Queue[_BackgroundRuntimeFailure] = asyncio.Queue()
52
+ self._tasks: dict[str, _LocalTask] = {}
53
+ self._slots: dict[str, _ProcessSlot] = {}
54
+ self._task_slots: dict[str, _ProcessSlot] = {}
55
+ self._register_tasks(
56
+ "worker",
57
+ {item.name: item.enabled for item in worker_catalog.definitions},
58
+ options.workers,
59
+ )
60
+ self._register_tasks(
61
+ "job", {item.name: True for item in job_catalog.definitions}, options.jobs
62
+ )
63
+ self._local = {
64
+ key
65
+ for key, task in self._tasks.items()
66
+ if task.options.mode is BackgroundExecutionMode.HOST
67
+ }
68
+ self._initial = {key for key in self._local if self._tasks[key].options.enabled}
69
+
70
+ def _register_tasks(
71
+ self,
72
+ kind: str,
73
+ declarations: Mapping[str, bool],
74
+ configured: Mapping[str, BackgroundJobOptions],
75
+ ) -> None:
76
+ unknown = set(configured) - declarations.keys()
77
+ if unknown:
78
+ raise ValueError(f"Unknown background {kind} configuration: {sorted(unknown)}")
79
+ for name, admitted in sorted(declarations.items()):
80
+ key = f"{kind}:{name}"
81
+ configured_options = configured.get(name, BackgroundJobOptions())
82
+ enabled = (
83
+ configured_options.enabled
84
+ if isinstance(configured_options, BackgroundTaskOptions)
85
+ else True
86
+ )
87
+ options = BackgroundTaskOptions(
88
+ mode=configured_options.mode, enabled=admitted and enabled
89
+ )
90
+ self._tasks[key] = _LocalTask(
91
+ key,
92
+ options,
93
+ self._options.graceful_shutdown_timeout.total_seconds(),
94
+ self._failures.put_nowait,
95
+ )
96
+
97
+ @property
98
+ def profile(self) -> BackgroundExecutionProfile:
99
+ return BackgroundExecutionProfile.DISABLED if self._database_only else self._options.profile
100
+
101
+ def disable_for_database(self) -> None:
102
+ if self._started or self._prepared:
103
+ raise RuntimeError("Database execution must be selected before initialization")
104
+ self._database_only = True
105
+
106
+ @property
107
+ def system_caller(self) -> BackgroundSystemCaller:
108
+ return self._caller
109
+
110
+ @property
111
+ def has_runtime_failure_source(self) -> bool:
112
+ return any(
113
+ task.execution is not None and task.execution.wait_for_failure is not None
114
+ for key, task in self._tasks.items()
115
+ if key in self._local
116
+ )
117
+
118
+ @property
119
+ def local_job_names(self) -> tuple[str, ...]:
120
+ if self.profile is BackgroundExecutionProfile.DISABLED:
121
+ return ()
122
+ return tuple(
123
+ key.removeprefix("job:")
124
+ for key in self._local
125
+ if key.startswith("job:") and self._tasks[key].options.enabled
126
+ )
127
+
128
+ def select_child(
129
+ self, tasks: dict[str, BackgroundTaskOptions], initial: tuple[str, ...]
130
+ ) -> None:
131
+ if self._prepared or self._started:
132
+ raise RuntimeError("Child task assignment is frozen before startup")
133
+ for key, options in tasks.items():
134
+ if key not in self._tasks or self._tasks[key].options != options:
135
+ raise RuntimeError("Child task catalog/configuration differs from parent")
136
+ self._child = True
137
+ self._local = set(tasks)
138
+ self._initial = set(initial)
139
+
140
+ def prepare(self) -> None:
141
+ if self._prepared:
142
+ raise RuntimeError("Background coordinator is already prepared")
143
+ self._prepared = True
144
+ self._worker_catalog._assert_unchanged()
145
+ self._job_catalog._assert_unchanged()
146
+ if self._child or self.profile is BackgroundExecutionProfile.DISABLED:
147
+ return
148
+ groups: dict[str, dict[str, BackgroundTaskOptions]] = defaultdict(dict)
149
+ for key, task in self._tasks.items():
150
+ mode = task.options.mode
151
+ if mode is not BackgroundExecutionMode.HOST:
152
+ position = "shared" if mode is BackgroundExecutionMode.SHARED else key
153
+ groups[position][key] = task.options
154
+ if groups:
155
+ _check_previous_owners(self._spec.project, self._spec.environment)
156
+ for position, tasks in groups.items():
157
+ slot = _ProcessSlot(position, tasks, self._spec, self._options)
158
+ slot.reserve()
159
+ self._slots[position] = slot
160
+ self._task_slots.update({key: slot for key in tasks})
161
+
162
+ def attach_container(self, container: AsyncContainer) -> None:
163
+ if self._container is not None:
164
+ raise RuntimeError("Background coordinator container is already attached")
165
+ self._container = container
166
+ for definition in self._worker_catalog.definitions:
167
+ self._tasks[f"worker:{definition.name}"].execution = _WorkerExecution(
168
+ definition, container, self._invocation_runtime, self._caller
169
+ ).binding
170
+
171
+ def bind_durable(self, name: str, binding: _TaskExecution) -> None:
172
+ task = self._tasks[f"job:{name}"]
173
+ if self._started or task.execution is not None:
174
+ raise RuntimeError("Durable background task may only be bound once before start")
175
+ task.execution = binding
176
+
177
+ async def start(self) -> None:
178
+ if self._started:
179
+ raise RuntimeError("Background coordinator is already started")
180
+ self._started = True
181
+ self.prepare()
182
+ if self.profile is BackgroundExecutionProfile.DISABLED:
183
+ return
184
+ for key in self._tasks:
185
+ if key in self._initial:
186
+ await self._tasks[key].start()
187
+ for slot in self._slots.values():
188
+ await slot.start()
189
+
190
+ def snapshots(self) -> tuple[BackgroundTaskSnapshot, ...]:
191
+ remote = {item.id: item for slot in self._slots.values() for item in slot.snapshots()}
192
+ return tuple(
193
+ remote.get(key, task.snapshot())
194
+ for key, task in self._tasks.items()
195
+ if not self._child or key in self._local
196
+ )
197
+
198
+ def request(self, key: str, *, start: bool) -> BackgroundTaskSnapshot:
199
+ if not self._started or self.profile is BackgroundExecutionProfile.DISABLED:
200
+ raise ValueError("Background execution is not active")
201
+ if key not in self._tasks or (self._child and key not in self._local):
202
+ raise KeyError(key)
203
+ slot = self._task_slots.get(key)
204
+ if slot is not None:
205
+ return slot.request(key, start)
206
+ return self._tasks[key].request(start)
207
+
208
+ async def stop(self) -> None:
209
+ # 即使启动前的预占或 Module 初始化失败,也必须释放已取得的位置。
210
+ operations = [task.shutdown() for key, task in self._tasks.items() if key in self._local]
211
+ operations.extend(slot.shutdown() for slot in self._slots.values())
212
+ outcomes = await asyncio.gather(*operations, return_exceptions=True)
213
+ self._started = False
214
+ failures = tuple(item for item in outcomes if isinstance(item, BaseException))
215
+ if failures:
216
+ raise BaseExceptionGroup("Background runtime drain failed", failures)
217
+
218
+ async def wait_for_runtime_failure(self) -> _BackgroundRuntimeFailure:
219
+ return await self._failures.get()
@@ -0,0 +1,11 @@
1
+ from .catalog import CacheCatalog
2
+ from .contracts import CacheDefinition, DistributedCache
3
+ from .errors import CacheDefinitionError, DistributedCacheError
4
+
5
+ __all__ = (
6
+ "CacheCatalog",
7
+ "CacheDefinition",
8
+ "CacheDefinitionError",
9
+ "DistributedCache",
10
+ "DistributedCacheError",
11
+ )
@@ -0,0 +1,73 @@
1
+ """Cache metadata 的 Application-local 动态聚合与 immutable view。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Mapping
6
+ from types import MappingProxyType
7
+ from typing import Any
8
+
9
+ from ..modularity import ModuleRegistry
10
+ from .contracts import CacheDefinition
11
+ from .errors import CacheDefinitionError
12
+
13
+
14
+ class CacheCatalog:
15
+ __slots__ = ("_definitions",)
16
+
17
+ def __init__(self, definitions: tuple[CacheDefinition[Any], ...]) -> None:
18
+ by_name: dict[str, CacheDefinition[Any]] = {}
19
+ for definition in definitions:
20
+ if not isinstance(definition, CacheDefinition):
21
+ raise CacheDefinitionError(
22
+ cache=repr(definition),
23
+ reason="catalog entries must be CacheDefinition values",
24
+ )
25
+ if definition.name in by_name:
26
+ raise CacheDefinitionError(
27
+ cache=definition.name,
28
+ reason="cache is declared more than once",
29
+ )
30
+ by_name[definition.name] = definition
31
+ self._definitions: Mapping[str, CacheDefinition[Any]] = MappingProxyType(
32
+ dict(sorted(by_name.items()))
33
+ )
34
+
35
+ @property
36
+ def definitions(self) -> tuple[CacheDefinition[Any], ...]:
37
+ return tuple(self._definitions.values())
38
+
39
+ def __iter__(self) -> Iterator[str]:
40
+ return iter(self._definitions)
41
+
42
+ def get(self, name: str) -> CacheDefinition[Any] | None:
43
+ return self._definitions.get(name)
44
+
45
+ def owns(self, definition: CacheDefinition[Any]) -> bool:
46
+ return self._definitions.get(definition.name) is definition
47
+
48
+
49
+ def _build_cache_catalog(registry: ModuleRegistry) -> CacheCatalog:
50
+ definitions: list[CacheDefinition[Any]] = []
51
+ owners: dict[str, str] = {}
52
+ for descriptor in registry.dependency_order:
53
+ declared = descriptor.module_type.cache_definitions
54
+ if not isinstance(declared, tuple):
55
+ raise CacheDefinitionError(
56
+ cache=str(descriptor.key),
57
+ reason="Module cache_definitions must be an immutable tuple",
58
+ )
59
+ for definition in declared:
60
+ if not isinstance(definition, CacheDefinition):
61
+ raise CacheDefinitionError(
62
+ cache=str(descriptor.key),
63
+ reason="Module cache_definitions contains an invalid item",
64
+ )
65
+ previous = owners.get(definition.name)
66
+ if previous is not None:
67
+ raise CacheDefinitionError(
68
+ cache=definition.name,
69
+ reason=f"cache is already owned by {previous}",
70
+ )
71
+ owners[definition.name] = str(descriptor.key)
72
+ definitions.append(definition)
73
+ return CacheCatalog(tuple(definitions))
@@ -0,0 +1,73 @@
1
+ """强制 TTL 的 typed cache definition 与最小读写契约。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import timedelta
6
+ from typing import Any, Generic, Protocol, TypeVar
7
+
8
+ from pydantic import TypeAdapter
9
+
10
+ from .errors import CacheDefinitionError
11
+
12
+ _T = TypeVar("_T")
13
+
14
+
15
+ class CacheDefinition(Generic[_T]):
16
+ __slots__ = ("_adapter", "_name", "_ttl", "_value_type")
17
+
18
+ def __init__(self, name: str, value_type: type[_T], *, ttl: timedelta) -> None:
19
+ if (
20
+ not isinstance(name, str)
21
+ or not name
22
+ or name != name.strip()
23
+ or name.startswith(":")
24
+ or name.endswith(":")
25
+ or "::" in name
26
+ # ':' 是 Redis key 的结构分隔符,definition name 不得制造跨 cache key 歧义。
27
+ or any(not (item.isalnum() or item in "._-") for item in name)
28
+ ):
29
+ raise CacheDefinitionError(
30
+ cache=name if isinstance(name, str) else repr(name),
31
+ reason="name must be a trimmed cache identifier",
32
+ )
33
+ if not isinstance(value_type, type):
34
+ raise CacheDefinitionError(cache=name, reason="value_type must be a runtime type")
35
+ if not isinstance(ttl, timedelta) or ttl.total_seconds() <= 0:
36
+ raise CacheDefinitionError(cache=name, reason="ttl must be a positive timedelta")
37
+ if ttl.total_seconds() > 2_147_483_647:
38
+ raise CacheDefinitionError(cache=name, reason="ttl exceeds the supported Redis range")
39
+ self._name = name
40
+ self._value_type = value_type
41
+ self._ttl = ttl
42
+ self._adapter: TypeAdapter[_T] = TypeAdapter(value_type)
43
+
44
+ @property
45
+ def name(self) -> str:
46
+ return self._name
47
+
48
+ @property
49
+ def value_type(self) -> type[_T]:
50
+ return self._value_type
51
+
52
+ @property
53
+ def ttl(self) -> timedelta:
54
+ return self._ttl
55
+
56
+ def __repr__(self) -> str:
57
+ return (
58
+ f"CacheDefinition(name={self.name!r}, value_type={self.value_type!r}, ttl={self.ttl!r})"
59
+ )
60
+
61
+ def _serialize(self, value: _T) -> bytes:
62
+ return self._adapter.dump_json(self._adapter.validate_python(value))
63
+
64
+ def _deserialize(self, value: bytes | str) -> _T:
65
+ return self._adapter.validate_json(value)
66
+
67
+
68
+ class DistributedCache(Protocol):
69
+ async def get(self, definition: CacheDefinition[_T], key: str) -> _T | None: ...
70
+
71
+ async def set(self, definition: CacheDefinition[_T], key: str, value: _T) -> None: ...
72
+
73
+ async def remove(self, definition: CacheDefinition[Any], key: str) -> None: ...
@@ -0,0 +1,20 @@
1
+ """Typed distributed cache definition 与 serialization failures。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..errors.base import FrameworkError
6
+
7
+
8
+ class CacheDefinitionError(FrameworkError):
9
+ def __init__(self, *, cache: str, reason: str) -> None:
10
+ self.cache = cache
11
+ self.reason = reason
12
+ super().__init__(f"Invalid cache {cache!r}: {reason}")
13
+
14
+
15
+ class DistributedCacheError(FrameworkError):
16
+ def __init__(self, *, operation: str, cache: str, error_type: str) -> None:
17
+ self.operation = operation
18
+ self.cache = cache
19
+ self.error_type = error_type
20
+ super().__init__(f"Distributed cache {operation} failed for {cache!r} with {error_type}")
@@ -0,0 +1,105 @@
1
+ """pddd 的轻量 argparse 入口;只有生成命令才加载 Developer Kit。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import os
8
+ import subprocess
9
+ from collections.abc import Sequence
10
+ from enum import Enum
11
+ from pathlib import Path
12
+
13
+ from ..hosting import ENVIRONMENT_VARIABLE as ENVIRONMENT_VARIABLE
14
+ from .errors import CommandError
15
+
16
+
17
+ class _Command(str, Enum):
18
+ NEW = "new"
19
+ ADD = "add"
20
+ DEV = "dev"
21
+ DEV_INIT = "dev-init"
22
+ DB = "db"
23
+ INSPECT = "inspect"
24
+
25
+
26
+ def _parser() -> argparse.ArgumentParser:
27
+ parser = argparse.ArgumentParser(prog="pddd", description="Tools for modular DDD applications")
28
+ commands = parser.add_subparsers(dest="command", title="commands")
29
+ new = commands.add_parser(_Command.NEW.value, help="Create a project from bundled templates")
30
+ new.add_argument("name")
31
+ new.add_argument("--framework-wheel", type=Path, help="Validate with a local framework wheel")
32
+ new.add_argument("--dry-run", action="store_true")
33
+ add = commands.add_parser(
34
+ _Command.ADD.value, help="Add a business module to the current project"
35
+ ).add_subparsers(dest="kind", required=True)
36
+ module = add.add_parser("module", help="Generate and register a business module")
37
+ module.add_argument("name")
38
+ module.add_argument("--dry-run", action="store_true")
39
+ dev = commands.add_parser(_Command.DEV.value, help="Run the development web server")
40
+ dev.add_argument("--host", default="127.0.0.1")
41
+ dev.add_argument("--port", type=int, default=8000)
42
+ for command, description in (
43
+ (_Command.DEV_INIT, "Prepare local development infrastructure"),
44
+ (_Command.DB, "Manage module migrations and seed data"),
45
+ (_Command.INSPECT, "Inspect modules, services and redacted configuration"),
46
+ ):
47
+ action = commands.add_parser(command.value, help=description, description=description)
48
+ action.add_argument(
49
+ "--environment",
50
+ default=os.environ.get(ENVIRONMENT_VARIABLE, "development"),
51
+ help="Host environment (default: %(default)s)",
52
+ )
53
+ if command is _Command.DB:
54
+ action.add_argument("operation", choices=("revision", "upgrade", "status", "seed"))
55
+ action.add_argument("--module", required=True)
56
+ return parser
57
+
58
+
59
+ def main(argv: Sequence[str] | None = None) -> int:
60
+ parser = _parser()
61
+ args = parser.parse_args(argv)
62
+ if args.command is None:
63
+ parser.print_help()
64
+ return 0
65
+ try:
66
+ return _execute(args)
67
+ except CommandError as error:
68
+ parser.exit(2, f"pddd: {error}\n")
69
+ except subprocess.CalledProcessError as error:
70
+ parser.exit(
71
+ error.returncode, "pddd: external command failed; diagnostic files were retained\n"
72
+ )
73
+
74
+
75
+ def _execute(args: argparse.Namespace) -> int:
76
+ command = _Command(args.command)
77
+ if command in (_Command.NEW, _Command.ADD):
78
+ try:
79
+ from ..developer_kit.generation import add_module, new_project
80
+ except ModuleNotFoundError as error:
81
+ if error.name not in {"cookiecutter", "libcst"}:
82
+ raise
83
+ raise CommandError(
84
+ "Install python-ddd-framework[developer-kit] to use generation commands"
85
+ ) from None
86
+ if command is _Command.NEW:
87
+ new_project(args.name, args.framework_wheel, args.dry_run)
88
+ else:
89
+ add_module(args.name, args.dry_run)
90
+ else:
91
+ from .runtime import database, development_server
92
+
93
+ if command is _Command.DEV:
94
+ development_server(args.host, args.port)
95
+ elif command is _Command.DEV_INIT:
96
+ from .development import dev_init
97
+
98
+ asyncio.run(dev_init(args.environment))
99
+ elif command is _Command.INSPECT:
100
+ from .inspection import inspect_application
101
+
102
+ asyncio.run(inspect_application(args.environment))
103
+ else:
104
+ asyncio.run(database(args.operation, args.environment, args.module))
105
+ return 0
@@ -0,0 +1,85 @@
1
+ """开发依赖的 Compose 接线;配置只来自当前 Host 构建出的快照。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+
8
+ from ..application import Application
9
+ from .errors import CommandError
10
+ from .project import Project
11
+
12
+
13
+ async def dev_init(environment: str) -> None:
14
+ if environment != "development":
15
+ raise CommandError("dev-init is only available in the development environment")
16
+ project = Project.current()
17
+ application = project.host("application").load()(environment=environment)
18
+ if not isinstance(application, Application):
19
+ raise CommandError("The application entry point must return Application")
20
+ try:
21
+ subprocess.run(
22
+ [
23
+ "docker",
24
+ "compose",
25
+ "-f",
26
+ str(project.root / "compose.dev.yaml"),
27
+ "up",
28
+ "-d",
29
+ "--wait",
30
+ ],
31
+ cwd=project.root,
32
+ env=_compose_environment(application),
33
+ check=True,
34
+ )
35
+ finally:
36
+ # build 只组合,关闭仍交给 Application;解析和子进程失败也不得遗留 APP owner。
37
+ await application.stop()
38
+
39
+
40
+ def _compose_environment(application: Application) -> dict[str, str]:
41
+ from sqlalchemy.engine import make_url
42
+ from sqlalchemy.exc import ArgumentError
43
+
44
+ from ..redis import RedisOptions
45
+ from ..sqlalchemy import ConnectionStringsOptions
46
+
47
+ try:
48
+ postgres = make_url(
49
+ application.options.get(ConnectionStringsOptions).default.get_secret_value()
50
+ )
51
+ redis = make_url(application.options.get(RedisOptions).url.get_secret_value())
52
+ images = {
53
+ "POSTGRES_IMAGE": application.configuration.get_path(
54
+ "development.compose.postgres_image"
55
+ ),
56
+ "REDIS_IMAGE": application.configuration.get_path("development.compose.redis_image"),
57
+ }
58
+ if any(not isinstance(value, str) or not value.strip() for value in images.values()):
59
+ raise ValueError("Compose images require non-empty strings")
60
+ # 模板只提供本机 TCP 依赖,拒绝无法兑现的远端、TLS 或查询参数覆盖。
61
+ for url in (postgres, redis):
62
+ if url.host not in ("127.0.0.1", "localhost") or not url.port or url.query:
63
+ raise ValueError("Compose requires local URLs with explicit ports and no query")
64
+ if not postgres.username or not postgres.password or not postgres.database:
65
+ raise ValueError("PostgreSQL URL requires credentials and database")
66
+ if redis.drivername != "redis" or redis.username or redis.password:
67
+ raise ValueError("Development Redis requires a plain local connection")
68
+ except (ArgumentError, KeyError, TypeError, ValueError):
69
+ # 原生 parser 异常可能包含完整 URL,CLI 只公开需要检查的配置位置。
70
+ raise CommandError(
71
+ "Invalid development Compose configuration; check connection_strings.default, "
72
+ "redis.url and development.compose (local URLs require explicit ports)"
73
+ ) from None
74
+ environment = dict(os.environ)
75
+ environment.pop("COMPOSE_ENV_FILES", None)
76
+ environment.update(
77
+ COMPOSE_DISABLE_ENV_FILE="1",
78
+ POSTGRES_DB=postgres.database,
79
+ POSTGRES_USER=postgres.username,
80
+ POSTGRES_PASSWORD=postgres.password,
81
+ POSTGRES_PORT=str(postgres.port),
82
+ REDIS_PORT=str(redis.port),
83
+ )
84
+ environment.update({key: str(value) for key, value in images.items()})
85
+ return environment
@@ -0,0 +1,5 @@
1
+ """CLI 可直接展示的操作诊断;依赖命令的退出码由调用方保留。"""
2
+
3
+
4
+ class CommandError(Exception):
5
+ pass