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
@@ -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,
@@ -9,8 +9,20 @@ from dishka import AsyncContainer
9
9
  from dishka.exceptions import NoFactoryError
10
10
  from pydantic import BaseModel, ConfigDict
11
11
 
12
- from ..authorization import CurrentUser, PermissionCatalog, PermissionChecker, PermissionName
13
- from ..caching import CacheDefinition, DistributedCache, DistributedCacheError
12
+ from ..authorization import (
13
+ AuthorizationOptions,
14
+ CurrentUser,
15
+ PermissionCatalog,
16
+ PermissionChecker,
17
+ PermissionName,
18
+ )
19
+ from ..caching import (
20
+ DistributedCache,
21
+ DistributedCacheEntryOptions,
22
+ DistributedCacheError,
23
+ cache_name,
24
+ )
25
+ from ..options import Options
14
26
  from .contracts import IdentityAccessStore
15
27
  from .errors import InvalidCredentialsError
16
28
  from .tokens import JwtTokenService
@@ -18,19 +30,13 @@ from .tokens import JwtTokenService
18
30
  _LOGGER = logging.getLogger(__name__)
19
31
 
20
32
 
33
+ @cache_name("identity.permissions")
21
34
  class PermissionCacheEntry(BaseModel):
22
35
  model_config = ConfigDict(frozen=True)
23
36
 
24
37
  permissions: tuple[str, ...]
25
38
 
26
39
 
27
- IDENTITY_PERMISSION_CACHE = CacheDefinition(
28
- "identity.permissions",
29
- PermissionCacheEntry,
30
- ttl=timedelta(minutes=5),
31
- )
32
-
33
-
34
40
  class IdentityAuthenticator:
35
41
  __slots__ = ("_access", "_tokens")
36
42
 
@@ -51,31 +57,35 @@ class IdentityAuthenticator:
51
57
 
52
58
 
53
59
  class IdentityPermissionChecker(PermissionChecker):
54
- __slots__ = ("_access", "_catalog", "_container")
60
+ __slots__ = ("_access", "_catalog", "_container", "_options")
55
61
 
56
62
  def __init__(
57
63
  self,
58
64
  access: IdentityAccessStore,
59
65
  catalog: PermissionCatalog,
60
66
  container: AsyncContainer,
67
+ options: Options[AuthorizationOptions],
61
68
  ) -> None:
62
69
  self._access = access
63
70
  self._catalog = catalog
64
71
  self._container = container
72
+ self._options = options.value
65
73
 
66
74
  async def is_granted(self, user: CurrentUser, permission: PermissionName) -> bool:
75
+ if self._options.always_allow:
76
+ return True
67
77
  if not user.is_authenticated or permission not in self._catalog:
68
78
  return False
69
79
  cache = await self._optional_cache()
70
80
  cache_key = _permission_cache_key(user, self._catalog.fingerprint)
71
81
  if cache is not None:
72
82
  try:
73
- cached = await cache.get(IDENTITY_PERMISSION_CACHE, cache_key)
83
+ cached = await cache.get(cache_key, hide_errors=False)
74
84
  except DistributedCacheError:
75
85
  cached = None
76
86
  _LOGGER.warning(
77
87
  "permission cache read failed; falling back to authoritative database",
78
- extra={"cache": IDENTITY_PERMISSION_CACHE.name},
88
+ extra={"cache": "identity.permissions"},
79
89
  )
80
90
  if cached is not None:
81
91
  return permission.value in cached.permissions
@@ -84,20 +94,23 @@ class IdentityPermissionChecker(PermissionChecker):
84
94
  if cache is not None:
85
95
  try:
86
96
  await cache.set(
87
- IDENTITY_PERMISSION_CACHE,
88
97
  cache_key,
89
98
  PermissionCacheEntry(permissions=tuple(sorted(permissions))),
99
+ options=DistributedCacheEntryOptions(
100
+ absolute_expiration_relative_to_now=timedelta(minutes=5)
101
+ ),
102
+ hide_errors=False,
90
103
  )
91
104
  except DistributedCacheError:
92
105
  _LOGGER.warning(
93
106
  "permission cache write failed; database result remains authoritative",
94
- extra={"cache": IDENTITY_PERMISSION_CACHE.name},
107
+ extra={"cache": "identity.permissions"},
95
108
  )
96
109
  return permission.value in permissions
97
110
 
98
- async def _optional_cache(self) -> DistributedCache | None:
111
+ async def _optional_cache(self) -> DistributedCache[PermissionCacheEntry] | None:
99
112
  try:
100
- return await self._container.get(DistributedCache)
113
+ return await self._container.get(DistributedCache[PermissionCacheEntry])
101
114
  except NoFactoryError:
102
115
  return None
103
116
 
@@ -0,0 +1,26 @@
1
+ """Identity 拥有基础扩展容器;消费者独立列由自身 revision 管理。"""
2
+
3
+ import sqlalchemy as sa
4
+ from alembic import op
5
+ from sqlalchemy.dialects import postgresql
6
+
7
+ revision = "identity_0003"
8
+ down_revision = "identity_0002"
9
+ branch_labels = None
10
+ depends_on = None
11
+
12
+
13
+ def upgrade() -> None:
14
+ op.add_column(
15
+ "identity_users",
16
+ sa.Column(
17
+ "extra_properties",
18
+ postgresql.JSONB(),
19
+ nullable=False,
20
+ server_default=sa.text("'{}'::jsonb"),
21
+ ),
22
+ )
23
+
24
+
25
+ def downgrade() -> None:
26
+ raise RuntimeError("removing extension data requires an explicit data migration")
@@ -0,0 +1,29 @@
1
+ """Identity owns edit versions independently of permission cache versions."""
2
+
3
+ import sqlalchemy as sa
4
+ from alembic import op
5
+
6
+ revision = "identity_0004"
7
+ down_revision = "identity_0003"
8
+ branch_labels = None
9
+ depends_on = None
10
+
11
+
12
+ def upgrade() -> None:
13
+ op.add_column(
14
+ "identity_users",
15
+ sa.Column(
16
+ "concurrency_version", sa.BigInteger(), nullable=False, server_default=sa.text("0")
17
+ ),
18
+ )
19
+ op.add_column(
20
+ "identity_roles",
21
+ sa.Column(
22
+ "concurrency_version", sa.BigInteger(), nullable=False, server_default=sa.text("0")
23
+ ),
24
+ )
25
+
26
+
27
+ def downgrade() -> None:
28
+ op.drop_column("identity_roles", "concurrency_version")
29
+ op.drop_column("identity_users", "concurrency_version")
@@ -3,9 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from datetime import datetime
6
+ from typing import TYPE_CHECKING
6
7
  from uuid import UUID
7
8
 
8
- from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, Integer, String, Uuid
9
+ from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, Integer, String, Uuid, text
10
+ from sqlalchemy.dialects.postgresql import JSONB
9
11
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
10
12
 
11
13
  from ...sqlalchemy import AuditedEntityMixin
@@ -15,7 +17,7 @@ class IdentityBase(DeclarativeBase):
15
17
  pass
16
18
 
17
19
 
18
- class UserRow(AuditedEntityMixin, IdentityBase):
20
+ class UserRowValues(AuditedEntityMixin):
19
21
  __tablename__ = "identity_users"
20
22
 
21
23
  user_id: Mapped[UUID] = mapped_column(Uuid, primary_key=True)
@@ -28,6 +30,20 @@ class UserRow(AuditedEntityMixin, IdentityBase):
28
30
  failed_access_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
29
31
  lockout_end: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
30
32
  permission_version: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0)
33
+ concurrency_version: Mapped[int] = mapped_column(
34
+ BigInteger, nullable=False, default=0, server_default=text("0")
35
+ )
36
+ extra_properties: Mapped[dict[str, object]] = mapped_column(
37
+ JSONB, nullable=False, default=dict, server_default=text("'{}'::jsonb")
38
+ )
39
+
40
+ if TYPE_CHECKING:
41
+ # 实例由 SQLAlchemy 原生 mapper 的关键字构造器创建。
42
+ def __init__(self, **values: object) -> None: ...
43
+
44
+
45
+ class UserRow(UserRowValues, IdentityBase):
46
+ """基础 Schema 声明;运行时使用本 Application 从 mixin 生成的 mapper。"""
31
47
 
32
48
 
33
49
  class RoleRow(AuditedEntityMixin, IdentityBase):
@@ -39,6 +55,9 @@ class RoleRow(AuditedEntityMixin, IdentityBase):
39
55
  String(120), nullable=False, unique=True, index=True
40
56
  )
41
57
  permission_version: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0)
58
+ concurrency_version: Mapped[int] = mapped_column(
59
+ BigInteger, nullable=False, default=0, server_default=text("0")
60
+ )
42
61
 
43
62
 
44
63
  class UserRoleRow(AuditedEntityMixin, IdentityBase):
@@ -7,22 +7,24 @@ from dishka import Provider, Scope
7
7
  from ...lifecycle import ConfigureContext
8
8
  from ...modularity import AppModule
9
9
  from ...sqlalchemy import (
10
+ SqlAlchemyExtensionMapping,
10
11
  SqlAlchemyModelRegistration,
11
12
  SqlAlchemyPersistenceModule,
12
13
  contributes_sqlalchemy_models,
13
14
  )
14
15
  from ..contracts import (
16
+ IDENTITY_USER_EXTENSION,
15
17
  IdentityAccessStore,
16
18
  IdentityLoginStore,
17
19
  IdentityRepository,
18
20
  RefreshSessionStore,
19
21
  )
20
22
  from ..module import IdentityModule
21
- from .models import IdentityBase
23
+ from .models import IdentityBase, UserRow, UserRowValues
24
+ from .repository import SqlAlchemyIdentityRepository
22
25
  from .stores import (
23
26
  SqlAlchemyIdentityAccessStore,
24
27
  SqlAlchemyIdentityLoginStore,
25
- SqlAlchemyIdentityRepository,
26
28
  SqlAlchemyRefreshSessionStore,
27
29
  )
28
30
 
@@ -32,6 +34,13 @@ from .stores import (
32
34
  metadata=IdentityBase.metadata,
33
35
  migrations_package="python_ddd_framework.identity.sqlalchemy.migrations",
34
36
  branch_label="identity",
37
+ extension_mappings=(
38
+ SqlAlchemyExtensionMapping(
39
+ IDENTITY_USER_EXTENSION,
40
+ IdentityBase.metadata.tables[UserRow.__tablename__],
41
+ UserRowValues,
42
+ ),
43
+ ),
35
44
  )
36
45
  )
37
46
  class SqlAlchemyIdentityModule(AppModule):