python-ddd-framework 0.3.2__py3-none-any.whl → 0.5.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 (337) 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 +120 -288
  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 +197 -15
  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/local.py +20 -1
  25. python_ddd_framework/background_execution/module.py +107 -1
  26. python_ddd_framework/background_execution/processes.py +11 -1
  27. python_ddd_framework/background_jobs/catalog.py +59 -42
  28. python_ddd_framework/background_jobs/contracts.py +16 -19
  29. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +4 -0
  30. python_ddd_framework/background_jobs/pgqueuer/module.py +2 -1
  31. python_ddd_framework/background_jobs/pgqueuer/runtime.py +7 -7
  32. python_ddd_framework/background_workers/catalog.py +23 -36
  33. python_ddd_framework/background_workers/contracts.py +3 -0
  34. python_ddd_framework/background_workers/execution.py +1 -1
  35. python_ddd_framework/background_workers/runtime.py +47 -1
  36. python_ddd_framework/caching/__init__.py +7 -6
  37. python_ddd_framework/caching/contracts.py +65 -67
  38. python_ddd_framework/caching/errors.py +0 -7
  39. python_ddd_framework/caching/module.py +18 -0
  40. python_ddd_framework/caching/naming.py +29 -0
  41. python_ddd_framework/caching/options.py +38 -0
  42. python_ddd_framework/caching/unit_of_work.py +46 -0
  43. python_ddd_framework/cli/__init__.py +31 -6
  44. python_ddd_framework/cli/project.py +68 -6
  45. python_ddd_framework/cli/runtime.py +1 -1
  46. python_ddd_framework/developer_kit/generation.py +75 -22
  47. python_ddd_framework/developer_kit/project_metadata.py +2 -4
  48. python_ddd_framework/developer_kit/templates/basic/cookiecutter.json +4 -0
  49. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +45 -0
  50. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/__init__.py.jinja +0 -0
  51. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/__init__.py.jinja +0 -0
  52. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/conversion_service.py.jinja +7 -0
  53. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/module.py.jinja +38 -0
  54. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/__init__.py.jinja +0 -0
  55. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/default_conversion_service.py.jinja +10 -0
  56. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
  57. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +42 -49
  58. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/__init__.py.jinja +1 -0
  59. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +1 -0
  60. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +45 -0
  61. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +8 -0
  62. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +1 -0
  63. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +35 -0
  64. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +5 -0
  65. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +15 -0
  66. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/__init__.py.jinja +1 -0
  67. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +28 -0
  68. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +26 -0
  69. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/__init__.py.jinja +1 -0
  70. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +9 -0
  71. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +9 -0
  72. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/__init__.py.jinja +1 -0
  73. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +23 -0
  74. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/__init__.py.jinja +1 -0
  75. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_integration_service.py.jinja +71 -0
  76. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +16 -0
  77. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/__init__.py.jinja +1 -0
  78. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +16 -0
  79. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/__init__.py.jinja +1 -0
  80. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +17 -0
  81. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +105 -10
  82. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/__init__.py.jinja +1 -0
  83. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{options.py.jinja → options/order_options.py.jinja} +1 -1
  84. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/__init__.py.jinja +1 -0
  85. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +65 -0
  86. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +35 -0
  87. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +41 -0
  88. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +1 -0
  89. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +17 -0
  90. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/__init__.py.jinja +1 -0
  91. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +6 -0
  92. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +7 -0
  93. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/__init__.py.jinja +1 -0
  94. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +7 -0
  95. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +39 -2
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/__init__.py.jinja +1 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +13 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +10 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +12 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/__init__.py.jinja +1 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +9 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +12 -0
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/__init__.py.jinja +1 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{orders.py.jinja → entities/order.py.jinja} +10 -18
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/__init__.py.jinja +1 -0
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +13 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +42 -3
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/__init__.py.jinja +1 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +17 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/__init__.py.jinja +1 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +22 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/__init__.py.jinja +1 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +23 -0
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/__init__.py.jinja +1 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{settings.py.jinja → settings/approval_settings.py.jinja} +3 -2
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/__init__.py.jinja +1 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +10 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/__init__.py.jinja +1 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +3 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/__init__.py.jinja +1 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +6 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/__init__.py.jinja +1 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +11 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/__init__.py.jinja +1 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +26 -0
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +13 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +40 -1
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/__init__.py.jinja +1 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/{permissions.py.jinja → permissions/order_permission_provider.py.jinja} +6 -2
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +7 -0
  131. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/__init__.py.jinja +1 -0
  132. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +18 -0
  133. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/__init__.py.jinja +1 -0
  134. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +15 -0
  135. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/__init__.py.jinja +1 -0
  136. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +5 -0
  137. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +85 -7
  138. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/__init__.py.jinja +1 -0
  139. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/{files.py.jinja → routers/order_files.py.jinja} +23 -20
  140. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/__init__.py.jinja +1 -0
  141. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +44 -0
  142. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -1
  143. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +1 -7
  144. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +7 -0
  145. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/{orders.py.jinja → order_model.py.jinja} +7 -3
  146. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +42 -5
  147. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +79 -0
  148. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/__init__.py.jinja +1 -0
  149. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +4 -3
  150. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +1 -1
  151. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +23 -3
  152. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +29 -22
  153. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{app.development.yaml.jinja → backend/app.development.yaml.jinja} +2 -0
  154. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{pyproject.toml.jinja → backend/pyproject.toml.jinja} +1 -1
  155. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/main.py.jinja +2 -0
  156. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/module.py.jinja +40 -3
  157. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +42 -11
  158. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +144 -23
  159. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/scripts/.gitkeep +0 -0
  160. python_ddd_framework/distributed_lock/contracts.py +4 -1
  161. python_ddd_framework/domain/__init__.py +2 -0
  162. python_ddd_framework/domain/services.py +11 -0
  163. python_ddd_framework/errors/business.py +10 -1
  164. python_ddd_framework/errors/lifecycle.py +3 -31
  165. python_ddd_framework/events/__init__.py +5 -2
  166. python_ddd_framework/events/aggregate.py +3 -3
  167. python_ddd_framework/events/catalog.py +12 -11
  168. python_ddd_framework/events/contracts.py +6 -1
  169. python_ddd_framework/events/contribution.py +1 -52
  170. python_ddd_framework/events/entities.py +23 -0
  171. python_ddd_framework/events/module.py +97 -0
  172. python_ddd_framework/events/runtime.py +80 -29
  173. python_ddd_framework/events/subscription.py +36 -0
  174. python_ddd_framework/events/unit_of_work.py +25 -0
  175. python_ddd_framework/extensions/__init__.py +22 -0
  176. python_ddd_framework/extensions/catalog.py +198 -0
  177. python_ddd_framework/extensions/contracts.py +85 -0
  178. python_ddd_framework/extensions/models.py +142 -0
  179. python_ddd_framework/extensions/module.py +21 -0
  180. python_ddd_framework/fastapi/__init__.py +2 -0
  181. python_ddd_framework/fastapi/action.py +1 -2
  182. python_ddd_framework/fastapi/adapter.py +16 -0
  183. python_ddd_framework/fastapi/application_services.py +43 -25
  184. python_ddd_framework/fastapi/background.py +2 -1
  185. python_ddd_framework/fastapi/contracts.py +26 -8
  186. python_ddd_framework/fastapi/health.py +1 -1
  187. python_ddd_framework/fastapi/module.py +14 -0
  188. python_ddd_framework/fastapi/parameters.py +3 -18
  189. python_ddd_framework/fastapi/realtime/connection.py +10 -7
  190. python_ddd_framework/fastapi/realtime/module.py +3 -0
  191. python_ddd_framework/fastapi/realtime/runtime.py +10 -24
  192. python_ddd_framework/fastapi/request_context.py +2 -1
  193. python_ddd_framework/fastapi/routing.py +2 -1
  194. python_ddd_framework/fastapi/server.py +8 -0
  195. python_ddd_framework/fastapi/settings.py +2 -1
  196. python_ddd_framework/hosted_services/bridge.py +95 -17
  197. python_ddd_framework/hosted_services/catalog.py +6 -16
  198. python_ddd_framework/hosted_services/contracts.py +9 -1
  199. python_ddd_framework/hosted_services/declarations.py +15 -0
  200. python_ddd_framework/hosted_services/module.py +69 -0
  201. python_ddd_framework/hosted_services/runtime.py +101 -18
  202. python_ddd_framework/identity/__init__.py +24 -0
  203. python_ddd_framework/identity/application.py +38 -90
  204. python_ddd_framework/identity/contracts.py +181 -16
  205. python_ddd_framework/identity/http_api.py +79 -2
  206. python_ddd_framework/identity/management.py +166 -0
  207. python_ddd_framework/identity/module.py +12 -4
  208. python_ddd_framework/identity/services.py +29 -16
  209. python_ddd_framework/identity/sqlalchemy/migrations/0003_extra_properties.py +26 -0
  210. python_ddd_framework/identity/sqlalchemy/migrations/0004_concurrency_version.py +29 -0
  211. python_ddd_framework/identity/sqlalchemy/models.py +21 -2
  212. python_ddd_framework/identity/sqlalchemy/module.py +11 -2
  213. python_ddd_framework/identity/sqlalchemy/repository.py +458 -0
  214. python_ddd_framework/identity/sqlalchemy/session_security.py +88 -0
  215. python_ddd_framework/identity/sqlalchemy/stores.py +69 -290
  216. python_ddd_framework/identity/tokens.py +2 -5
  217. python_ddd_framework/{application_services/validation.py → invocation/arguments.py} +8 -6
  218. python_ddd_framework/invocation/contribution.py +61 -0
  219. python_ddd_framework/invocation/dispatcher.py +291 -0
  220. python_ddd_framework/invocation/entries.py +2 -25
  221. python_ddd_framework/invocation/entrypoints.py +126 -0
  222. python_ddd_framework/{application_services → invocation}/execution.py +8 -5
  223. python_ddd_framework/invocation/function_runtime.py +3 -1
  224. python_ddd_framework/invocation/interception.py +113 -42
  225. python_ddd_framework/{application_services → invocation}/interceptors.py +2 -2
  226. python_ddd_framework/invocation/managed_proxy.py +116 -0
  227. python_ddd_framework/invocation/managed_services.py +250 -0
  228. python_ddd_framework/invocation/methods.py +345 -0
  229. python_ddd_framework/invocation/module.py +113 -0
  230. python_ddd_framework/{application_services → invocation}/policies.py +8 -1
  231. python_ddd_framework/invocation/validation.py +25 -0
  232. python_ddd_framework/lifecycle/__init__.py +2 -0
  233. python_ddd_framework/lifecycle/composition.py +5 -2
  234. python_ddd_framework/lifecycle/participants.py +37 -0
  235. python_ddd_framework/messaging/__init__.py +25 -0
  236. python_ddd_framework/messaging/channel.py +232 -0
  237. python_ddd_framework/messaging/contracts.py +78 -0
  238. python_ddd_framework/messaging/module.py +114 -0
  239. python_ddd_framework/messaging/options.py +13 -0
  240. python_ddd_framework/messaging/receipt.py +27 -0
  241. python_ddd_framework/messaging/runtime.py +139 -0
  242. python_ddd_framework/modularity/__init__.py +11 -1
  243. python_ddd_framework/modularity/contracts.py +47 -16
  244. python_ddd_framework/modularity/discovery.py +21 -2
  245. python_ddd_framework/modularity/graph.py +45 -1
  246. python_ddd_framework/modularity/registry.py +83 -3
  247. python_ddd_framework/notifications/__init__.py +2 -0
  248. python_ddd_framework/notifications/catalog.py +3 -2
  249. python_ddd_framework/notifications/declarations.py +18 -0
  250. python_ddd_framework/notifications/module.py +21 -0
  251. python_ddd_framework/observability/logging.py +3 -0
  252. python_ddd_framework/observability/tracing.py +3 -0
  253. python_ddd_framework/options/contribution.py +12 -0
  254. python_ddd_framework/options/registry.py +44 -11
  255. python_ddd_framework/realtime/__init__.py +2 -1
  256. python_ddd_framework/realtime/contracts.py +3 -7
  257. python_ddd_framework/realtime/messages.py +26 -1
  258. python_ddd_framework/redis/cache.py +243 -0
  259. python_ddd_framework/redis/cache_scripts.py +40 -0
  260. python_ddd_framework/redis/distributed_lock.py +10 -2
  261. python_ddd_framework/redis/module.py +13 -4
  262. python_ddd_framework/redis/notifications.py +5 -2
  263. python_ddd_framework/redis/runtime.py +1 -79
  264. python_ddd_framework/seeding/__init__.py +13 -0
  265. python_ddd_framework/seeding/contracts.py +45 -0
  266. python_ddd_framework/seeding/module.py +73 -0
  267. python_ddd_framework/seeding/runtime.py +63 -0
  268. python_ddd_framework/services/arbitration.py +23 -0
  269. python_ddd_framework/services/binding.py +2 -3
  270. python_ddd_framework/services/composition.py +108 -0
  271. python_ddd_framework/services/contribution.py +44 -54
  272. python_ddd_framework/services/convention.py +53 -181
  273. python_ddd_framework/services/fixed_lifetime.py +12 -15
  274. python_ddd_framework/services/framework_provider.py +3 -158
  275. python_ddd_framework/services/native_graph.py +35 -60
  276. python_ddd_framework/services/provider.py +59 -4
  277. python_ddd_framework/services/repository.py +6 -0
  278. python_ddd_framework/services/runtime.py +129 -215
  279. python_ddd_framework/settings/catalog.py +5 -4
  280. python_ddd_framework/settings/declarations.py +18 -0
  281. python_ddd_framework/settings/definitions.py +5 -0
  282. python_ddd_framework/settings/manager.py +1 -1
  283. python_ddd_framework/settings/module.py +50 -1
  284. python_ddd_framework/settings/notifications.py +4 -2
  285. python_ddd_framework/settings/refresh_module.py +9 -2
  286. python_ddd_framework/settings/sqlalchemy/module.py +2 -1
  287. python_ddd_framework/sqlalchemy/__init__.py +12 -0
  288. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +1 -0
  289. python_ddd_framework/sqlalchemy/auditing.py +7 -1
  290. python_ddd_framework/sqlalchemy/extensions.py +186 -0
  291. python_ddd_framework/sqlalchemy/metadata.py +110 -6
  292. python_ddd_framework/sqlalchemy/migration.py +61 -8
  293. python_ddd_framework/sqlalchemy/migration_operations.py +86 -0
  294. python_ddd_framework/sqlalchemy/module.py +14 -11
  295. python_ddd_framework/sqlalchemy/module_migration.py +71 -3
  296. python_ddd_framework/sqlalchemy/repository.py +217 -13
  297. python_ddd_framework/sqlalchemy/session_provider.py +14 -1
  298. python_ddd_framework/testing/runtime.py +5 -4
  299. python_ddd_framework/unit_of_work/__init__.py +2 -1
  300. python_ddd_framework/unit_of_work/contracts.py +6 -7
  301. python_ddd_framework/unit_of_work/errors.py +11 -0
  302. python_ddd_framework/unit_of_work/lifecycle.py +19 -0
  303. python_ddd_framework/unit_of_work/manager.py +86 -32
  304. python_ddd_framework/unit_of_work/module.py +61 -0
  305. python_ddd_framework/unit_of_work/options.py +7 -1
  306. python_ddd_framework/validation/__init__.py +1 -0
  307. python_ddd_framework/validation/models.py +37 -0
  308. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/METADATA +147 -44
  309. python_ddd_framework-0.5.1.dist-info/RECORD +488 -0
  310. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/WHEEL +1 -1
  311. python_ddd_framework/application_services/seeding.py +0 -72
  312. python_ddd_framework/caching/catalog.py +0 -73
  313. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +0 -10
  314. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +0 -24
  315. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +0 -41
  316. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +0 -83
  317. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +0 -84
  318. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +0 -35
  319. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +0 -14
  320. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +0 -19
  321. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +0 -24
  322. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +0 -36
  323. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +0 -49
  324. python_ddd_framework-0.3.2.dist-info/RECORD +0 -355
  325. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.dockerignore → backend/.dockerignore} +0 -0
  326. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.gitignore → backend/.gitignore} +0 -0
  327. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.python-version → backend/.python-version} +0 -0
  328. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{Dockerfile → backend/Dockerfile} +0 -0
  329. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.dev.yaml.jinja → backend/compose.dev.yaml.jinja} +0 -0
  330. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.production.yaml.jinja → backend/compose.production.yaml.jinja} +0 -0
  331. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/__init__.py.jinja +0 -0
  332. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/conftest.py.jinja +0 -0
  333. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/host/test_http.py.jinja +0 -0
  334. /python_ddd_framework/{application_services → invocation}/signature.py +0 -0
  335. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/entry_points.txt +0 -0
  336. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/licenses/LICENSE +0 -0
  337. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -10,7 +10,8 @@ from typing import cast
10
10
  from dishka import AsyncContainer
11
11
 
12
12
  from ..application_services import ApplicationServiceCatalog
13
- from ..lifecycle import ApplicationState, ShutdownReason
13
+ from ..application_services.invocation import _InvocationRuntime
14
+ from ..lifecycle import ApplicationState, RuntimeParticipant, ShutdownReason
14
15
  from .bridge import HostedServiceContext, _HostedServiceBridge
15
16
  from .catalog import HostedServiceCatalog, HostedServiceRegistration
16
17
  from .contracts import HostedService, HostedServiceStatus
@@ -25,6 +26,8 @@ class _HostedServiceEntry:
25
26
  service: HostedService
26
27
  context: HostedServiceContext
27
28
  bridge: _HostedServiceBridge
29
+ started: bool = False
30
+ failed: bool = False
28
31
 
29
32
 
30
33
  class _HostedServiceCoordinator:
@@ -72,6 +75,38 @@ class _HostedServiceCoordinator:
72
75
  def readiness(self) -> ApplicationReadinessSnapshot:
73
76
  return self._state_owner.snapshot(self._state_getter())
74
77
 
78
+ def runtime_participant(self) -> RuntimeParticipant:
79
+ enabled = True
80
+
81
+ def prepare(run_services: bool) -> None:
82
+ nonlocal enabled
83
+ enabled = run_services
84
+
85
+ async def start() -> None:
86
+ if not enabled:
87
+ return
88
+ try:
89
+ await self.start()
90
+ except HostedServiceLifecycleError:
91
+ raise
92
+ except BaseException as error:
93
+ raise HostedServiceLifecycleError(
94
+ service=type(self).__qualname__,
95
+ operation="start",
96
+ error_type=type(error).__name__,
97
+ ) from error
98
+
99
+ return RuntimeParticipant(
100
+ prepare=prepare,
101
+ start=start,
102
+ stop=self.stop,
103
+ quiesce=self.close_admission,
104
+ stopping=self.stopping,
105
+ close_admission=self.close_admission,
106
+ drain=self.drain,
107
+ ready=lambda: self.readiness().ready,
108
+ )
109
+
75
110
  def select_child(self) -> None:
76
111
  if self._started:
77
112
  raise RuntimeError("Hosted Service process selection must precede start")
@@ -97,6 +132,7 @@ class _HostedServiceCoordinator:
97
132
  if container is None or application_service_catalog is None:
98
133
  raise RuntimeError("Hosted Service coordinator container is unavailable")
99
134
  self._started = True
135
+ runtime = await container.get(_InvocationRuntime)
100
136
  for registration in self._registrations:
101
137
  self._state_owner.set_status(registration.name, HostedServiceStatus.STARTING)
102
138
  try:
@@ -121,17 +157,55 @@ class _HostedServiceCoordinator:
121
157
  container=container,
122
158
  catalog=application_service_catalog,
123
159
  limit=self._options.max_pending_invocations,
160
+ runtime=runtime,
161
+ stopping_owners=(registration.service_type,),
124
162
  )
125
163
  context = HostedServiceContext(registration.name, bridge)
126
164
  entry = _HostedServiceEntry(registration, resolved, context, bridge)
127
165
  self._entries.append(entry)
128
166
  bridge.open()
129
167
  await self._start_entry(entry)
168
+ entry.started = True
130
169
 
131
170
  def close_admission(self) -> None:
132
171
  for entry in self._entries:
133
172
  entry.bridge.close()
134
173
 
174
+ async def stopping(self, reason: ShutdownReason) -> tuple[BaseException, ...]:
175
+ failures: list[BaseException] = []
176
+ for entry in reversed(self._entries):
177
+ self._state_owner.set_status(entry.registration.name, HostedServiceStatus.STOPPING)
178
+ if not entry.started:
179
+ continue
180
+ assert self._container is not None
181
+ runtime = await self._container.get(_InvocationRuntime)
182
+ permission = runtime.grant_stopping(entry.registration.service_type)
183
+ context = HostedServiceContext(entry.registration.name, entry.bridge, permission)
184
+ try:
185
+ failure = await self._stop_entry(entry, reason, context=context)
186
+ except BaseException as error:
187
+ failures.append(error)
188
+ entry.failed = True
189
+ else:
190
+ if failure is not None:
191
+ failures.append(failure)
192
+ entry.failed = True
193
+ finally:
194
+ runtime.revoke_stopping(permission)
195
+ return tuple(failures)
196
+
197
+ async def drain(self) -> None:
198
+ outcomes = await asyncio.gather(
199
+ *(
200
+ entry.bridge.drain(self._options.shutdown_timeout.total_seconds())
201
+ for entry in self._entries
202
+ ),
203
+ return_exceptions=True,
204
+ )
205
+ failures = tuple(item for item in outcomes if isinstance(item, BaseException))
206
+ if failures:
207
+ raise BaseExceptionGroup("Hosted Service invocation drain failed", failures)
208
+
135
209
  async def stop(
136
210
  self,
137
211
  reason: ShutdownReason,
@@ -147,19 +221,14 @@ class _HostedServiceCoordinator:
147
221
  if failure is not None:
148
222
  failures.append(failure)
149
223
 
150
- if self._entries:
151
- await asyncio.gather(
152
- *(
153
- entry.bridge.drain(self._options.shutdown_timeout.total_seconds())
154
- for entry in self._entries
155
- )
156
- )
157
224
  failed_names = {failure.service for failure in failures}
158
225
  for entry in self._entries:
159
226
  name = entry.registration.name
160
227
  self._state_owner.set_status(
161
228
  name,
162
- HostedServiceStatus.FAILED if name in failed_names else HostedServiceStatus.STOPPED,
229
+ HostedServiceStatus.FAILED
230
+ if name in failed_names or entry.failed
231
+ else HostedServiceStatus.STOPPED,
163
232
  )
164
233
  self._started = False
165
234
  return tuple(failures)
@@ -224,18 +293,29 @@ class _HostedServiceCoordinator:
224
293
  self,
225
294
  entry: _HostedServiceEntry,
226
295
  reason: ShutdownReason,
296
+ *,
297
+ context: HostedServiceContext | None = None,
227
298
  ) -> HostedServiceLifecycleError | None:
228
299
  name = entry.registration.name
229
- callback = cast(
230
- Callable[[ShutdownReason], Awaitable[object]],
231
- entry.service.stop,
232
- )
300
+ operation = "stopping" if context is not None else "stop"
233
301
 
234
302
  async def invoke_stop() -> object:
235
- return await callback(reason)
303
+ if context is None:
304
+ callback = cast(Callable[[ShutdownReason], Awaitable[object]], entry.service.stop)
305
+ return await callback(reason)
306
+ stopping_callback = cast(
307
+ Callable[[HostedServiceContext, ShutdownReason], Awaitable[object]],
308
+ entry.service.stopping,
309
+ )
310
+ try:
311
+ return await stopping_callback(context, reason)
312
+ finally:
313
+ # 返回和失败都在用户钩子的 task 内撤权,不留下一轮调度的准入窗口。
314
+ assert context._permission is not None
315
+ context._permission.runtime.revoke_stopping(context._permission)
236
316
 
237
317
  stop_task = asyncio.create_task(
238
- invoke_stop(), name=f"python-ddd-framework-hosted-service-stop:{name}"
318
+ invoke_stop(), name=f"python-ddd-framework-hosted-service-{operation}:{name}"
239
319
  )
240
320
  done, _ = await asyncio.wait(
241
321
  (stop_task,), timeout=self._options.shutdown_timeout.total_seconds()
@@ -243,25 +323,28 @@ class _HostedServiceCoordinator:
243
323
  timed_out = not done
244
324
  if timed_out:
245
325
  # 取消只是一项请求。实现可能在 finally 中异步释放 SDK 资源;完成前不能释放容器。
326
+ if context is not None:
327
+ assert context._permission is not None
328
+ context._permission.runtime.revoke_stopping(context._permission)
246
329
  stop_task.cancel()
247
330
  try:
248
331
  result = await stop_task
249
332
  except BaseException as original_error:
250
333
  return HostedServiceLifecycleError(
251
334
  service=name,
252
- operation="stop",
335
+ operation=operation,
253
336
  error_type="TimeoutError"
254
337
  if timed_out and isinstance(original_error, asyncio.CancelledError)
255
338
  else type(original_error).__name__,
256
339
  )
257
340
  if timed_out:
258
341
  return HostedServiceLifecycleError(
259
- service=name, operation="stop", error_type="TimeoutError"
342
+ service=name, operation=operation, error_type="TimeoutError"
260
343
  )
261
344
  if result is not None:
262
345
  return HostedServiceLifecycleError(
263
346
  service=name,
264
- operation="stop",
347
+ operation=operation,
265
348
  error_type="InvalidReturnValue",
266
349
  )
267
350
  return None
@@ -1,11 +1,17 @@
1
+ from .application import (
2
+ DefaultAuthenticationApplicationService,
3
+ )
1
4
  from .contracts import (
2
5
  IDENTITY_ROLES_MANAGE,
6
+ IDENTITY_USER_EXTENSION,
3
7
  IDENTITY_USERS_MANAGE,
4
8
  AuthenticationApplicationService,
9
+ ChangePasswordCommand,
5
10
  CreateRoleCommand,
6
11
  CreateUserCommand,
7
12
  CurrentUserView,
8
13
  IdentityAccessStore,
14
+ IdentityConcurrencyCommand,
9
15
  IdentityLoginStore,
10
16
  IdentityManagementApplicationService,
11
17
  IdentityOptions,
@@ -19,10 +25,16 @@ from .contracts import (
19
25
  RefreshSessionStore,
20
26
  RefreshTokenCommand,
21
27
  RevokeTokenCommand,
28
+ RolePermissionsView,
22
29
  RoleView,
30
+ SetPasswordCommand,
23
31
  SetRolePermissionsCommand,
32
+ SetUserActiveCommand,
24
33
  SetUserRolesCommand,
25
34
  TokenPair,
35
+ UpdateRoleCommand,
36
+ UpdateUserCommand,
37
+ UserRolesView,
26
38
  UserView,
27
39
  )
28
40
  from .errors import (
@@ -31,6 +43,7 @@ from .errors import (
31
43
  IdentityResourceNotFoundError,
32
44
  InvalidCredentialsError,
33
45
  )
46
+ from .management import DefaultIdentityManagementApplicationService
34
47
  from .module import IdentityModule
35
48
  from .passwords import PasswordHasher
36
49
  from .services import IdentityAuthenticator
@@ -39,12 +52,17 @@ from .tokens import JwtTokenService
39
52
  __all__ = (
40
53
  "IDENTITY_ROLES_MANAGE",
41
54
  "IDENTITY_USERS_MANAGE",
55
+ "IDENTITY_USER_EXTENSION",
42
56
  "AuthenticationApplicationService",
57
+ "ChangePasswordCommand",
43
58
  "CreateRoleCommand",
44
59
  "CreateUserCommand",
45
60
  "CurrentUserView",
61
+ "DefaultAuthenticationApplicationService",
62
+ "DefaultIdentityManagementApplicationService",
46
63
  "IdentityAccessStore",
47
64
  "IdentityAuthenticator",
65
+ "IdentityConcurrencyCommand",
48
66
  "IdentityConflictError",
49
67
  "IdentityDefinitionError",
50
68
  "IdentityLoginStore",
@@ -65,9 +83,15 @@ __all__ = (
65
83
  "RefreshSessionStore",
66
84
  "RefreshTokenCommand",
67
85
  "RevokeTokenCommand",
86
+ "RolePermissionsView",
68
87
  "RoleView",
88
+ "SetPasswordCommand",
69
89
  "SetRolePermissionsCommand",
90
+ "SetUserActiveCommand",
70
91
  "SetUserRolesCommand",
71
92
  "TokenPair",
93
+ "UpdateRoleCommand",
94
+ "UpdateUserCommand",
95
+ "UserRolesView",
72
96
  "UserView",
73
97
  )
@@ -2,27 +2,23 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from datetime import UTC, datetime, timedelta
5
+ from datetime import UTC, datetime
6
6
  from uuid import UUID, uuid4
7
7
 
8
- from ..application_services import ApplicationService, DataSeedContributor, application_service
8
+ from ..application_services import ApplicationService, application_service
9
9
  from ..authorization import (
10
10
  CurrentUser,
11
11
  PermissionCatalog,
12
- PermissionName,
13
12
  allow_anonymous,
14
13
  authorize,
15
14
  )
16
15
  from ..options import Options
16
+ from ..seeding.contracts import DataSeedContributor
17
17
  from .contracts import (
18
- IDENTITY_ROLES_MANAGE,
19
- IDENTITY_USERS_MANAGE,
20
18
  AuthenticationApplicationService,
21
- CreateRoleCommand,
22
- CreateUserCommand,
19
+ ChangePasswordCommand,
23
20
  CurrentUserView,
24
21
  IdentityLoginStore,
25
- IdentityManagementApplicationService,
26
22
  IdentityOptions,
27
23
  IdentityRepository,
28
24
  IdentityRole,
@@ -33,34 +29,24 @@ from .contracts import (
33
29
  RefreshSessionStore,
34
30
  RefreshTokenCommand,
35
31
  RevokeTokenCommand,
36
- RoleView,
37
- SetRolePermissionsCommand,
38
- SetUserRolesCommand,
39
32
  TokenPair,
40
- UserView,
41
33
  _normalize_name,
42
34
  )
43
35
  from .errors import (
44
- IdentityConflictError,
45
- IdentityResourceNotFoundError,
46
36
  InvalidCredentialsError,
47
37
  )
48
38
  from .passwords import PasswordHasher
49
39
  from .tokens import (
50
- ACCESS_TOKEN_LIFETIME,
51
40
  BEARER_TOKEN_TYPE,
52
41
  JwtTokenService,
53
42
  hash_refresh_token,
54
43
  new_refresh_token,
55
44
  )
56
45
 
57
- FAILED_ATTEMPT_LIMIT = 5
58
- LOCKOUT_DURATION = timedelta(minutes=15)
59
-
60
46
 
61
47
  @authorize()
62
48
  @application_service(provides=AuthenticationApplicationService)
63
- class _AuthenticationApplicationService(ApplicationService):
49
+ class DefaultAuthenticationApplicationService(ApplicationService):
64
50
  def __init__(
65
51
  self,
66
52
  login_store: IdentityLoginStore,
@@ -68,12 +54,16 @@ class _AuthenticationApplicationService(ApplicationService):
68
54
  passwords: PasswordHasher,
69
55
  tokens: JwtTokenService,
70
56
  current_user: CurrentUser,
57
+ repository: IdentityRepository,
58
+ options: Options[IdentityOptions],
71
59
  ) -> None:
72
60
  self._login_store = login_store
73
61
  self._sessions = sessions
74
62
  self._passwords = passwords
75
63
  self._tokens = tokens
76
64
  self._current_user = current_user
65
+ self._repository = repository
66
+ self._options = options.value
77
67
 
78
68
  @allow_anonymous
79
69
  async def login(self, command: LoginCommand) -> TokenPair:
@@ -95,8 +85,8 @@ class _AuthenticationApplicationService(ApplicationService):
95
85
  await self._login_store.record_failure(
96
86
  user.user_id,
97
87
  now=now,
98
- attempt_limit=FAILED_ATTEMPT_LIMIT,
99
- lockout_duration_seconds=int(LOCKOUT_DURATION.total_seconds()),
88
+ attempt_limit=self._options.failed_attempt_limit,
89
+ lockout_duration_seconds=int(self._options.lockout_duration.total_seconds()),
100
90
  )
101
91
  raise InvalidCredentialsError
102
92
 
@@ -146,6 +136,28 @@ class _AuthenticationApplicationService(ApplicationService):
146
136
  user_id=self._current_user.user_id,
147
137
  session_id=self._current_user.session_id,
148
138
  username=self._current_user.username,
139
+ concurrency_version=(
140
+ await self._repository.get_user(self._current_user.user_id)
141
+ ).concurrency_version,
142
+ )
143
+
144
+ async def change_password(self, command: ChangePasswordCommand) -> None:
145
+ user_id = self._current_user.user_id
146
+ if user_id is None:
147
+ raise InvalidCredentialsError
148
+ user = await self._repository.get_user(user_id)
149
+ verified, _ = await self._passwords.verify(
150
+ command.old_password.get_secret_value(), user.password_hash
151
+ )
152
+ if not verified:
153
+ raise InvalidCredentialsError
154
+ password_hash = await self._passwords.hash(command.new_password.get_secret_value())
155
+ # Hash 在线程计算;写入时重新核对版本及旧 hash,拒绝计算期间的凭据竞争。
156
+ await self._repository.set_password(
157
+ user_id,
158
+ password_hash,
159
+ command.concurrency_version,
160
+ expected_password_hash=user.password_hash,
149
161
  )
150
162
 
151
163
  def _token_pair(
@@ -159,77 +171,10 @@ class _AuthenticationApplicationService(ApplicationService):
159
171
  access_token=self._tokens.issue(session.user_id, session.session_id, now=now),
160
172
  refresh_token=refresh_token,
161
173
  token_type=BEARER_TOKEN_TYPE,
162
- expires_in=int(ACCESS_TOKEN_LIFETIME.total_seconds()),
163
- )
164
-
165
-
166
- @application_service(provides=IdentityManagementApplicationService)
167
- class _IdentityManagementApplicationService(ApplicationService):
168
- def __init__(
169
- self,
170
- repository: IdentityRepository,
171
- passwords: PasswordHasher,
172
- permissions: PermissionCatalog,
173
- ) -> None:
174
- self._repository = repository
175
- self._passwords = passwords
176
- self._permissions = permissions
177
-
178
- @authorize(IDENTITY_USERS_MANAGE)
179
- async def create_user(self, command: CreateUserCommand) -> UserView:
180
- normalized = _normalize_name(command.username, role="username")
181
- if await self._repository.find_user_by_normalized_username(normalized) is not None:
182
- raise IdentityConflictError(resource="user")
183
- user = IdentityUser(
184
- user_id=uuid4(),
185
- username=command.username.strip(),
186
- normalized_username=normalized,
187
- password_hash=await self._passwords.hash(command.password.get_secret_value()),
188
- is_active=True,
189
- failed_access_count=0,
190
- lockout_end=None,
191
- permission_version=0,
174
+ expires_in=int(self._options.access_token_lifetime.total_seconds()),
192
175
  )
193
- await self._repository.create_user(user)
194
- return UserView(user.user_id, user.username, user.is_active, ())
195
176
 
196
- @authorize(IDENTITY_USERS_MANAGE)
197
- async def get_users(self) -> tuple[UserView, ...]:
198
- return await self._repository.list_users()
199
177
 
200
- @authorize(IDENTITY_ROLES_MANAGE)
201
- async def create_role(self, command: CreateRoleCommand) -> RoleView:
202
- normalized = _normalize_name(command.name, role="role name")
203
- if await self._repository.find_role_by_normalized_name(normalized) is not None:
204
- raise IdentityConflictError(resource="role")
205
- role = IdentityRole(uuid4(), command.name.strip(), normalized, 0)
206
- await self._repository.create_role(role)
207
- return RoleView(role.role_id, role.name, ())
208
-
209
- @authorize(IDENTITY_ROLES_MANAGE)
210
- async def get_roles(self) -> tuple[RoleView, ...]:
211
- return await self._repository.list_roles()
212
-
213
- @authorize(IDENTITY_USERS_MANAGE, IDENTITY_ROLES_MANAGE)
214
- async def set_user_roles(self, user_id: UUID, command: SetUserRolesCommand) -> None:
215
- role_ids = tuple(dict.fromkeys(command.role_ids))
216
- await self._repository.set_user_roles(user_id, role_ids)
217
-
218
- @authorize(IDENTITY_ROLES_MANAGE)
219
- async def set_role_permissions(self, role_id: UUID, command: SetRolePermissionsCommand) -> None:
220
- names = tuple(PermissionName(item) for item in command.permissions)
221
- if len(set(names)) != len(names):
222
- raise IdentityConflictError(resource="role permission")
223
- unknown = tuple(item for item in names if item not in self._permissions)
224
- if unknown:
225
- raise IdentityResourceNotFoundError(resource="permission")
226
- await self._repository.set_role_permissions(
227
- role_id,
228
- tuple(item.value for item in names),
229
- )
230
-
231
-
232
- @application_service()
233
178
  class IdentityAdminSeedContributor(DataSeedContributor):
234
179
  def __init__(
235
180
  self,
@@ -270,8 +215,11 @@ class IdentityAdminSeedContributor(DataSeedContributor):
270
215
  await self._repository.set_role_permissions(
271
216
  role.role_id,
272
217
  tuple(item.name.value for item in self._permissions.definitions),
218
+ role.concurrency_version,
219
+ )
220
+ await self._repository.set_user_roles(
221
+ user.user_id, (role.role_id,), user.concurrency_version
273
222
  )
274
- await self._repository.set_user_roles(user.user_id, (role.role_id,))
275
223
 
276
224
 
277
225
  def _new_session(user_id: UUID, refresh_token: str, *, now: datetime) -> RefreshSession: