python-ddd-framework 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. python_ddd_framework/__init__.py +139 -21
  2. python_ddd_framework/application/builder.py +23 -78
  3. python_ddd_framework/application/composition.py +4 -22
  4. python_ddd_framework/application/runtime.py +96 -291
  5. python_ddd_framework/application/state.py +7 -75
  6. python_ddd_framework/application_services/__init__.py +5 -21
  7. python_ddd_framework/{services/application_bindings.py → application_services/bindings.py} +52 -9
  8. python_ddd_framework/application_services/catalog.py +36 -288
  9. python_ddd_framework/application_services/contracts.py +14 -13
  10. python_ddd_framework/application_services/dispatcher.py +19 -269
  11. python_ddd_framework/application_services/invocation.py +126 -7
  12. python_ddd_framework/application_services/module.py +168 -0
  13. python_ddd_framework/application_services/pagination.py +21 -0
  14. python_ddd_framework/auditing/control.py +1 -1
  15. python_ddd_framework/auditing/module.py +15 -0
  16. python_ddd_framework/authorization/__init__.py +2 -0
  17. python_ddd_framework/authorization/contracts.py +7 -2
  18. python_ddd_framework/authorization/definitions.py +5 -0
  19. python_ddd_framework/authorization/module.py +40 -0
  20. python_ddd_framework/authorization/options.py +7 -0
  21. python_ddd_framework/background_execution/child.py +13 -3
  22. python_ddd_framework/background_execution/declarations.py +45 -0
  23. python_ddd_framework/background_execution/lifecycle.py +12 -0
  24. python_ddd_framework/background_execution/module.py +107 -1
  25. python_ddd_framework/background_jobs/catalog.py +59 -42
  26. python_ddd_framework/background_jobs/contracts.py +16 -19
  27. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +4 -0
  28. python_ddd_framework/background_jobs/pgqueuer/module.py +2 -1
  29. python_ddd_framework/background_jobs/pgqueuer/runtime.py +7 -7
  30. python_ddd_framework/background_workers/catalog.py +23 -36
  31. python_ddd_framework/background_workers/contracts.py +3 -0
  32. python_ddd_framework/background_workers/execution.py +1 -1
  33. python_ddd_framework/background_workers/runtime.py +38 -1
  34. python_ddd_framework/caching/__init__.py +7 -6
  35. python_ddd_framework/caching/contracts.py +65 -67
  36. python_ddd_framework/caching/errors.py +0 -7
  37. python_ddd_framework/caching/module.py +18 -0
  38. python_ddd_framework/caching/naming.py +29 -0
  39. python_ddd_framework/caching/options.py +38 -0
  40. python_ddd_framework/caching/unit_of_work.py +46 -0
  41. python_ddd_framework/cli/__init__.py +31 -6
  42. python_ddd_framework/cli/project.py +68 -6
  43. python_ddd_framework/cli/runtime.py +1 -1
  44. python_ddd_framework/developer_kit/generation.py +75 -22
  45. python_ddd_framework/developer_kit/project_metadata.py +2 -4
  46. python_ddd_framework/developer_kit/templates/basic/cookiecutter.json +4 -0
  47. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +45 -0
  48. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/__init__.py.jinja +0 -0
  49. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/__init__.py.jinja +0 -0
  50. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/conversion_service.py.jinja +7 -0
  51. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/module.py.jinja +38 -0
  52. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/__init__.py.jinja +0 -0
  53. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/default_conversion_service.py.jinja +10 -0
  54. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
  55. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +41 -49
  56. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/__init__.py.jinja +1 -0
  57. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +1 -0
  58. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +45 -0
  59. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +8 -0
  60. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +1 -0
  61. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +35 -0
  62. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +5 -0
  63. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +15 -0
  64. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/__init__.py.jinja +1 -0
  65. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +28 -0
  66. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +26 -0
  67. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/__init__.py.jinja +1 -0
  68. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +9 -0
  69. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +9 -0
  70. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/__init__.py.jinja +1 -0
  71. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +23 -0
  72. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/__init__.py.jinja +1 -0
  73. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{integration.py.jinja → hosted_services/order_integration_service.py.jinja} +8 -2
  74. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +16 -0
  75. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/__init__.py.jinja +1 -0
  76. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +16 -0
  77. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/__init__.py.jinja +1 -0
  78. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +17 -0
  79. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +105 -10
  80. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/__init__.py.jinja +1 -0
  81. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{options.py.jinja → options/order_options.py.jinja} +1 -1
  82. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/__init__.py.jinja +1 -0
  83. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +65 -0
  84. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +35 -0
  85. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +41 -0
  86. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +1 -0
  87. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +17 -0
  88. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/__init__.py.jinja +1 -0
  89. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +6 -0
  90. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +7 -0
  91. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/__init__.py.jinja +1 -0
  92. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +7 -0
  93. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +39 -2
  94. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/__init__.py.jinja +1 -0
  95. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +13 -0
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +10 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +12 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/__init__.py.jinja +1 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +9 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +12 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/__init__.py.jinja +1 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{orders.py.jinja → entities/order.py.jinja} +10 -18
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/__init__.py.jinja +1 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +13 -0
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +42 -3
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/__init__.py.jinja +1 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +17 -0
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/__init__.py.jinja +1 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +22 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/__init__.py.jinja +1 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +23 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/__init__.py.jinja +1 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{settings.py.jinja → settings/approval_settings.py.jinja} +3 -2
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/__init__.py.jinja +1 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +10 -0
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/__init__.py.jinja +1 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +3 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/__init__.py.jinja +1 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +6 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/__init__.py.jinja +1 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +11 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/__init__.py.jinja +1 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +26 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +13 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +40 -1
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/__init__.py.jinja +1 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/{permissions.py.jinja → permissions/order_permission_provider.py.jinja} +6 -2
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +7 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/__init__.py.jinja +1 -0
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +18 -0
  131. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/__init__.py.jinja +1 -0
  132. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +15 -0
  133. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/__init__.py.jinja +1 -0
  134. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +5 -0
  135. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +85 -7
  136. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/__init__.py.jinja +1 -0
  137. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/{files.py.jinja → routers/order_files.py.jinja} +23 -20
  138. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/__init__.py.jinja +1 -0
  139. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +44 -0
  140. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -1
  141. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +1 -7
  142. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +7 -0
  143. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/{orders.py.jinja → order_model.py.jinja} +7 -3
  144. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +42 -5
  145. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +79 -0
  146. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/__init__.py.jinja +1 -0
  147. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +4 -3
  148. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +1 -1
  149. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +16 -3
  150. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +29 -22
  151. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{app.development.yaml.jinja → backend/app.development.yaml.jinja} +2 -0
  152. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{pyproject.toml.jinja → backend/pyproject.toml.jinja} +1 -1
  153. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/main.py.jinja +2 -0
  154. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/module.py.jinja +40 -3
  155. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +40 -10
  156. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +134 -23
  157. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/scripts/.gitkeep +0 -0
  158. python_ddd_framework/distributed_lock/contracts.py +4 -1
  159. python_ddd_framework/domain/__init__.py +2 -0
  160. python_ddd_framework/domain/services.py +11 -0
  161. python_ddd_framework/errors/business.py +10 -1
  162. python_ddd_framework/errors/lifecycle.py +3 -31
  163. python_ddd_framework/events/__init__.py +5 -2
  164. python_ddd_framework/events/aggregate.py +3 -3
  165. python_ddd_framework/events/catalog.py +12 -11
  166. python_ddd_framework/events/contracts.py +6 -1
  167. python_ddd_framework/events/contribution.py +1 -52
  168. python_ddd_framework/events/entities.py +23 -0
  169. python_ddd_framework/events/module.py +97 -0
  170. python_ddd_framework/events/runtime.py +80 -29
  171. python_ddd_framework/events/subscription.py +36 -0
  172. python_ddd_framework/events/unit_of_work.py +25 -0
  173. python_ddd_framework/extensions/__init__.py +22 -0
  174. python_ddd_framework/extensions/catalog.py +198 -0
  175. python_ddd_framework/extensions/contracts.py +85 -0
  176. python_ddd_framework/extensions/models.py +142 -0
  177. python_ddd_framework/extensions/module.py +21 -0
  178. python_ddd_framework/fastapi/__init__.py +2 -0
  179. python_ddd_framework/fastapi/action.py +1 -2
  180. python_ddd_framework/fastapi/adapter.py +16 -0
  181. python_ddd_framework/fastapi/application_services.py +43 -25
  182. python_ddd_framework/fastapi/background.py +2 -1
  183. python_ddd_framework/fastapi/contracts.py +26 -8
  184. python_ddd_framework/fastapi/health.py +1 -1
  185. python_ddd_framework/fastapi/module.py +14 -0
  186. python_ddd_framework/fastapi/parameters.py +3 -18
  187. python_ddd_framework/fastapi/realtime/connection.py +10 -7
  188. python_ddd_framework/fastapi/realtime/module.py +3 -0
  189. python_ddd_framework/fastapi/realtime/runtime.py +10 -24
  190. python_ddd_framework/fastapi/request_context.py +2 -1
  191. python_ddd_framework/fastapi/routing.py +2 -1
  192. python_ddd_framework/fastapi/server.py +8 -0
  193. python_ddd_framework/fastapi/settings.py +2 -1
  194. python_ddd_framework/hosted_services/bridge.py +20 -1
  195. python_ddd_framework/hosted_services/catalog.py +6 -16
  196. python_ddd_framework/hosted_services/contracts.py +4 -0
  197. python_ddd_framework/hosted_services/declarations.py +15 -0
  198. python_ddd_framework/hosted_services/module.py +69 -0
  199. python_ddd_framework/hosted_services/runtime.py +30 -1
  200. python_ddd_framework/identity/__init__.py +24 -0
  201. python_ddd_framework/identity/application.py +38 -90
  202. python_ddd_framework/identity/contracts.py +181 -16
  203. python_ddd_framework/identity/http_api.py +79 -2
  204. python_ddd_framework/identity/management.py +166 -0
  205. python_ddd_framework/identity/module.py +12 -4
  206. python_ddd_framework/identity/services.py +29 -16
  207. python_ddd_framework/identity/sqlalchemy/migrations/0003_extra_properties.py +26 -0
  208. python_ddd_framework/identity/sqlalchemy/migrations/0004_concurrency_version.py +29 -0
  209. python_ddd_framework/identity/sqlalchemy/models.py +21 -2
  210. python_ddd_framework/identity/sqlalchemy/module.py +11 -2
  211. python_ddd_framework/identity/sqlalchemy/repository.py +458 -0
  212. python_ddd_framework/identity/sqlalchemy/session_security.py +88 -0
  213. python_ddd_framework/identity/sqlalchemy/stores.py +69 -290
  214. python_ddd_framework/identity/tokens.py +2 -5
  215. python_ddd_framework/{application_services/validation.py → invocation/arguments.py} +8 -6
  216. python_ddd_framework/invocation/contribution.py +61 -0
  217. python_ddd_framework/invocation/dispatcher.py +291 -0
  218. python_ddd_framework/invocation/entries.py +2 -25
  219. python_ddd_framework/invocation/entrypoints.py +126 -0
  220. python_ddd_framework/{application_services → invocation}/execution.py +8 -5
  221. python_ddd_framework/invocation/function_runtime.py +3 -1
  222. python_ddd_framework/invocation/interception.py +113 -42
  223. python_ddd_framework/{application_services → invocation}/interceptors.py +2 -2
  224. python_ddd_framework/invocation/managed_proxy.py +116 -0
  225. python_ddd_framework/invocation/managed_services.py +250 -0
  226. python_ddd_framework/invocation/methods.py +345 -0
  227. python_ddd_framework/invocation/module.py +113 -0
  228. python_ddd_framework/{application_services → invocation}/policies.py +8 -1
  229. python_ddd_framework/invocation/validation.py +25 -0
  230. python_ddd_framework/lifecycle/__init__.py +2 -0
  231. python_ddd_framework/lifecycle/composition.py +5 -2
  232. python_ddd_framework/lifecycle/participants.py +33 -0
  233. python_ddd_framework/messaging/__init__.py +25 -0
  234. python_ddd_framework/messaging/channel.py +205 -0
  235. python_ddd_framework/messaging/contracts.py +69 -0
  236. python_ddd_framework/messaging/module.py +101 -0
  237. python_ddd_framework/messaging/options.py +13 -0
  238. python_ddd_framework/messaging/receipt.py +27 -0
  239. python_ddd_framework/messaging/runtime.py +124 -0
  240. python_ddd_framework/modularity/__init__.py +11 -1
  241. python_ddd_framework/modularity/contracts.py +47 -16
  242. python_ddd_framework/modularity/discovery.py +21 -2
  243. python_ddd_framework/modularity/graph.py +45 -1
  244. python_ddd_framework/modularity/registry.py +83 -3
  245. python_ddd_framework/notifications/__init__.py +2 -0
  246. python_ddd_framework/notifications/catalog.py +3 -2
  247. python_ddd_framework/notifications/declarations.py +18 -0
  248. python_ddd_framework/notifications/module.py +21 -0
  249. python_ddd_framework/observability/logging.py +3 -0
  250. python_ddd_framework/observability/tracing.py +3 -0
  251. python_ddd_framework/options/contribution.py +12 -0
  252. python_ddd_framework/options/registry.py +44 -11
  253. python_ddd_framework/realtime/__init__.py +2 -1
  254. python_ddd_framework/realtime/contracts.py +3 -7
  255. python_ddd_framework/realtime/messages.py +26 -1
  256. python_ddd_framework/redis/cache.py +243 -0
  257. python_ddd_framework/redis/cache_scripts.py +40 -0
  258. python_ddd_framework/redis/distributed_lock.py +10 -2
  259. python_ddd_framework/redis/module.py +13 -4
  260. python_ddd_framework/redis/notifications.py +5 -2
  261. python_ddd_framework/redis/runtime.py +1 -79
  262. python_ddd_framework/seeding/__init__.py +13 -0
  263. python_ddd_framework/seeding/contracts.py +45 -0
  264. python_ddd_framework/seeding/module.py +73 -0
  265. python_ddd_framework/seeding/runtime.py +63 -0
  266. python_ddd_framework/services/arbitration.py +23 -0
  267. python_ddd_framework/services/binding.py +2 -3
  268. python_ddd_framework/services/composition.py +108 -0
  269. python_ddd_framework/services/contribution.py +44 -54
  270. python_ddd_framework/services/convention.py +53 -181
  271. python_ddd_framework/services/fixed_lifetime.py +12 -15
  272. python_ddd_framework/services/framework_provider.py +3 -158
  273. python_ddd_framework/services/native_graph.py +35 -60
  274. python_ddd_framework/services/provider.py +59 -4
  275. python_ddd_framework/services/repository.py +6 -0
  276. python_ddd_framework/services/runtime.py +129 -215
  277. python_ddd_framework/settings/catalog.py +5 -4
  278. python_ddd_framework/settings/declarations.py +18 -0
  279. python_ddd_framework/settings/definitions.py +5 -0
  280. python_ddd_framework/settings/manager.py +1 -1
  281. python_ddd_framework/settings/module.py +50 -1
  282. python_ddd_framework/settings/notifications.py +4 -2
  283. python_ddd_framework/settings/refresh_module.py +9 -2
  284. python_ddd_framework/settings/sqlalchemy/module.py +2 -1
  285. python_ddd_framework/sqlalchemy/__init__.py +12 -0
  286. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +1 -0
  287. python_ddd_framework/sqlalchemy/auditing.py +7 -1
  288. python_ddd_framework/sqlalchemy/extensions.py +186 -0
  289. python_ddd_framework/sqlalchemy/metadata.py +110 -6
  290. python_ddd_framework/sqlalchemy/migration.py +61 -8
  291. python_ddd_framework/sqlalchemy/migration_operations.py +86 -0
  292. python_ddd_framework/sqlalchemy/module.py +14 -11
  293. python_ddd_framework/sqlalchemy/module_migration.py +71 -3
  294. python_ddd_framework/sqlalchemy/repository.py +217 -13
  295. python_ddd_framework/sqlalchemy/session_provider.py +14 -1
  296. python_ddd_framework/testing/runtime.py +5 -4
  297. python_ddd_framework/unit_of_work/__init__.py +2 -1
  298. python_ddd_framework/unit_of_work/contracts.py +6 -7
  299. python_ddd_framework/unit_of_work/errors.py +11 -0
  300. python_ddd_framework/unit_of_work/lifecycle.py +19 -0
  301. python_ddd_framework/unit_of_work/manager.py +86 -32
  302. python_ddd_framework/unit_of_work/module.py +61 -0
  303. python_ddd_framework/unit_of_work/options.py +7 -1
  304. python_ddd_framework/validation/__init__.py +1 -0
  305. python_ddd_framework/validation/models.py +37 -0
  306. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/METADATA +139 -44
  307. python_ddd_framework-0.4.0.dist-info/RECORD +488 -0
  308. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/WHEEL +1 -1
  309. python_ddd_framework/application_services/seeding.py +0 -72
  310. python_ddd_framework/caching/catalog.py +0 -73
  311. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +0 -10
  312. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +0 -24
  313. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +0 -83
  314. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +0 -84
  315. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +0 -35
  316. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +0 -14
  317. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +0 -19
  318. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +0 -24
  319. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +0 -36
  320. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +0 -49
  321. python_ddd_framework-0.3.2.dist-info/RECORD +0 -355
  322. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.dockerignore → backend/.dockerignore} +0 -0
  323. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.gitignore → backend/.gitignore} +0 -0
  324. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.python-version → backend/.python-version} +0 -0
  325. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{Dockerfile → backend/Dockerfile} +0 -0
  326. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.dev.yaml.jinja → backend/compose.dev.yaml.jinja} +0 -0
  327. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.production.yaml.jinja → backend/compose.production.yaml.jinja} +0 -0
  328. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/__init__.py.jinja +0 -0
  329. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/conftest.py.jinja +0 -0
  330. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/host/test_http.py.jinja +0 -0
  331. /python_ddd_framework/{application_services → invocation}/signature.py +0 -0
  332. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/entry_points.txt +0 -0
  333. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/LICENSE +0 -0
  334. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -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:
@@ -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,)