python-ddd-framework 0.3.2__py3-none-any.whl → 0.4.0__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 (334) hide show
  1. python_ddd_framework/__init__.py +139 -21
  2. python_ddd_framework/application/builder.py +23 -78
  3. python_ddd_framework/application/composition.py +4 -22
  4. python_ddd_framework/application/runtime.py +96 -291
  5. python_ddd_framework/application/state.py +7 -75
  6. python_ddd_framework/application_services/__init__.py +5 -21
  7. python_ddd_framework/{services/application_bindings.py → application_services/bindings.py} +52 -9
  8. python_ddd_framework/application_services/catalog.py +36 -288
  9. python_ddd_framework/application_services/contracts.py +14 -13
  10. python_ddd_framework/application_services/dispatcher.py +19 -269
  11. python_ddd_framework/application_services/invocation.py +126 -7
  12. python_ddd_framework/application_services/module.py +168 -0
  13. python_ddd_framework/application_services/pagination.py +21 -0
  14. python_ddd_framework/auditing/control.py +1 -1
  15. python_ddd_framework/auditing/module.py +15 -0
  16. python_ddd_framework/authorization/__init__.py +2 -0
  17. python_ddd_framework/authorization/contracts.py +7 -2
  18. python_ddd_framework/authorization/definitions.py +5 -0
  19. python_ddd_framework/authorization/module.py +40 -0
  20. python_ddd_framework/authorization/options.py +7 -0
  21. python_ddd_framework/background_execution/child.py +13 -3
  22. python_ddd_framework/background_execution/declarations.py +45 -0
  23. python_ddd_framework/background_execution/lifecycle.py +12 -0
  24. python_ddd_framework/background_execution/module.py +107 -1
  25. python_ddd_framework/background_jobs/catalog.py +59 -42
  26. python_ddd_framework/background_jobs/contracts.py +16 -19
  27. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +4 -0
  28. python_ddd_framework/background_jobs/pgqueuer/module.py +2 -1
  29. python_ddd_framework/background_jobs/pgqueuer/runtime.py +7 -7
  30. python_ddd_framework/background_workers/catalog.py +23 -36
  31. python_ddd_framework/background_workers/contracts.py +3 -0
  32. python_ddd_framework/background_workers/execution.py +1 -1
  33. python_ddd_framework/background_workers/runtime.py +38 -1
  34. python_ddd_framework/caching/__init__.py +7 -6
  35. python_ddd_framework/caching/contracts.py +65 -67
  36. python_ddd_framework/caching/errors.py +0 -7
  37. python_ddd_framework/caching/module.py +18 -0
  38. python_ddd_framework/caching/naming.py +29 -0
  39. python_ddd_framework/caching/options.py +38 -0
  40. python_ddd_framework/caching/unit_of_work.py +46 -0
  41. python_ddd_framework/cli/__init__.py +31 -6
  42. python_ddd_framework/cli/project.py +68 -6
  43. python_ddd_framework/cli/runtime.py +1 -1
  44. python_ddd_framework/developer_kit/generation.py +75 -22
  45. python_ddd_framework/developer_kit/project_metadata.py +2 -4
  46. python_ddd_framework/developer_kit/templates/basic/cookiecutter.json +4 -0
  47. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +45 -0
  48. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/__init__.py.jinja +0 -0
  49. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/__init__.py.jinja +0 -0
  50. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/conversion_service.py.jinja +7 -0
  51. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/module.py.jinja +38 -0
  52. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/__init__.py.jinja +0 -0
  53. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/default_conversion_service.py.jinja +10 -0
  54. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
  55. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +41 -49
  56. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/__init__.py.jinja +1 -0
  57. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +1 -0
  58. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +45 -0
  59. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +8 -0
  60. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +1 -0
  61. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +35 -0
  62. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +5 -0
  63. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +15 -0
  64. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/__init__.py.jinja +1 -0
  65. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +28 -0
  66. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +26 -0
  67. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/__init__.py.jinja +1 -0
  68. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +9 -0
  69. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +9 -0
  70. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/__init__.py.jinja +1 -0
  71. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +23 -0
  72. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/__init__.py.jinja +1 -0
  73. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{integration.py.jinja → hosted_services/order_integration_service.py.jinja} +8 -2
  74. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +16 -0
  75. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/__init__.py.jinja +1 -0
  76. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +16 -0
  77. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/__init__.py.jinja +1 -0
  78. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +17 -0
  79. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +105 -10
  80. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/__init__.py.jinja +1 -0
  81. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{options.py.jinja → options/order_options.py.jinja} +1 -1
  82. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/__init__.py.jinja +1 -0
  83. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +65 -0
  84. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +35 -0
  85. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +41 -0
  86. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +1 -0
  87. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +17 -0
  88. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/__init__.py.jinja +1 -0
  89. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +6 -0
  90. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +7 -0
  91. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/__init__.py.jinja +1 -0
  92. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +7 -0
  93. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +39 -2
  94. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/__init__.py.jinja +1 -0
  95. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +13 -0
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +10 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +12 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/__init__.py.jinja +1 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +9 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +12 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/__init__.py.jinja +1 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{orders.py.jinja → entities/order.py.jinja} +10 -18
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/__init__.py.jinja +1 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +13 -0
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +42 -3
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/__init__.py.jinja +1 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +17 -0
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/__init__.py.jinja +1 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +22 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/__init__.py.jinja +1 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +23 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/__init__.py.jinja +1 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{settings.py.jinja → settings/approval_settings.py.jinja} +3 -2
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/__init__.py.jinja +1 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +10 -0
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/__init__.py.jinja +1 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +3 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/__init__.py.jinja +1 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +6 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/__init__.py.jinja +1 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +11 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/__init__.py.jinja +1 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +26 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +13 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +40 -1
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/__init__.py.jinja +1 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/{permissions.py.jinja → permissions/order_permission_provider.py.jinja} +6 -2
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +7 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/__init__.py.jinja +1 -0
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +18 -0
  131. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/__init__.py.jinja +1 -0
  132. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +15 -0
  133. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/__init__.py.jinja +1 -0
  134. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +5 -0
  135. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +85 -7
  136. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/__init__.py.jinja +1 -0
  137. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/{files.py.jinja → routers/order_files.py.jinja} +23 -20
  138. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/__init__.py.jinja +1 -0
  139. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +44 -0
  140. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -1
  141. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +1 -7
  142. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +7 -0
  143. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/{orders.py.jinja → order_model.py.jinja} +7 -3
  144. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +42 -5
  145. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +79 -0
  146. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/__init__.py.jinja +1 -0
  147. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +4 -3
  148. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +1 -1
  149. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +16 -3
  150. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +29 -22
  151. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{app.development.yaml.jinja → backend/app.development.yaml.jinja} +2 -0
  152. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{pyproject.toml.jinja → backend/pyproject.toml.jinja} +1 -1
  153. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/main.py.jinja +2 -0
  154. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/module.py.jinja +40 -3
  155. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +40 -10
  156. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +134 -23
  157. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/scripts/.gitkeep +0 -0
  158. python_ddd_framework/distributed_lock/contracts.py +4 -1
  159. python_ddd_framework/domain/__init__.py +2 -0
  160. python_ddd_framework/domain/services.py +11 -0
  161. python_ddd_framework/errors/business.py +10 -1
  162. python_ddd_framework/errors/lifecycle.py +3 -31
  163. python_ddd_framework/events/__init__.py +5 -2
  164. python_ddd_framework/events/aggregate.py +3 -3
  165. python_ddd_framework/events/catalog.py +12 -11
  166. python_ddd_framework/events/contracts.py +6 -1
  167. python_ddd_framework/events/contribution.py +1 -52
  168. python_ddd_framework/events/entities.py +23 -0
  169. python_ddd_framework/events/module.py +97 -0
  170. python_ddd_framework/events/runtime.py +80 -29
  171. python_ddd_framework/events/subscription.py +36 -0
  172. python_ddd_framework/events/unit_of_work.py +25 -0
  173. python_ddd_framework/extensions/__init__.py +22 -0
  174. python_ddd_framework/extensions/catalog.py +198 -0
  175. python_ddd_framework/extensions/contracts.py +85 -0
  176. python_ddd_framework/extensions/models.py +142 -0
  177. python_ddd_framework/extensions/module.py +21 -0
  178. python_ddd_framework/fastapi/__init__.py +2 -0
  179. python_ddd_framework/fastapi/action.py +1 -2
  180. python_ddd_framework/fastapi/adapter.py +16 -0
  181. python_ddd_framework/fastapi/application_services.py +43 -25
  182. python_ddd_framework/fastapi/background.py +2 -1
  183. python_ddd_framework/fastapi/contracts.py +26 -8
  184. python_ddd_framework/fastapi/health.py +1 -1
  185. python_ddd_framework/fastapi/module.py +14 -0
  186. python_ddd_framework/fastapi/parameters.py +3 -18
  187. python_ddd_framework/fastapi/realtime/connection.py +10 -7
  188. python_ddd_framework/fastapi/realtime/module.py +3 -0
  189. python_ddd_framework/fastapi/realtime/runtime.py +10 -24
  190. python_ddd_framework/fastapi/request_context.py +2 -1
  191. python_ddd_framework/fastapi/routing.py +2 -1
  192. python_ddd_framework/fastapi/server.py +8 -0
  193. python_ddd_framework/fastapi/settings.py +2 -1
  194. python_ddd_framework/hosted_services/bridge.py +20 -1
  195. python_ddd_framework/hosted_services/catalog.py +6 -16
  196. python_ddd_framework/hosted_services/contracts.py +4 -0
  197. python_ddd_framework/hosted_services/declarations.py +15 -0
  198. python_ddd_framework/hosted_services/module.py +69 -0
  199. python_ddd_framework/hosted_services/runtime.py +30 -1
  200. python_ddd_framework/identity/__init__.py +24 -0
  201. python_ddd_framework/identity/application.py +38 -90
  202. python_ddd_framework/identity/contracts.py +181 -16
  203. python_ddd_framework/identity/http_api.py +79 -2
  204. python_ddd_framework/identity/management.py +166 -0
  205. python_ddd_framework/identity/module.py +12 -4
  206. python_ddd_framework/identity/services.py +29 -16
  207. python_ddd_framework/identity/sqlalchemy/migrations/0003_extra_properties.py +26 -0
  208. python_ddd_framework/identity/sqlalchemy/migrations/0004_concurrency_version.py +29 -0
  209. python_ddd_framework/identity/sqlalchemy/models.py +21 -2
  210. python_ddd_framework/identity/sqlalchemy/module.py +11 -2
  211. python_ddd_framework/identity/sqlalchemy/repository.py +458 -0
  212. python_ddd_framework/identity/sqlalchemy/session_security.py +88 -0
  213. python_ddd_framework/identity/sqlalchemy/stores.py +69 -290
  214. python_ddd_framework/identity/tokens.py +2 -5
  215. python_ddd_framework/{application_services/validation.py → invocation/arguments.py} +8 -6
  216. python_ddd_framework/invocation/contribution.py +61 -0
  217. python_ddd_framework/invocation/dispatcher.py +291 -0
  218. python_ddd_framework/invocation/entries.py +2 -25
  219. python_ddd_framework/invocation/entrypoints.py +126 -0
  220. python_ddd_framework/{application_services → invocation}/execution.py +8 -5
  221. python_ddd_framework/invocation/function_runtime.py +3 -1
  222. python_ddd_framework/invocation/interception.py +113 -42
  223. python_ddd_framework/{application_services → invocation}/interceptors.py +2 -2
  224. python_ddd_framework/invocation/managed_proxy.py +116 -0
  225. python_ddd_framework/invocation/managed_services.py +250 -0
  226. python_ddd_framework/invocation/methods.py +345 -0
  227. python_ddd_framework/invocation/module.py +113 -0
  228. python_ddd_framework/{application_services → invocation}/policies.py +8 -1
  229. python_ddd_framework/invocation/validation.py +25 -0
  230. python_ddd_framework/lifecycle/__init__.py +2 -0
  231. python_ddd_framework/lifecycle/composition.py +5 -2
  232. python_ddd_framework/lifecycle/participants.py +33 -0
  233. python_ddd_framework/messaging/__init__.py +25 -0
  234. python_ddd_framework/messaging/channel.py +205 -0
  235. python_ddd_framework/messaging/contracts.py +69 -0
  236. python_ddd_framework/messaging/module.py +101 -0
  237. python_ddd_framework/messaging/options.py +13 -0
  238. python_ddd_framework/messaging/receipt.py +27 -0
  239. python_ddd_framework/messaging/runtime.py +124 -0
  240. python_ddd_framework/modularity/__init__.py +11 -1
  241. python_ddd_framework/modularity/contracts.py +47 -16
  242. python_ddd_framework/modularity/discovery.py +21 -2
  243. python_ddd_framework/modularity/graph.py +45 -1
  244. python_ddd_framework/modularity/registry.py +83 -3
  245. python_ddd_framework/notifications/__init__.py +2 -0
  246. python_ddd_framework/notifications/catalog.py +3 -2
  247. python_ddd_framework/notifications/declarations.py +18 -0
  248. python_ddd_framework/notifications/module.py +21 -0
  249. python_ddd_framework/observability/logging.py +3 -0
  250. python_ddd_framework/observability/tracing.py +3 -0
  251. python_ddd_framework/options/contribution.py +12 -0
  252. python_ddd_framework/options/registry.py +44 -11
  253. python_ddd_framework/realtime/__init__.py +2 -1
  254. python_ddd_framework/realtime/contracts.py +3 -7
  255. python_ddd_framework/realtime/messages.py +26 -1
  256. python_ddd_framework/redis/cache.py +243 -0
  257. python_ddd_framework/redis/cache_scripts.py +40 -0
  258. python_ddd_framework/redis/distributed_lock.py +10 -2
  259. python_ddd_framework/redis/module.py +13 -4
  260. python_ddd_framework/redis/notifications.py +5 -2
  261. python_ddd_framework/redis/runtime.py +1 -79
  262. python_ddd_framework/seeding/__init__.py +13 -0
  263. python_ddd_framework/seeding/contracts.py +45 -0
  264. python_ddd_framework/seeding/module.py +73 -0
  265. python_ddd_framework/seeding/runtime.py +63 -0
  266. python_ddd_framework/services/arbitration.py +23 -0
  267. python_ddd_framework/services/binding.py +2 -3
  268. python_ddd_framework/services/composition.py +108 -0
  269. python_ddd_framework/services/contribution.py +44 -54
  270. python_ddd_framework/services/convention.py +53 -181
  271. python_ddd_framework/services/fixed_lifetime.py +12 -15
  272. python_ddd_framework/services/framework_provider.py +3 -158
  273. python_ddd_framework/services/native_graph.py +35 -60
  274. python_ddd_framework/services/provider.py +59 -4
  275. python_ddd_framework/services/repository.py +6 -0
  276. python_ddd_framework/services/runtime.py +129 -215
  277. python_ddd_framework/settings/catalog.py +5 -4
  278. python_ddd_framework/settings/declarations.py +18 -0
  279. python_ddd_framework/settings/definitions.py +5 -0
  280. python_ddd_framework/settings/manager.py +1 -1
  281. python_ddd_framework/settings/module.py +50 -1
  282. python_ddd_framework/settings/notifications.py +4 -2
  283. python_ddd_framework/settings/refresh_module.py +9 -2
  284. python_ddd_framework/settings/sqlalchemy/module.py +2 -1
  285. python_ddd_framework/sqlalchemy/__init__.py +12 -0
  286. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +1 -0
  287. python_ddd_framework/sqlalchemy/auditing.py +7 -1
  288. python_ddd_framework/sqlalchemy/extensions.py +186 -0
  289. python_ddd_framework/sqlalchemy/metadata.py +110 -6
  290. python_ddd_framework/sqlalchemy/migration.py +61 -8
  291. python_ddd_framework/sqlalchemy/migration_operations.py +86 -0
  292. python_ddd_framework/sqlalchemy/module.py +14 -11
  293. python_ddd_framework/sqlalchemy/module_migration.py +71 -3
  294. python_ddd_framework/sqlalchemy/repository.py +217 -13
  295. python_ddd_framework/sqlalchemy/session_provider.py +14 -1
  296. python_ddd_framework/testing/runtime.py +5 -4
  297. python_ddd_framework/unit_of_work/__init__.py +2 -1
  298. python_ddd_framework/unit_of_work/contracts.py +6 -7
  299. python_ddd_framework/unit_of_work/errors.py +11 -0
  300. python_ddd_framework/unit_of_work/lifecycle.py +19 -0
  301. python_ddd_framework/unit_of_work/manager.py +86 -32
  302. python_ddd_framework/unit_of_work/module.py +61 -0
  303. python_ddd_framework/unit_of_work/options.py +7 -1
  304. python_ddd_framework/validation/__init__.py +1 -0
  305. python_ddd_framework/validation/models.py +37 -0
  306. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/METADATA +139 -44
  307. python_ddd_framework-0.4.0.dist-info/RECORD +488 -0
  308. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/WHEEL +1 -1
  309. python_ddd_framework/application_services/seeding.py +0 -72
  310. python_ddd_framework/caching/catalog.py +0 -73
  311. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +0 -10
  312. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +0 -24
  313. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +0 -83
  314. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +0 -84
  315. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +0 -35
  316. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +0 -14
  317. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +0 -19
  318. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +0 -24
  319. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +0 -36
  320. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +0 -49
  321. python_ddd_framework-0.3.2.dist-info/RECORD +0 -355
  322. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.dockerignore → backend/.dockerignore} +0 -0
  323. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.gitignore → backend/.gitignore} +0 -0
  324. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.python-version → backend/.python-version} +0 -0
  325. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{Dockerfile → backend/Dockerfile} +0 -0
  326. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.dev.yaml.jinja → backend/compose.dev.yaml.jinja} +0 -0
  327. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.production.yaml.jinja → backend/compose.production.yaml.jinja} +0 -0
  328. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/__init__.py.jinja +0 -0
  329. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/conftest.py.jinja +0 -0
  330. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/host/test_http.py.jinja +0 -0
  331. /python_ddd_framework/{application_services → invocation}/signature.py +0 -0
  332. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/entry_points.txt +0 -0
  333. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/LICENSE +0 -0
  334. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -1,73 +1,71 @@
1
- """强制 TTL 的 typed cache definition 与最小读写契约。"""
1
+ """注入时确定值类型的缓存契约。"""
2
2
 
3
- from __future__ import annotations
3
+ from collections.abc import Awaitable, Callable, Iterable
4
+ from typing import Protocol, TypeVar
4
5
 
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
6
+ from .options import DistributedCacheEntryOptions
11
7
 
12
8
  _T = TypeVar("_T")
13
9
 
14
10
 
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: ...
11
+ class DistributedCache(Protocol[_T]):
12
+ async def get(
13
+ self, key: str, *, hide_errors: bool | None = None, consider_uow: bool = False
14
+ ) -> _T | None: ...
15
+
16
+ async def get_many(
17
+ self, keys: Iterable[str], *, hide_errors: bool | None = None, consider_uow: bool = False
18
+ ) -> list[tuple[str, _T | None]]: ...
19
+
20
+ async def get_or_add(
21
+ self,
22
+ key: str,
23
+ factory: Callable[[], Awaitable[_T | None]],
24
+ *,
25
+ options_factory: Callable[[], DistributedCacheEntryOptions] | None = None,
26
+ hide_errors: bool | None = None,
27
+ consider_uow: bool = False,
28
+ ) -> _T | None: ...
29
+
30
+ async def get_or_add_many(
31
+ self,
32
+ keys: Iterable[str],
33
+ factory: Callable[[list[str]], Awaitable[Iterable[tuple[str, _T]]]],
34
+ *,
35
+ options_factory: Callable[[], DistributedCacheEntryOptions] | None = None,
36
+ hide_errors: bool | None = None,
37
+ consider_uow: bool = False,
38
+ ) -> list[tuple[str, _T | None]]: ...
39
+
40
+ async def set(
41
+ self,
42
+ key: str,
43
+ value: _T,
44
+ *,
45
+ options: DistributedCacheEntryOptions | None = None,
46
+ hide_errors: bool | None = None,
47
+ consider_uow: bool = False,
48
+ ) -> None: ...
49
+
50
+ async def set_many(
51
+ self,
52
+ items: Iterable[tuple[str, _T]],
53
+ *,
54
+ options: DistributedCacheEntryOptions | None = None,
55
+ hide_errors: bool | None = None,
56
+ consider_uow: bool = False,
57
+ ) -> None: ...
58
+
59
+ async def remove(
60
+ self, key: str, *, hide_errors: bool | None = None, consider_uow: bool = False
61
+ ) -> None: ...
62
+
63
+ async def remove_many(
64
+ self, keys: Iterable[str], *, hide_errors: bool | None = None, consider_uow: bool = False
65
+ ) -> None: ...
66
+
67
+ async def refresh(self, key: str, *, hide_errors: bool | None = None) -> None: ...
68
+
69
+ async def refresh_many(
70
+ self, keys: Iterable[str], *, hide_errors: bool | None = None
71
+ ) -> None: ...
@@ -5,13 +5,6 @@ from __future__ import annotations
5
5
  from ..errors.base import FrameworkError
6
6
 
7
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
8
  class DistributedCacheError(FrameworkError):
16
9
  def __init__(self, *, operation: str, cache: str, error_type: str) -> None:
17
10
  self.operation = operation
@@ -0,0 +1,18 @@
1
+ """缓存默认选项与具体 UoW 延迟写生命周期的组装 owner。"""
2
+
3
+ from ..lifecycle import ConfigureContext
4
+ from ..modularity import AppModule
5
+ from ..unit_of_work.lifecycle import UnitOfWorkLifecycleFactory
6
+ from ..unit_of_work.module import UnitOfWorkModule
7
+ from .options import DistributedCacheOptions
8
+ from .unit_of_work import _CacheLifecycleFactory
9
+
10
+
11
+ class CachingModule(AppModule):
12
+ dependencies = (UnitOfWorkModule,)
13
+
14
+ def configure(self, context: ConfigureContext) -> None:
15
+ context.configure.default(DistributedCacheOptions, section="caching")
16
+ context.services.add_singleton(
17
+ UnitOfWorkLifecycleFactory, _CacheLifecycleFactory, multi=True
18
+ )
@@ -0,0 +1,29 @@
1
+ """值类型拥有缓存名称;Provider 复用此元数据,不逐类型注册。"""
2
+
3
+ from collections.abc import Callable
4
+ from typing import TypeVar, get_origin
5
+
6
+ _T = TypeVar("_T")
7
+ _CACHE_NAME = "__distributed_cache_name__"
8
+
9
+
10
+ def cache_name(name: str) -> Callable[[type[_T]], type[_T]]:
11
+ if not name or any(not (character.isalnum() or character in "._-") for character in name):
12
+ raise ValueError("cache name must be a stable identifier")
13
+
14
+ def decorate(value_type: type[_T]) -> type[_T]:
15
+ setattr(value_type, _CACHE_NAME, name)
16
+ return value_type
17
+
18
+ return decorate
19
+
20
+
21
+ def _cache_name(value_type: type[object]) -> str:
22
+ if get_origin(value_type) is not None:
23
+ return str(value_type)
24
+ name = value_type.__dict__.get(_CACHE_NAME)
25
+ if name is None:
26
+ return f"{value_type.__module__}.{value_type.__qualname__}"
27
+ if not isinstance(name, str):
28
+ raise TypeError("cache name metadata must be a string")
29
+ return name
@@ -0,0 +1,38 @@
1
+ """全局默认与调用级完整对象覆盖,不建立按缓存类型的配置表。"""
2
+
3
+ from datetime import datetime, timedelta
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
6
+
7
+ from ..options import BaseOptions
8
+
9
+
10
+ class DistributedCacheEntryOptions(BaseModel):
11
+ model_config = ConfigDict(frozen=True, extra="forbid", validate_default=True)
12
+
13
+ absolute_expiration: datetime | None = None
14
+ absolute_expiration_relative_to_now: timedelta | None = None
15
+ sliding_expiration: timedelta | None = None
16
+
17
+ @field_validator("absolute_expiration")
18
+ @classmethod
19
+ def _aware(cls, value: datetime | None) -> datetime | None:
20
+ if value is not None and value.utcoffset() is None:
21
+ raise ValueError("absolute_expiration must include a timezone")
22
+ return value
23
+
24
+ @field_validator("absolute_expiration_relative_to_now", "sliding_expiration")
25
+ @classmethod
26
+ def _positive(cls, value: timedelta | None) -> timedelta | None:
27
+ if value is not None and not 0 < value.total_seconds() <= 2_147_483_647:
28
+ raise ValueError("expiration must be positive and within the Redis range")
29
+ return value
30
+
31
+
32
+ class DistributedCacheOptions(BaseOptions):
33
+ hide_errors: bool = True
34
+ default_entry_options: DistributedCacheEntryOptions = Field(
35
+ default_factory=lambda: DistributedCacheEntryOptions(
36
+ sliding_expiration=timedelta(minutes=20)
37
+ )
38
+ )
@@ -0,0 +1,46 @@
1
+ """延迟写快照归具体 UoW;未完成 owner 的生命周期状态随资源退出丢弃。"""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from dataclasses import dataclass, field
5
+
6
+ from dishka import AsyncContainer
7
+
8
+ from ..invocation import ApplicationInvocationContext
9
+ from ..unit_of_work import UnitOfWork
10
+
11
+
12
+ @dataclass
13
+ class _CachePending:
14
+ writes: dict[str, tuple[bytes | None, Callable[[], Awaitable[None]]]] = field(
15
+ default_factory=dict
16
+ )
17
+
18
+ async def before_commit(self) -> None:
19
+ pass
20
+
21
+ async def after_commit(self) -> None:
22
+ failures: list[Exception] = []
23
+ try:
24
+ for _, apply in self.writes.values():
25
+ try:
26
+ await apply()
27
+ except Exception as error:
28
+ failures.append(error)
29
+ finally:
30
+ self.writes.clear()
31
+ if failures:
32
+ raise ExceptionGroup("Committed cache writes failed", failures)
33
+
34
+
35
+ class _CacheLifecycleFactory:
36
+ def create(
37
+ self, container: AsyncContainer, context: ApplicationInvocationContext
38
+ ) -> _CachePending:
39
+ return _CachePending()
40
+
41
+
42
+ def _pending(work: UnitOfWork) -> _CachePending:
43
+ for lifecycle in work._lifecycles:
44
+ if isinstance(lifecycle, _CachePending):
45
+ return lifecycle
46
+ raise RuntimeError("Caching lifecycle is not configured for this UnitOfWork")
@@ -6,6 +6,7 @@ import argparse
6
6
  import asyncio
7
7
  import os
8
8
  import subprocess
9
+ import sys
9
10
  from collections.abc import Sequence
10
11
  from enum import Enum
11
12
  from pathlib import Path
@@ -30,12 +31,15 @@ def _parser() -> argparse.ArgumentParser:
30
31
  new.add_argument("name")
31
32
  new.add_argument("--framework-wheel", type=Path, help="Validate with a local framework wheel")
32
33
  new.add_argument("--dry-run", action="store_true")
34
+ new.add_argument("--python", help="Portable target version, e.g. 3.12 or 3.12.13")
35
+ new.add_argument("--no-sync", action="store_true", help="Generate files without an environment")
33
36
  add = commands.add_parser(
34
37
  _Command.ADD.value, help="Add a business module to the current project"
35
38
  ).add_subparsers(dest="kind", required=True)
36
39
  module = add.add_parser("module", help="Generate and register a business module")
37
40
  module.add_argument("name")
38
41
  module.add_argument("--dry-run", action="store_true")
42
+ module.add_argument("--template", choices=("basic", "ddd"), default="ddd")
39
43
  dev = commands.add_parser(_Command.DEV.value, help="Run the development web server")
40
44
  dev.add_argument("--host", default="127.0.0.1")
41
45
  dev.add_argument("--port", type=int, default=8000)
@@ -58,11 +62,24 @@ def _parser() -> argparse.ArgumentParser:
58
62
 
59
63
  def main(argv: Sequence[str] | None = None) -> int:
60
64
  parser = _parser()
61
- args = parser.parse_args(argv)
62
- if args.command is None:
63
- parser.print_help()
64
- return 0
65
+ arguments = list(sys.argv[1:] if argv is None else argv)
66
+ previous_bytecode = sys.dont_write_bytecode
67
+ if "--dry-run" in arguments:
68
+ # 已处于后端环境时也不让 Host 导入在消费者源码目录留下 pycache。
69
+ sys.dont_write_bytecode = True
65
70
  try:
71
+ if arguments and arguments[0] in {
72
+ item.value for item in _Command if item is not _Command.NEW
73
+ }:
74
+ from .project import Project
75
+
76
+ result = Project.current().dispatch(arguments)
77
+ if result is not None:
78
+ return result
79
+ args = parser.parse_args(arguments)
80
+ if args.command is None:
81
+ parser.print_help()
82
+ return 0
66
83
  return _execute(args)
67
84
  except CommandError as error:
68
85
  parser.exit(2, f"pddd: {error}\n")
@@ -70,6 +87,8 @@ def main(argv: Sequence[str] | None = None) -> int:
70
87
  parser.exit(
71
88
  error.returncode, "pddd: external command failed; diagnostic files were retained\n"
72
89
  )
90
+ finally:
91
+ sys.dont_write_bytecode = previous_bytecode
73
92
 
74
93
 
75
94
  def _execute(args: argparse.Namespace) -> int:
@@ -84,9 +103,15 @@ def _execute(args: argparse.Namespace) -> int:
84
103
  "Install python-ddd-framework[developer-kit] to use generation commands"
85
104
  ) from None
86
105
  if command is _Command.NEW:
87
- new_project(args.name, args.framework_wheel, args.dry_run)
106
+ new_project(
107
+ args.name,
108
+ args.framework_wheel,
109
+ args.dry_run,
110
+ python=args.python,
111
+ no_sync=args.no_sync,
112
+ )
88
113
  else:
89
- add_module(args.name, args.dry_run)
114
+ add_module(args.name, args.dry_run, template=args.template)
90
115
  else:
91
116
  from .runtime import database, development_server
92
117
 
@@ -3,7 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import importlib.metadata as metadata
6
+ import os
7
+ import subprocess
8
+ import sys
6
9
  import tomllib
10
+ from collections.abc import Sequence
7
11
  from dataclasses import dataclass
8
12
  from pathlib import Path
9
13
  from typing import Any
@@ -33,12 +37,53 @@ class Project:
33
37
 
34
38
  @classmethod
35
39
  def current(cls) -> Project:
36
- root = Path.cwd().resolve()
37
- project = read_project(root / "pyproject.toml").get("project", {})
38
- name = project.get("name") if isinstance(project, dict) else None
39
- if not isinstance(name, str) or not name:
40
- raise CommandError("Current pyproject.toml must declare project.name")
41
- return cls(root, name)
40
+ current = Path.cwd().resolve()
41
+ # 标准 Host entry point 标识所属后端;无 Host 的嵌套 Python 项目不拥有 pddd 命令。
42
+ for parent in (current, *current.parents):
43
+ for root in (parent / "backend", parent):
44
+ path = root / "pyproject.toml"
45
+ if not path.is_file():
46
+ continue
47
+ project = read_project(path).get("project", {})
48
+ if not isinstance(project, dict):
49
+ raise CommandError("Project metadata must declare project.name")
50
+ hosts = project.get("entry-points", {}).get(HOST_ENTRY_POINT_GROUP)
51
+ if hosts is None:
52
+ continue
53
+ name = project.get("name")
54
+ if not isinstance(name, str) or not name:
55
+ raise CommandError("Project metadata must declare project.name")
56
+ return cls(root, name)
57
+ raise CommandError("Cannot find backend project metadata with framework Host entry points")
58
+
59
+ def dispatch(self, argv: Sequence[str]) -> int | None:
60
+ """先交给所属环境中的 CLI,再解释命令,避免全局工具版本劫持项目。"""
61
+ environment = self.root / ".venv"
62
+ if Path(sys.prefix).resolve() == environment.resolve():
63
+ os.chdir(self.root)
64
+ return None
65
+ if not (environment / "pyvenv.cfg").is_file():
66
+ raise CommandError(f'Run uv sync --project "{self.root}" before project commands')
67
+ return subprocess.run(
68
+ [
69
+ "uv",
70
+ "run",
71
+ "--project",
72
+ str(self.root),
73
+ "--directory",
74
+ str(self.root),
75
+ "--no-sync",
76
+ "python",
77
+ "-I",
78
+ "-B",
79
+ "-c",
80
+ "from python_ddd_framework.cli import main; raise SystemExit(main())",
81
+ *argv,
82
+ ],
83
+ cwd=self.root,
84
+ env=project_environment(),
85
+ check=False,
86
+ ).returncode
42
87
 
43
88
  def host(self, name: str) -> metadata.EntryPoint:
44
89
  try:
@@ -67,3 +112,20 @@ class Project:
67
112
  if len(matches) != 1:
68
113
  raise CommandError(f"Module alias {name!r} must select exactly one live Module")
69
114
  return live[matches.pop()]
115
+
116
+
117
+ def project_environment() -> dict[str, str]:
118
+ # 父进程可能来自 ROS、uv tool 或另一个项目;后端环境由本项目声明和 uv 选择。
119
+ environment = dict(os.environ)
120
+ for key in (
121
+ "VIRTUAL_ENV",
122
+ "PYTHONPATH",
123
+ "PYTHONHOME",
124
+ "UV_PROJECT_ENVIRONMENT",
125
+ "UV_PROJECT",
126
+ "UV_WORKING_DIR",
127
+ "UV_WORKING_DIRECTORY",
128
+ "UV_PYTHON",
129
+ ):
130
+ environment.pop(key, None)
131
+ return environment
@@ -11,7 +11,7 @@ from .project import Project
11
11
 
12
12
  async def database(operation: str, environment: str, module: str) -> None:
13
13
  from ..application import Application
14
- from ..application_services.seeding import DataSeeder
14
+ from ..seeding import DataSeeder
15
15
  from ..sqlalchemy import SqlAlchemyMigrator
16
16
 
17
17
  project = Project.current()
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import ast
6
+ import asyncio
6
7
  import importlib.metadata as metadata
7
8
  import importlib.resources as resources
8
9
  import inspect
@@ -15,7 +16,7 @@ import shutil
15
16
  import subprocess
16
17
  import tempfile
17
18
  from pathlib import Path
18
- from typing import Any
19
+ from typing import Any, Literal
19
20
 
20
21
  # 官方库未发布 typing marker;仅在第三方导入边界忽略缺失的类型信息。
21
22
  from cookiecutter.main import cookiecutter # type: ignore[import-untyped]
@@ -23,7 +24,7 @@ from cookiecutter.main import cookiecutter # type: ignore[import-untyped]
23
24
  from ..application import Application
24
25
  from ..cli import ENVIRONMENT_VARIABLE
25
26
  from ..cli.errors import CommandError
26
- from ..cli.project import Project, read_project
27
+ from ..cli.project import Project, project_environment, read_project
27
28
  from .project_metadata import add_module_metadata
28
29
  from .source import framework_source
29
30
  from .wiring import add_dependency
@@ -79,8 +80,30 @@ def _validate(root: Path) -> None:
79
80
  read_project(path)
80
81
 
81
82
 
82
- def new_project(name: str, wheel: Path | None, dry_run: bool) -> None:
83
+ def _python_version(value: str | None) -> tuple[str, str]:
84
+ version = value or platform.python_version()
85
+ match = re.fullmatch(r"(\d+\.\d+)(?:\.\d+)?", version)
86
+ distribution = metadata.distribution("python-ddd-framework")
87
+ supported = {
88
+ classifier.removeprefix("Programming Language :: Python :: ")
89
+ for classifier in distribution.metadata.get_all("Classifier", [])
90
+ if classifier.startswith("Programming Language :: Python :: ")
91
+ }
92
+ if match is None or match[1] not in supported:
93
+ raise CommandError("--python must be a supported Python major.minor[.patch] version")
94
+ return version, "==" + version + (".*" if version.count(".") == 1 else "")
95
+
96
+
97
+ def new_project(
98
+ name: str,
99
+ wheel: Path | None,
100
+ dry_run: bool,
101
+ *,
102
+ python: str | None = None,
103
+ no_sync: bool = False,
104
+ ) -> None:
83
105
  package, class_prefix = _name(name)
106
+ python_version, requires_python = _python_version(python)
84
107
  destination = Path.cwd() / name
85
108
  if destination.exists():
86
109
  raise CommandError(f"Destination already exists: {destination}")
@@ -100,7 +123,8 @@ def new_project(name: str, wheel: Path | None, dry_run: bool) -> None:
100
123
  "class_prefix": class_prefix,
101
124
  "framework_version": source.version,
102
125
  "framework_source": source.uv_source,
103
- "python_version": platform.python_version(),
126
+ "python_version": python_version,
127
+ "requires_python": requires_python,
104
128
  "build_version": generator,
105
129
  "host_environment_variable": ENVIRONMENT_VARIABLE,
106
130
  "database_password": secrets.token_urlsafe(24),
@@ -109,22 +133,32 @@ def new_project(name: str, wheel: Path | None, dry_run: bool) -> None:
109
133
  },
110
134
  )
111
135
  if source.wheel is not None:
112
- vendor = staged / "vendor"
136
+ vendor = staged / "backend" / "vendor"
113
137
  vendor.mkdir()
114
138
  shutil.copy2(source.wheel, vendor / source.wheel.name)
115
139
  # copytree 原生拒绝已存在目录;staging 跨 Windows 盘符,不能依赖目录 rename。
116
140
  shutil.copytree(staged, destination)
117
141
  print(f"Generated {destination}; staging retained at {staged}")
118
- subprocess.run(["uv", "sync"], cwd=destination, check=True)
142
+ if not no_sync:
143
+ backend = destination / "backend"
144
+ subprocess.run(
145
+ ["uv", "sync", "--directory", str(backend)],
146
+ cwd=backend,
147
+ env=project_environment(),
148
+ check=True,
149
+ )
119
150
 
120
151
 
121
- def add_module(name: str, dry_run: bool) -> None:
152
+ def add_module(name: str, dry_run: bool, *, template: Literal["basic", "ddd"] = "ddd") -> None:
122
153
  package, class_prefix = _name(name)
123
154
  project = Project.current()
124
155
  application = project.host("application").load()(environment="development")
125
156
  if not isinstance(application, Application):
126
157
  raise CommandError("The application entry point must return Application")
127
- startup = application.modules.startup.module_type
158
+ try:
159
+ startup = application.modules.startup.module_type
160
+ finally:
161
+ asyncio.run(application.stop())
128
162
  if startup.__qualname__ != startup.__name__:
129
163
  raise CommandError("Startup Module must be a top-level class for module generation")
130
164
  source_path = Path(inspect.getfile(startup)).resolve()
@@ -138,29 +172,43 @@ def add_module(name: str, dry_run: bool) -> None:
138
172
  raise CommandError(f"Module destination already exists: {destination}")
139
173
  source = source_path.read_text(encoding="utf-8")
140
174
  updated = source
141
- for layer, suffix in (
142
- ("application", "ApplicationModule"),
143
- ("sqlalchemy", "SqlAlchemyModule"),
144
- ("http_api", "HttpApiModule"),
145
- ):
146
- updated = add_dependency(
147
- updated, startup.__name__, f"modules.{package}.{layer}.module", class_prefix + suffix
175
+ entries = (
176
+ ((f"modules.{package}.module", class_prefix + "Module"),)
177
+ if template == "basic"
178
+ else tuple(
179
+ (f"modules.{package}.{layer}.module", class_prefix + suffix)
180
+ for layer, suffix in (
181
+ ("application", "ApplicationModule"),
182
+ ("sqlalchemy", "SqlAlchemyModule"),
183
+ ("http_api", "HttpApiModule"),
184
+ )
148
185
  )
186
+ )
187
+ for import_path, class_name in entries:
188
+ updated = add_dependency(updated, startup.__name__, import_path, class_name)
149
189
  metadata_path = project.root / "pyproject.toml"
150
190
  metadata_source = metadata_path.read_text(encoding="utf-8")
151
- updated_metadata = add_module_metadata(metadata_source, package, class_prefix)
191
+ alias_path, alias_class = entries[0 if template == "basic" else 1]
192
+ updated_metadata = add_module_metadata(metadata_source, package, f"{alias_path}:{alias_class}")
152
193
  ast.parse(updated)
153
194
  if dry_run:
154
195
  print(
155
196
  f"Would add {destination}, wire modules into {source_path} and update {metadata_path}"
156
197
  )
157
198
  return
199
+ context = {"module_name": package, "class_prefix": class_prefix}
200
+ if template == "ddd":
201
+ from ..fastapi.application_services import _operation_id, _service_path
202
+
203
+ # HTTP 身份由正式约定推导;basic 不读取或生成传输层配置。
204
+ http_identity = type(class_prefix + "ApplicationService", (), {})
205
+ context.update(
206
+ http_service_path=_service_path("", http_identity),
207
+ http_operation_prefix=_operation_id(http_identity, ""),
208
+ )
158
209
  staged = _render(
159
- "module",
160
- {
161
- "module_name": package,
162
- "class_prefix": class_prefix,
163
- },
210
+ "basic" if template == "basic" else "module",
211
+ context,
164
212
  )
165
213
  # 先验证全部新文件和接线;已有 startup 文件仅在内容仍等于读取快照时替换。
166
214
  if (
@@ -178,5 +226,10 @@ def add_module(name: str, dry_run: bool) -> None:
178
226
  pending.write(updated)
179
227
  Path(pending.name).replace(source_path)
180
228
  metadata_path.write_text(updated_metadata, encoding="utf-8")
181
- subprocess.run(["uv", "sync"], cwd=project.root, check=True)
229
+ subprocess.run(
230
+ ["uv", "sync", "--directory", str(project.root)],
231
+ cwd=project.root,
232
+ env=project_environment(),
233
+ check=True,
234
+ )
182
235
  print(f"Added {package}; staging retained at {staged}")
@@ -8,7 +8,7 @@ from ..cli.errors import CommandError
8
8
  from ..cli.project import MODULE_ENTRY_POINT_GROUP
9
9
 
10
10
 
11
- def add_module_metadata(source: str, package: str, class_prefix: str) -> str:
11
+ def add_module_metadata(source: str, package: str, entry_point: str) -> str:
12
12
  project = tomllib.loads(source)
13
13
  backend = project.get("tool", {}).get("uv", {}).get("build-backend", {})
14
14
  names = backend.get("module-name")
@@ -32,9 +32,7 @@ def add_module_metadata(source: str, package: str, class_prefix: str) -> str:
32
32
  if package in entries:
33
33
  raise CommandError(f"Module alias already exists: {package}")
34
34
  header = f'[project.entry-points."{MODULE_ENTRY_POINT_GROUP}"]'
35
- declaration = (
36
- f'{package} = "modules.{package}.sqlalchemy.module:{class_prefix}SqlAlchemyModule"\n'
37
- )
35
+ declaration = f'{package} = "{entry_point}"\n'
38
36
  if MODULE_ENTRY_POINT_GROUP in project["project"].get("entry-points", {}):
39
37
  if header not in updated:
40
38
  raise CommandError("Module entry point table must use the standard quoted group name")