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
@@ -3,20 +3,22 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Mapping
6
- from dataclasses import dataclass
7
- from datetime import datetime
6
+ from dataclasses import dataclass, field, replace
7
+ from datetime import datetime, timedelta
8
8
  from enum import Enum
9
9
  from typing import Annotated, Protocol
10
10
  from uuid import UUID
11
11
 
12
12
  from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator
13
13
 
14
- from ..application_services import ApplicationServiceContract
14
+ from ..application_services import ApplicationServiceContract, PagedResult, PagedResultRequestDto
15
15
  from ..authorization import (
16
16
  CurrentUser,
17
17
  PermissionName,
18
18
  RolePermissionVersion,
19
19
  )
20
+ from ..domain import OptimisticConcurrencyError
21
+ from ..extensions.contracts import ExtensibleModel, ExtensionProperties, ExtensionTarget
20
22
  from ..options import BaseOptions
21
23
 
22
24
  IDENTITY_USERS_MANAGE = PermissionName("identity.users.manage")
@@ -28,6 +30,11 @@ class IdentityOptions(BaseOptions):
28
30
  audience: str
29
31
  active_kid: str
30
32
  keys: Mapping[str, SecretStr]
33
+ access_token_lifetime: timedelta = Field(default=timedelta(minutes=15), ge=timedelta(seconds=1))
34
+ refresh_inactivity_lifetime: timedelta = Field(default=timedelta(days=30), gt=timedelta(0))
35
+ refresh_absolute_lifetime: timedelta = Field(default=timedelta(days=90), gt=timedelta(0))
36
+ failed_attempt_limit: int = Field(default=5, ge=1)
37
+ lockout_duration: timedelta = Field(default=timedelta(minutes=15), ge=timedelta(seconds=1))
31
38
  seed_admin_username: str | None = None
32
39
  seed_admin_password: SecretStr | None = None
33
40
 
@@ -80,26 +87,80 @@ class RevokeTokenCommand(BaseModel):
80
87
  refresh_token: SecretStr
81
88
 
82
89
 
83
- class CreateUserCommand(BaseModel):
90
+ class CreateUserCommand(ExtensibleModel):
84
91
  model_config = ConfigDict(frozen=True)
85
92
 
86
93
  username: Annotated[str, Field(min_length=1, max_length=120)]
87
94
  password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
88
95
 
96
+ @field_validator("username")
97
+ @classmethod
98
+ def _validate_username(cls, value: str) -> str:
99
+ _normalize_name(value, role="username")
100
+ return value.strip()
101
+
89
102
 
90
103
  class CreateRoleCommand(BaseModel):
91
104
  model_config = ConfigDict(frozen=True)
92
105
 
93
106
  name: Annotated[str, Field(min_length=1, max_length=120)]
94
107
 
108
+ @field_validator("name")
109
+ @classmethod
110
+ def _validate_name(cls, value: str) -> str:
111
+ _normalize_name(value, role="role name")
112
+ return value.strip()
113
+
114
+
115
+ class IdentityConcurrencyCommand(BaseModel):
116
+ model_config = ConfigDict(frozen=True)
117
+
118
+ concurrency_version: Annotated[int, Field(ge=0)]
119
+
120
+
121
+ class UpdateUserCommand(ExtensibleModel):
122
+ model_config = ConfigDict(frozen=True)
123
+
124
+ username: Annotated[str, Field(min_length=1, max_length=120)]
125
+ concurrency_version: Annotated[int, Field(ge=0)]
126
+
127
+ @field_validator("username")
128
+ @classmethod
129
+ def _validate_username(cls, value: str) -> str:
130
+ _normalize_name(value, role="username")
131
+ return value.strip()
132
+
133
+
134
+ class SetUserActiveCommand(IdentityConcurrencyCommand):
135
+ is_active: bool
136
+
137
+
138
+ class UpdateRoleCommand(IdentityConcurrencyCommand):
139
+ name: Annotated[str, Field(min_length=1, max_length=120)]
140
+
141
+ @field_validator("name")
142
+ @classmethod
143
+ def _validate_name(cls, value: str) -> str:
144
+ _normalize_name(value, role="role name")
145
+ return value.strip()
146
+
147
+
148
+ class ChangePasswordCommand(IdentityConcurrencyCommand):
149
+ old_password: SecretStr
150
+ new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
95
151
 
96
- class SetUserRolesCommand(BaseModel):
152
+
153
+ class SetPasswordCommand(IdentityConcurrencyCommand):
154
+ new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
155
+
156
+
157
+ class SetUserRolesCommand(IdentityConcurrencyCommand):
97
158
  model_config = ConfigDict(frozen=True)
98
159
 
99
160
  role_ids: tuple[UUID, ...]
100
161
 
101
162
 
102
- class SetRolePermissionsCommand(BaseModel):
163
+ class SetRolePermissionsCommand(IdentityConcurrencyCommand):
103
164
  model_config = ConfigDict(frozen=True)
104
165
 
105
166
  permissions: tuple[str, ...]
@@ -113,12 +174,12 @@ class TokenPair:
113
174
  expires_in: int
114
175
 
115
176
 
116
- @dataclass(frozen=True, slots=True)
117
- class UserView:
177
+ class UserView(ExtensibleModel):
118
178
  user_id: UUID
119
179
  username: str
120
180
  is_active: bool
121
181
  role_ids: tuple[UUID, ...]
182
+ concurrency_version: int
122
183
 
123
184
 
124
185
  @dataclass(frozen=True, slots=True)
@@ -126,6 +187,17 @@ class RoleView:
126
187
  role_id: UUID
127
188
  name: str
128
189
  permissions: tuple[str, ...]
190
+ concurrency_version: int
191
+
192
+
193
+ class UserRolesView(BaseModel):
194
+ role_ids: tuple[UUID, ...]
195
+ concurrency_version: int
196
+
197
+
198
+ class RolePermissionsView(BaseModel):
199
+ permissions: tuple[str, ...]
200
+ concurrency_version: int
129
201
 
130
202
 
131
203
  @dataclass(frozen=True, slots=True)
@@ -133,6 +205,7 @@ class CurrentUserView:
133
205
  user_id: UUID
134
206
  session_id: UUID
135
207
  username: str
208
+ concurrency_version: int
136
209
 
137
210
 
138
211
  @dataclass(frozen=True, slots=True)
@@ -145,6 +218,30 @@ class IdentityUser:
145
218
  failed_access_count: int
146
219
  lockout_end: datetime | None
147
220
  permission_version: int
221
+ extra_properties: ExtensionProperties = field(default_factory=ExtensionProperties)
222
+ concurrency_version: int = 0
223
+
224
+ def renamed(self, username: str, expected_version: int) -> IdentityUser:
225
+ _check_version(self.concurrency_version, expected_version)
226
+ return replace(
227
+ self,
228
+ username=username.strip(),
229
+ normalized_username=_normalize_name(username, role="username"),
230
+ concurrency_version=self.concurrency_version + 1,
231
+ )
232
+
233
+ def with_active(self, is_active: bool, expected_version: int) -> IdentityUser:
234
+ _check_version(self.concurrency_version, expected_version)
235
+ return replace(
236
+ self,
237
+ is_active=is_active,
238
+ concurrency_version=self.concurrency_version + (self.is_active != is_active),
239
+ )
240
+
241
+
242
+ IDENTITY_USER_EXTENSION = ExtensionTarget(
243
+ "identity.user", (CreateUserCommand, UpdateUserCommand, UserView), IdentityUser
244
+ )
148
245
 
149
246
 
150
247
  @dataclass(frozen=True, slots=True)
@@ -153,6 +250,16 @@ class IdentityRole:
153
250
  name: str
154
251
  normalized_name: str
155
252
  permission_version: int
253
+ concurrency_version: int = 0
254
+
255
+ def renamed(self, name: str, expected_version: int) -> IdentityRole:
256
+ _check_version(self.concurrency_version, expected_version)
257
+ return replace(
258
+ self,
259
+ name=name.strip(),
260
+ normalized_name=_normalize_name(name, role="role name"),
261
+ concurrency_version=self.concurrency_version + 1,
262
+ )
156
263
 
157
264
 
158
265
  @dataclass(frozen=True, slots=True)
@@ -188,17 +295,46 @@ class IdentityRepository(Protocol):
188
295
 
189
296
  async def create_user(self, user: IdentityUser) -> None: ...
190
297
 
191
- async def list_users(self) -> tuple[UserView, ...]: ...
298
+ async def get_user(self, user_id: UUID) -> IdentityUser: ...
299
+
300
+ async def get_user_view(self, user_id: UUID) -> UserView: ...
301
+
302
+ async def list_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]: ...
303
+
304
+ async def update_user(self, user_id: UUID, command: UpdateUserCommand) -> UserView: ...
305
+
306
+ async def set_user_active(self, user_id: UUID, command: SetUserActiveCommand) -> UserView: ...
307
+
308
+ async def delete_user(self, user_id: UUID, concurrency_version: int) -> None: ...
309
+
310
+ async def set_password(
311
+ self,
312
+ user_id: UUID,
313
+ password_hash: str,
314
+ concurrency_version: int,
315
+ *,
316
+ expected_password_hash: str | None = None,
317
+ ) -> None: ...
192
318
 
193
319
  async def create_role(self, role: IdentityRole) -> None: ...
194
320
 
195
321
  async def find_role_by_normalized_name(self, normalized_name: str) -> IdentityRole | None: ...
196
322
 
197
- async def list_roles(self) -> tuple[RoleView, ...]: ...
323
+ async def get_role(self, role_id: UUID) -> RoleView: ...
324
+
325
+ async def list_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]: ...
326
+
327
+ async def update_role(self, role_id: UUID, command: UpdateRoleCommand) -> RoleView: ...
198
328
 
199
- async def set_user_roles(self, user_id: UUID, role_ids: tuple[UUID, ...]) -> None: ...
329
+ async def delete_role(self, role_id: UUID, concurrency_version: int) -> None: ...
200
330
 
201
- async def set_role_permissions(self, role_id: UUID, permissions: tuple[str, ...]) -> None: ...
331
+ async def set_user_roles(
332
+ self, user_id: UUID, role_ids: tuple[UUID, ...], concurrency_version: int
333
+ ) -> UserRolesView: ...
334
+
335
+ async def set_role_permissions(
336
+ self, role_id: UUID, permissions: tuple[str, ...], concurrency_version: int
337
+ ) -> RolePermissionsView: ...
202
338
 
203
339
 
204
340
  class IdentityAccessStore(Protocol):
@@ -259,21 +395,45 @@ class AuthenticationApplicationService(ApplicationServiceContract, Protocol):
259
395
 
260
396
  async def me(self) -> CurrentUserView: ...
261
397
 
398
+ async def change_password(self, command: ChangePasswordCommand) -> None: ...
399
+
262
400
 
263
401
  class IdentityManagementApplicationService(ApplicationServiceContract, Protocol):
264
402
  async def create_user(self, command: CreateUserCommand) -> UserView: ...
265
403
 
266
- async def get_users(self) -> tuple[UserView, ...]: ...
404
+ async def get_user(self, user_id: UUID) -> UserView: ...
405
+
406
+ async def get_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]: ...
407
+
408
+ async def update_user(self, user_id: UUID, command: UpdateUserCommand) -> UserView: ...
409
+
410
+ async def set_user_active(self, user_id: UUID, command: SetUserActiveCommand) -> UserView: ...
411
+
412
+ async def delete_user(self, user_id: UUID, command: IdentityConcurrencyCommand) -> None: ...
413
+
414
+ async def set_password(self, user_id: UUID, command: SetPasswordCommand) -> None: ...
267
415
 
268
416
  async def create_role(self, command: CreateRoleCommand) -> RoleView: ...
269
417
 
270
- async def get_roles(self) -> tuple[RoleView, ...]: ...
418
+ async def get_role(self, role_id: UUID) -> RoleView: ...
271
419
 
272
- async def set_user_roles(self, user_id: UUID, command: SetUserRolesCommand) -> None: ...
420
+ async def get_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]: ...
421
+
422
+ async def update_role(self, role_id: UUID, command: UpdateRoleCommand) -> RoleView: ...
423
+
424
+ async def delete_role(self, role_id: UUID, command: IdentityConcurrencyCommand) -> None: ...
425
+
426
+ async def get_user_roles(self, user_id: UUID) -> UserRolesView: ...
427
+
428
+ async def get_role_permissions(self, role_id: UUID) -> RolePermissionsView: ...
429
+
430
+ async def set_user_roles(
431
+ self, user_id: UUID, command: SetUserRolesCommand
432
+ ) -> UserRolesView: ...
273
433
 
274
434
  async def set_role_permissions(
275
435
  self, role_id: UUID, command: SetRolePermissionsCommand
276
- ) -> None: ...
436
+ ) -> RolePermissionsView: ...
277
437
 
278
438
 
279
439
  def _normalize_name(value: str, *, role: str) -> str:
@@ -281,3 +441,8 @@ def _normalize_name(value: str, *, role: str) -> str:
281
441
  if not normalized or len(normalized) > 120:
282
442
  raise ValueError(f"{role} must contain between 1 and 120 characters")
283
443
  return normalized
444
+
445
+
446
+ def _check_version(actual: int, expected: int) -> None:
447
+ if actual != expected:
448
+ raise OptimisticConcurrencyError
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from ..fastapi import ApplicationServiceRouteOverride, exposes_application_services
5
+ from ..fastapi import ApplicationServiceRouteOverride, FastApiModule, exposes_application_services
6
6
  from ..modularity import AppModule
7
7
  from .contracts import AuthenticationApplicationService, IdentityManagementApplicationService
8
8
  from .module import IdentityModule
@@ -12,6 +12,83 @@ from .module import IdentityModule
12
12
  IdentityModule,
13
13
  root_path="identity",
14
14
  overrides=(
15
+ ApplicationServiceRouteOverride(
16
+ AuthenticationApplicationService,
17
+ method=AuthenticationApplicationService.change_password,
18
+ path="/auth/password",
19
+ verb="PUT",
20
+ operation_id="identity_change_password",
21
+ ),
22
+ ApplicationServiceRouteOverride(
23
+ IdentityManagementApplicationService,
24
+ method=IdentityManagementApplicationService.get_user,
25
+ path="/identity/users/{user_id}",
26
+ verb="GET",
27
+ operation_id="identity_get_user",
28
+ ),
29
+ ApplicationServiceRouteOverride(
30
+ IdentityManagementApplicationService,
31
+ method=IdentityManagementApplicationService.update_user,
32
+ path="/identity/users/{user_id}",
33
+ verb="PUT",
34
+ operation_id="identity_update_user",
35
+ ),
36
+ ApplicationServiceRouteOverride(
37
+ IdentityManagementApplicationService,
38
+ method=IdentityManagementApplicationService.delete_user,
39
+ path="/identity/users/{user_id}",
40
+ verb="DELETE",
41
+ operation_id="identity_delete_user",
42
+ ),
43
+ ApplicationServiceRouteOverride(
44
+ IdentityManagementApplicationService,
45
+ method=IdentityManagementApplicationService.set_user_active,
46
+ path="/identity/users/{user_id}/active",
47
+ verb="PUT",
48
+ operation_id="identity_set_user_active",
49
+ ),
50
+ ApplicationServiceRouteOverride(
51
+ IdentityManagementApplicationService,
52
+ method=IdentityManagementApplicationService.set_password,
53
+ path="/identity/users/{user_id}/password",
54
+ verb="PUT",
55
+ operation_id="identity_set_password",
56
+ ),
57
+ ApplicationServiceRouteOverride(
58
+ IdentityManagementApplicationService,
59
+ method=IdentityManagementApplicationService.get_user_roles,
60
+ path="/identity/users/{user_id}/roles",
61
+ verb="GET",
62
+ operation_id="identity_get_user_roles",
63
+ ),
64
+ ApplicationServiceRouteOverride(
65
+ IdentityManagementApplicationService,
66
+ method=IdentityManagementApplicationService.get_role,
67
+ path="/identity/roles/{role_id}",
68
+ verb="GET",
69
+ operation_id="identity_get_role",
70
+ ),
71
+ ApplicationServiceRouteOverride(
72
+ IdentityManagementApplicationService,
73
+ method=IdentityManagementApplicationService.update_role,
74
+ path="/identity/roles/{role_id}",
75
+ verb="PUT",
76
+ operation_id="identity_update_role",
77
+ ),
78
+ ApplicationServiceRouteOverride(
79
+ IdentityManagementApplicationService,
80
+ method=IdentityManagementApplicationService.delete_role,
81
+ path="/identity/roles/{role_id}",
82
+ verb="DELETE",
83
+ operation_id="identity_delete_role",
84
+ ),
85
+ ApplicationServiceRouteOverride(
86
+ IdentityManagementApplicationService,
87
+ method=IdentityManagementApplicationService.get_role_permissions,
88
+ path="/identity/roles/{role_id}/permissions",
89
+ verb="GET",
90
+ operation_id="identity_get_role_permissions",
91
+ ),
15
92
  ApplicationServiceRouteOverride(
16
93
  AuthenticationApplicationService,
17
94
  method=AuthenticationApplicationService.login,
@@ -87,4 +164,4 @@ from .module import IdentityModule
87
164
  ),
88
165
  )
89
166
  class IdentityHttpApiModule(AppModule):
90
- dependencies = (IdentityModule,)
167
+ dependencies = (FastApiModule,)
@@ -0,0 +1,166 @@
1
+ """Identity 管理用例;持久化并发与事务由 repository/UoW 负责。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID, uuid4
6
+
7
+ from ..application_services import (
8
+ ApplicationService,
9
+ PagedResult,
10
+ PagedResultRequestDto,
11
+ application_service,
12
+ )
13
+ from ..authorization import (
14
+ PermissionCatalog,
15
+ PermissionName,
16
+ authorize,
17
+ )
18
+ from .contracts import (
19
+ IDENTITY_ROLES_MANAGE,
20
+ IDENTITY_USERS_MANAGE,
21
+ CreateRoleCommand,
22
+ CreateUserCommand,
23
+ IdentityConcurrencyCommand,
24
+ IdentityManagementApplicationService,
25
+ IdentityRepository,
26
+ IdentityRole,
27
+ IdentityUser,
28
+ RolePermissionsView,
29
+ RoleView,
30
+ SetPasswordCommand,
31
+ SetRolePermissionsCommand,
32
+ SetUserActiveCommand,
33
+ SetUserRolesCommand,
34
+ UpdateRoleCommand,
35
+ UpdateUserCommand,
36
+ UserRolesView,
37
+ UserView,
38
+ _normalize_name,
39
+ )
40
+ from .errors import (
41
+ IdentityConflictError,
42
+ IdentityResourceNotFoundError,
43
+ )
44
+ from .passwords import PasswordHasher
45
+
46
+
47
+ @application_service(provides=IdentityManagementApplicationService)
48
+ class DefaultIdentityManagementApplicationService(ApplicationService):
49
+ def __init__(
50
+ self,
51
+ repository: IdentityRepository,
52
+ passwords: PasswordHasher,
53
+ permissions: PermissionCatalog,
54
+ ) -> None:
55
+ self._repository = repository
56
+ self._passwords = passwords
57
+ self._permissions = permissions
58
+
59
+ @authorize(IDENTITY_USERS_MANAGE)
60
+ async def create_user(self, command: CreateUserCommand) -> UserView:
61
+ normalized = _normalize_name(command.username, role="username")
62
+ if await self._repository.find_user_by_normalized_username(normalized) is not None:
63
+ raise IdentityConflictError(resource="user")
64
+ user = IdentityUser(
65
+ user_id=uuid4(),
66
+ username=command.username.strip(),
67
+ normalized_username=normalized,
68
+ password_hash=await self._passwords.hash(command.password.get_secret_value()),
69
+ is_active=True,
70
+ failed_access_count=0,
71
+ lockout_end=None,
72
+ permission_version=0,
73
+ extra_properties=command.extra_properties,
74
+ )
75
+ await self._repository.create_user(user)
76
+ return UserView(
77
+ user_id=user.user_id,
78
+ username=user.username,
79
+ is_active=user.is_active,
80
+ role_ids=(),
81
+ extra_properties=user.extra_properties,
82
+ concurrency_version=user.concurrency_version,
83
+ )
84
+
85
+ @authorize(IDENTITY_USERS_MANAGE)
86
+ async def get_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]:
87
+ return await self._repository.list_users(query)
88
+
89
+ @authorize(IDENTITY_USERS_MANAGE)
90
+ async def get_user(self, user_id: UUID) -> UserView:
91
+ return await self._repository.get_user_view(user_id)
92
+
93
+ @authorize(IDENTITY_USERS_MANAGE)
94
+ async def update_user(self, user_id: UUID, command: UpdateUserCommand) -> UserView:
95
+ return await self._repository.update_user(user_id, command)
96
+
97
+ @authorize(IDENTITY_USERS_MANAGE)
98
+ async def set_user_active(self, user_id: UUID, command: SetUserActiveCommand) -> UserView:
99
+ return await self._repository.set_user_active(user_id, command)
100
+
101
+ @authorize(IDENTITY_USERS_MANAGE)
102
+ async def delete_user(self, user_id: UUID, command: IdentityConcurrencyCommand) -> None:
103
+ await self._repository.delete_user(user_id, command.concurrency_version)
104
+
105
+ @authorize(IDENTITY_USERS_MANAGE)
106
+ async def set_password(self, user_id: UUID, command: SetPasswordCommand) -> None:
107
+ password_hash = await self._passwords.hash(command.new_password.get_secret_value())
108
+ await self._repository.set_password(user_id, password_hash, command.concurrency_version)
109
+
110
+ @authorize(IDENTITY_ROLES_MANAGE)
111
+ async def create_role(self, command: CreateRoleCommand) -> RoleView:
112
+ normalized = _normalize_name(command.name, role="role name")
113
+ if await self._repository.find_role_by_normalized_name(normalized) is not None:
114
+ raise IdentityConflictError(resource="role")
115
+ role = IdentityRole(uuid4(), command.name.strip(), normalized, 0)
116
+ await self._repository.create_role(role)
117
+ return RoleView(role.role_id, role.name, (), role.concurrency_version)
118
+
119
+ @authorize(IDENTITY_ROLES_MANAGE)
120
+ async def get_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]:
121
+ return await self._repository.list_roles(query)
122
+
123
+ @authorize(IDENTITY_ROLES_MANAGE)
124
+ async def get_role(self, role_id: UUID) -> RoleView:
125
+ return await self._repository.get_role(role_id)
126
+
127
+ @authorize(IDENTITY_ROLES_MANAGE)
128
+ async def update_role(self, role_id: UUID, command: UpdateRoleCommand) -> RoleView:
129
+ return await self._repository.update_role(role_id, command)
130
+
131
+ @authorize(IDENTITY_ROLES_MANAGE)
132
+ async def delete_role(self, role_id: UUID, command: IdentityConcurrencyCommand) -> None:
133
+ await self._repository.delete_role(role_id, command.concurrency_version)
134
+
135
+ @authorize(IDENTITY_USERS_MANAGE, IDENTITY_ROLES_MANAGE)
136
+ async def get_user_roles(self, user_id: UUID) -> UserRolesView:
137
+ user = await self._repository.get_user_view(user_id)
138
+ return UserRolesView(role_ids=user.role_ids, concurrency_version=user.concurrency_version)
139
+
140
+ @authorize(IDENTITY_ROLES_MANAGE)
141
+ async def get_role_permissions(self, role_id: UUID) -> RolePermissionsView:
142
+ role = await self._repository.get_role(role_id)
143
+ return RolePermissionsView(
144
+ permissions=role.permissions, concurrency_version=role.concurrency_version
145
+ )
146
+
147
+ @authorize(IDENTITY_USERS_MANAGE, IDENTITY_ROLES_MANAGE)
148
+ async def set_user_roles(self, user_id: UUID, command: SetUserRolesCommand) -> UserRolesView:
149
+ role_ids = tuple(dict.fromkeys(command.role_ids))
150
+ return await self._repository.set_user_roles(user_id, role_ids, command.concurrency_version)
151
+
152
+ @authorize(IDENTITY_ROLES_MANAGE)
153
+ async def set_role_permissions(
154
+ self, role_id: UUID, command: SetRolePermissionsCommand
155
+ ) -> RolePermissionsView:
156
+ names = tuple(PermissionName(item) for item in command.permissions)
157
+ if len(set(names)) != len(names):
158
+ raise IdentityConflictError(resource="role permission")
159
+ unknown = tuple(item for item in names if item not in self._permissions)
160
+ if unknown:
161
+ raise IdentityResourceNotFoundError(resource="permission")
162
+ return await self._repository.set_role_permissions(
163
+ role_id,
164
+ tuple(item.value for item in names),
165
+ command.concurrency_version,
166
+ )
@@ -5,14 +5,17 @@ from __future__ import annotations
5
5
  from dishka import Provider, Scope
6
6
 
7
7
  from ..authorization import AccessTokenAuthenticator, PermissionChecker
8
+ from ..caching.module import CachingModule
9
+ from ..extensions.contracts import ExtensionPoints
8
10
  from ..lifecycle import ConfigureContext
9
- from ..modularity import AppModule
11
+ from ..modularity import AppModule, ModuleRef
12
+ from ..seeding.module import DataSeedingModule
10
13
  from .contracts import (
14
+ IDENTITY_USER_EXTENSION,
11
15
  IdentityOptions,
12
16
  )
13
17
  from .passwords import PasswordHasher
14
18
  from .services import (
15
- IDENTITY_PERMISSION_CACHE,
16
19
  IdentityAuthenticator,
17
20
  IdentityPermissionChecker,
18
21
  )
@@ -20,13 +23,18 @@ from .tokens import JwtTokenService
20
23
 
21
24
 
22
25
  class IdentityModule(AppModule):
26
+ extension_points = ExtensionPoints((IDENTITY_USER_EXTENSION,))
27
+ dependencies = (
28
+ DataSeedingModule,
29
+ CachingModule,
30
+ ModuleRef("python_ddd_framework.application_services.module:ApplicationServicesModule"),
31
+ )
23
32
  scan_packages = (
24
33
  "python_ddd_framework.identity.application",
34
+ "python_ddd_framework.identity.management",
25
35
  "python_ddd_framework.identity.permissions",
26
36
  )
27
37
 
28
- cache_definitions = (IDENTITY_PERMISSION_CACHE,)
29
-
30
38
  def configure(self, context: ConfigureContext) -> None:
31
39
  context.configure(
32
40
  IdentityOptions,