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,458 @@
1
+ """管理仓储加入调用方 UoW;拥有并发写入与实体/有效 ORM 映射。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from uuid import UUID
7
+
8
+ from sqlalchemy import Select, func, select
9
+ from sqlalchemy.exc import IntegrityError
10
+
11
+ from ...application_services import PagedResult, PagedResultRequestDto
12
+ from ...domain import OptimisticConcurrencyError
13
+ from ...extensions import ExtensionProperties
14
+ from ...sqlalchemy import SqlAlchemyModelCatalog, SqlAlchemySessionProvider
15
+ from ..contracts import (
16
+ IDENTITY_USER_EXTENSION,
17
+ IdentityRole,
18
+ IdentityUser,
19
+ RolePermissionsView,
20
+ RoleView,
21
+ SetUserActiveCommand,
22
+ UpdateRoleCommand,
23
+ UpdateUserCommand,
24
+ UserRolesView,
25
+ UserView,
26
+ _check_version,
27
+ )
28
+ from ..errors import IdentityConflictError, IdentityResourceNotFoundError
29
+ from .models import (
30
+ RolePermissionRow,
31
+ RoleRow,
32
+ UserRoleRow,
33
+ UserRowValues,
34
+ )
35
+ from .session_security import _lock_identity_user, _revoke_user_sessions
36
+
37
+
38
+ class SqlAlchemyIdentityRepository:
39
+ __slots__ = ("_provider", "_users")
40
+
41
+ def __init__(self, provider: SqlAlchemySessionProvider, models: SqlAlchemyModelCatalog) -> None:
42
+ self._provider = provider
43
+ self._users = models.extension_model(IDENTITY_USER_EXTENSION, UserRowValues)
44
+
45
+ async def find_user_by_normalized_username(
46
+ self,
47
+ normalized_username: str,
48
+ ) -> IdentityUser | None:
49
+ async with self._provider.operation("find_user_by_normalized_username"):
50
+ session = await self._provider.get_session()
51
+ statement = select(self._users.model).where(
52
+ self._users.model.normalized_username == normalized_username
53
+ )
54
+ row = await session.scalar(statement)
55
+ return None if row is None else _user_value(row, self._users.read(row))
56
+
57
+ async def create_user(self, user: IdentityUser) -> None:
58
+ async with self._provider.operation("create_user", write=True):
59
+ session = await self._provider.get_session()
60
+ row = self._users.model(
61
+ user_id=user.user_id,
62
+ username=user.username,
63
+ normalized_username=user.normalized_username,
64
+ password_hash=user.password_hash,
65
+ is_active=user.is_active,
66
+ failed_access_count=user.failed_access_count,
67
+ lockout_end=user.lockout_end,
68
+ permission_version=user.permission_version,
69
+ concurrency_version=user.concurrency_version,
70
+ )
71
+ self._users.write(row, user.extra_properties)
72
+ session.add(row)
73
+ await self._flush_unique("user")
74
+
75
+ async def get_user(self, user_id: UUID) -> IdentityUser:
76
+ async with self._provider.operation("get_user"):
77
+ row = await self._require_user(user_id, for_update=False)
78
+ return _user_value(row, self._users.read(row))
79
+
80
+ async def get_user_view(self, user_id: UUID) -> UserView:
81
+ async with self._provider.operation("get_user_view"):
82
+ items = await self._read_user_views(
83
+ select(self._users.model.user_id).where(self._users.model.user_id == user_id)
84
+ )
85
+ if not items:
86
+ raise IdentityResourceNotFoundError(resource="user")
87
+ return items[0]
88
+
89
+ async def _user_view(self, row: UserRowValues) -> UserView:
90
+ session = await self._provider.get_session()
91
+ roles = tuple(
92
+ (
93
+ await session.scalars(
94
+ select(UserRoleRow.role_id)
95
+ .where(UserRoleRow.user_id == row.user_id)
96
+ .order_by(UserRoleRow.role_id)
97
+ )
98
+ ).all()
99
+ )
100
+ return UserView(
101
+ user_id=row.user_id,
102
+ username=row.username,
103
+ is_active=row.is_active,
104
+ role_ids=roles,
105
+ concurrency_version=row.concurrency_version,
106
+ extra_properties=self._users.read(row),
107
+ )
108
+
109
+ async def list_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]:
110
+ async with self._provider.operation("list_users"):
111
+ session = await self._provider.get_session()
112
+ total = await session.scalar(select(func.count()).select_from(self._users.model))
113
+ identifiers = (
114
+ select(self._users.model.user_id)
115
+ .order_by(self._users.model.normalized_username, self._users.model.user_id)
116
+ .offset(query.skip_count)
117
+ .limit(query.max_result_count)
118
+ )
119
+ items = await self._read_user_views(identifiers)
120
+ return PagedResult[UserView](items=items, total_count=total or 0)
121
+
122
+ async def _read_user_views(self, identifiers: Select[tuple[UUID]]) -> list[UserView]:
123
+ session = await self._provider.get_session()
124
+ role_ids = (
125
+ select(func.array_agg(UserRoleRow.role_id))
126
+ .where(UserRoleRow.user_id == self._users.model.user_id)
127
+ .correlate(self._users.model)
128
+ .scalar_subquery()
129
+ )
130
+ # 一条 SQL 快照同时读取编辑版本和关系,避免读锁升级与 user gate 形成反向等待。
131
+ rows = (
132
+ await session.execute(
133
+ select(self._users.model, role_ids)
134
+ .where(self._users.model.user_id.in_(identifiers))
135
+ .order_by(self._users.model.normalized_username, self._users.model.user_id)
136
+ .execution_options(populate_existing=True)
137
+ )
138
+ ).all()
139
+ return [
140
+ UserView(
141
+ user_id=row.user_id,
142
+ username=row.username,
143
+ is_active=row.is_active,
144
+ role_ids=tuple(sorted(roles or ())),
145
+ concurrency_version=row.concurrency_version,
146
+ extra_properties=self._users.read(row),
147
+ )
148
+ for row, roles in rows
149
+ ]
150
+
151
+ async def update_user(self, user_id: UUID, command: UpdateUserCommand) -> UserView:
152
+ async with self._provider.operation("update_user", write=True):
153
+ row = await self._require_user(user_id, for_update=True)
154
+ user = _user_value(row, self._users.read(row)).renamed(
155
+ command.username, command.concurrency_version
156
+ )
157
+ row.normalized_username = user.normalized_username
158
+ row.username = user.username
159
+ self._users.write(row, command.extra_properties)
160
+ row.concurrency_version = user.concurrency_version
161
+ await self._flush_unique("user")
162
+ return await self._user_view(row)
163
+
164
+ async def set_user_active(self, user_id: UUID, command: SetUserActiveCommand) -> UserView:
165
+ async with self._provider.operation("set_user_active", write=True):
166
+ session = await self._provider.get_session()
167
+ row = await self._require_user(user_id, for_update=True)
168
+ user = _user_value(row, self._users.read(row)).with_active(
169
+ command.is_active, command.concurrency_version
170
+ )
171
+ row.is_active = user.is_active
172
+ row.concurrency_version = user.concurrency_version
173
+ if not command.is_active:
174
+ await _revoke_user_sessions(session, user_id, now=datetime.now(UTC))
175
+ await session.flush()
176
+ return await self._user_view(row)
177
+
178
+ async def delete_user(self, user_id: UUID, concurrency_version: int) -> None:
179
+ async with self._provider.operation("delete_user", write=True):
180
+ session = await self._provider.get_session()
181
+ row = await self._require_user(user_id, for_update=True)
182
+ _check_version(row.concurrency_version, concurrency_version)
183
+ await _revoke_user_sessions(session, user_id, now=datetime.now(UTC))
184
+ # FK CASCADE 在同一 UoW 内删除该用户的关系和所有 family 历史。
185
+ await session.delete(row)
186
+ await session.flush()
187
+
188
+ async def set_password(
189
+ self,
190
+ user_id: UUID,
191
+ password_hash: str,
192
+ concurrency_version: int,
193
+ *,
194
+ expected_password_hash: str | None = None,
195
+ ) -> None:
196
+ async with self._provider.operation("set_password", write=True):
197
+ session = await self._provider.get_session()
198
+ row = await self._require_user(user_id, for_update=True)
199
+ _check_version(row.concurrency_version, concurrency_version)
200
+ if expected_password_hash is not None and row.password_hash != expected_password_hash:
201
+ raise OptimisticConcurrencyError
202
+ row.password_hash = password_hash
203
+ row.failed_access_count = 0
204
+ row.lockout_end = None
205
+ row.concurrency_version += 1
206
+ await _revoke_user_sessions(session, user_id, now=datetime.now(UTC))
207
+ await session.flush()
208
+
209
+ async def create_role(self, role: IdentityRole) -> None:
210
+ async with self._provider.operation("create_role", write=True):
211
+ session = await self._provider.get_session()
212
+ session.add(
213
+ RoleRow(
214
+ role_id=role.role_id,
215
+ name=role.name,
216
+ normalized_name=role.normalized_name,
217
+ permission_version=role.permission_version,
218
+ concurrency_version=role.concurrency_version,
219
+ )
220
+ )
221
+ await self._flush_unique("role")
222
+
223
+ async def find_role_by_normalized_name(self, normalized_name: str) -> IdentityRole | None:
224
+ async with self._provider.operation("find_role_by_normalized_name"):
225
+ session = await self._provider.get_session()
226
+ row = await session.scalar(
227
+ select(RoleRow).where(RoleRow.normalized_name == normalized_name)
228
+ )
229
+ return None if row is None else _role_value(row)
230
+
231
+ async def get_role(self, role_id: UUID) -> RoleView:
232
+ async with self._provider.operation("get_role"):
233
+ items = await self._read_role_views(
234
+ select(RoleRow.role_id).where(RoleRow.role_id == role_id)
235
+ )
236
+ if not items:
237
+ raise IdentityResourceNotFoundError(resource="role")
238
+ return items[0]
239
+
240
+ async def _role_view(self, row: RoleRow) -> RoleView:
241
+ session = await self._provider.get_session()
242
+ permissions = tuple(
243
+ (
244
+ await session.scalars(
245
+ select(RolePermissionRow.permission_name)
246
+ .where(RolePermissionRow.role_id == row.role_id)
247
+ .order_by(RolePermissionRow.permission_name)
248
+ )
249
+ ).all()
250
+ )
251
+ return RoleView(row.role_id, row.name, permissions, row.concurrency_version)
252
+
253
+ async def list_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]:
254
+ async with self._provider.operation("list_roles"):
255
+ session = await self._provider.get_session()
256
+ total = await session.scalar(select(func.count()).select_from(RoleRow))
257
+ identifiers = (
258
+ select(RoleRow.role_id)
259
+ .order_by(RoleRow.normalized_name, RoleRow.role_id)
260
+ .offset(query.skip_count)
261
+ .limit(query.max_result_count)
262
+ )
263
+ items = await self._read_role_views(identifiers)
264
+ return PagedResult[RoleView](items=items, total_count=total or 0)
265
+
266
+ async def _read_role_views(self, identifiers: Select[tuple[UUID]]) -> list[RoleView]:
267
+ session = await self._provider.get_session()
268
+ permissions = (
269
+ select(func.array_agg(RolePermissionRow.permission_name))
270
+ .where(RolePermissionRow.role_id == RoleRow.role_id)
271
+ .correlate(RoleRow)
272
+ .scalar_subquery()
273
+ )
274
+ rows = (
275
+ await session.execute(
276
+ select(RoleRow, permissions)
277
+ .where(RoleRow.role_id.in_(identifiers))
278
+ .order_by(RoleRow.normalized_name, RoleRow.role_id)
279
+ .execution_options(populate_existing=True)
280
+ )
281
+ ).all()
282
+ return [
283
+ RoleView(row.role_id, row.name, tuple(sorted(names or ())), row.concurrency_version)
284
+ for row, names in rows
285
+ ]
286
+
287
+ async def update_role(self, role_id: UUID, command: UpdateRoleCommand) -> RoleView:
288
+ async with self._provider.operation("update_role", write=True):
289
+ row = await self._require_role(role_id, for_update=True)
290
+ role = _role_value(row).renamed(command.name, command.concurrency_version)
291
+ row.normalized_name = role.normalized_name
292
+ row.name = role.name
293
+ row.concurrency_version = role.concurrency_version
294
+ await self._flush_unique("role")
295
+ return await self._role_view(row)
296
+
297
+ async def delete_role(self, role_id: UUID, concurrency_version: int) -> None:
298
+ async with self._provider.operation("delete_role", write=True):
299
+ session = await self._provider.get_session()
300
+ role = await self._require_role(role_id, for_update=True)
301
+ _check_version(role.concurrency_version, concurrency_version)
302
+ user_ids = tuple(
303
+ (
304
+ await session.scalars(
305
+ select(UserRoleRow.user_id)
306
+ .where(UserRoleRow.role_id == role_id)
307
+ .order_by(UserRoleRow.user_id)
308
+ )
309
+ ).all()
310
+ )
311
+ # 和分配保持 role -> user 顺序;级联删除也是用户关系的修改,必须推进其版本。
312
+ for user_id in user_ids:
313
+ await _lock_identity_user(session, user_id)
314
+ user = await session.scalar(
315
+ select(self._users.model)
316
+ .where(self._users.model.user_id == user_id)
317
+ .with_for_update()
318
+ .execution_options(populate_existing=True)
319
+ )
320
+ if user is not None:
321
+ user.concurrency_version += 1
322
+ user.permission_version += 1
323
+ await session.delete(role)
324
+ await session.flush()
325
+
326
+ async def set_user_roles(
327
+ self, user_id: UUID, role_ids: tuple[UUID, ...], concurrency_version: int
328
+ ) -> UserRolesView:
329
+ async with self._provider.operation("set_user_roles", write=True):
330
+ session = await self._provider.get_session()
331
+ requested = frozenset(role_ids)
332
+ roles = tuple(
333
+ (
334
+ await session.scalars(
335
+ select(RoleRow)
336
+ .where(RoleRow.role_id.in_(requested))
337
+ .order_by(RoleRow.role_id)
338
+ .with_for_update()
339
+ )
340
+ ).all()
341
+ )
342
+ if len(roles) != len(requested):
343
+ raise IdentityResourceNotFoundError(resource="role")
344
+ user = await self._require_user(user_id, for_update=True)
345
+ _check_version(user.concurrency_version, concurrency_version)
346
+ existing = tuple(
347
+ (
348
+ await session.scalars(
349
+ select(UserRoleRow).where(UserRoleRow.user_id == user_id).with_for_update()
350
+ )
351
+ ).all()
352
+ )
353
+ current = frozenset(item.role_id for item in existing)
354
+ if current == requested:
355
+ return UserRolesView(
356
+ role_ids=tuple(sorted(current)), concurrency_version=user.concurrency_version
357
+ )
358
+ for item in existing:
359
+ if item.role_id not in requested:
360
+ await session.delete(item)
361
+ for role_id in sorted(requested - current):
362
+ session.add(UserRoleRow(user_id=user_id, role_id=role_id))
363
+ user.permission_version += 1
364
+ user.concurrency_version += 1
365
+ await session.flush()
366
+ return UserRolesView(
367
+ role_ids=tuple(sorted(requested)), concurrency_version=user.concurrency_version
368
+ )
369
+
370
+ async def set_role_permissions(
371
+ self, role_id: UUID, permissions: tuple[str, ...], concurrency_version: int
372
+ ) -> RolePermissionsView:
373
+ async with self._provider.operation("set_role_permissions", write=True):
374
+ session = await self._provider.get_session()
375
+ role = await self._require_role(role_id, for_update=True)
376
+ _check_version(role.concurrency_version, concurrency_version)
377
+ requested = frozenset(permissions)
378
+ existing = tuple(
379
+ (
380
+ await session.scalars(
381
+ select(RolePermissionRow)
382
+ .where(RolePermissionRow.role_id == role_id)
383
+ .with_for_update()
384
+ )
385
+ ).all()
386
+ )
387
+ current = frozenset(item.permission_name for item in existing)
388
+ if current == requested:
389
+ return RolePermissionsView(
390
+ permissions=tuple(sorted(current)), concurrency_version=role.concurrency_version
391
+ )
392
+ for item in existing:
393
+ if item.permission_name not in requested:
394
+ await session.delete(item)
395
+ for permission in sorted(requested - current):
396
+ session.add(RolePermissionRow(role_id=role_id, permission_name=permission))
397
+ role.permission_version += 1
398
+ role.concurrency_version += 1
399
+ await session.flush()
400
+ return RolePermissionsView(
401
+ permissions=tuple(sorted(requested)), concurrency_version=role.concurrency_version
402
+ )
403
+
404
+ async def _require_user(self, user_id: UUID, *, for_update: bool) -> UserRowValues:
405
+ session = await self._provider.get_session()
406
+ statement = select(self._users.model).where(self._users.model.user_id == user_id)
407
+ if for_update:
408
+ await _lock_identity_user(session, user_id)
409
+ statement = statement.with_for_update()
410
+ # 写锁后强制刷新 identity map,不拿事务内缓存的旧版本执行并发比较。
411
+ statement = statement.execution_options(populate_existing=True)
412
+ row = await session.scalar(statement)
413
+ if row is None:
414
+ raise IdentityResourceNotFoundError(resource="user")
415
+ return row
416
+
417
+ async def _require_role(self, role_id: UUID, *, for_update: bool) -> RoleRow:
418
+ session = await self._provider.get_session()
419
+ statement = select(RoleRow).where(RoleRow.role_id == role_id)
420
+ if for_update:
421
+ statement = statement.with_for_update()
422
+ statement = statement.execution_options(populate_existing=True)
423
+ row = await session.scalar(statement)
424
+ if row is None:
425
+ raise IdentityResourceNotFoundError(resource="role")
426
+ return row
427
+
428
+ async def _flush_unique(self, resource: str) -> None:
429
+ session = await self._provider.get_session()
430
+ try:
431
+ await session.flush()
432
+ except IntegrityError:
433
+ raise IdentityConflictError(resource=resource) from None
434
+
435
+
436
+ def _user_value(row: UserRowValues, extra_properties: ExtensionProperties) -> IdentityUser:
437
+ return IdentityUser(
438
+ user_id=row.user_id,
439
+ username=row.username,
440
+ normalized_username=row.normalized_username,
441
+ password_hash=row.password_hash,
442
+ is_active=row.is_active,
443
+ failed_access_count=row.failed_access_count,
444
+ lockout_end=row.lockout_end,
445
+ permission_version=row.permission_version,
446
+ concurrency_version=row.concurrency_version,
447
+ extra_properties=extra_properties,
448
+ )
449
+
450
+
451
+ def _role_value(row: RoleRow) -> IdentityRole:
452
+ return IdentityRole(
453
+ role_id=row.role_id,
454
+ name=row.name,
455
+ normalized_name=row.normalized_name,
456
+ permission_version=row.permission_version,
457
+ concurrency_version=row.concurrency_version,
458
+ )
@@ -0,0 +1,88 @@
1
+ """凭据会话的统一锁序、撤销与期限判断;调用方拥有事务。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from hashlib import blake2b
7
+ from uuid import UUID
8
+
9
+ from sqlalchemy import func, select
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ from ..contracts import (
13
+ IdentityOptions,
14
+ )
15
+ from .models import (
16
+ RefreshSessionRow,
17
+ )
18
+
19
+ _REFRESH_FAMILY_LOCK_PERSON = b"pyplat.rfam.v1"
20
+
21
+
22
+ async def _revoke_family(
23
+ session: AsyncSession,
24
+ family_id: UUID,
25
+ *,
26
+ now: datetime,
27
+ ) -> None:
28
+ rows = tuple(
29
+ (
30
+ await session.scalars(
31
+ select(RefreshSessionRow)
32
+ .where(RefreshSessionRow.family_id == family_id)
33
+ .with_for_update()
34
+ )
35
+ ).all()
36
+ )
37
+ for row in rows:
38
+ if row.revoked_at is None:
39
+ row.revoked_at = now
40
+
41
+
42
+ async def _lock_refresh_family(session: AsyncSession, family_id: UUID) -> None:
43
+ # family 没有可锁的稳定 parent row;transaction advisory lock 在同一短事务内提供
44
+ # family -> token/family rows 的统一顺序,并随 commit/rollback 自动释放。
45
+ await session.scalar(select(func.pg_advisory_xact_lock(_refresh_family_lock_key(family_id))))
46
+
47
+
48
+ async def _lock_identity_user(session: AsyncSession, user_id: UUID) -> None:
49
+ # 登录创建新 family 与管理撤销共同持有稳定 user gate,阻止枚举 family 后漏掉新会话。
50
+ # 所有凭据/family 写入先取得此 gate;family 内仍是 advisory -> refresh rows。
51
+ digest = blake2b(user_id.bytes, digest_size=8, person=b"pyplat.user.v1").digest()
52
+ key = int.from_bytes(digest, byteorder="big", signed=True)
53
+ await session.scalar(select(func.pg_advisory_xact_lock(key)))
54
+
55
+
56
+ async def _revoke_user_sessions(session: AsyncSession, user_id: UUID, *, now: datetime) -> None:
57
+ # 调用方已持有 user gate;family 集合不会在此事务期间增长。
58
+ families = tuple(
59
+ (
60
+ await session.scalars(
61
+ select(RefreshSessionRow.family_id)
62
+ .where(RefreshSessionRow.user_id == user_id)
63
+ .distinct()
64
+ .order_by(RefreshSessionRow.family_id)
65
+ )
66
+ ).all()
67
+ )
68
+ for family_id in families:
69
+ await _lock_refresh_family(session, family_id)
70
+ await _revoke_family(session, family_id, now=now)
71
+
72
+
73
+ def _refresh_family_lock_key(family_id: UUID) -> int:
74
+ digest = blake2b(
75
+ family_id.bytes,
76
+ digest_size=8,
77
+ person=_REFRESH_FAMILY_LOCK_PERSON,
78
+ ).digest()
79
+ return int.from_bytes(digest, byteorder="big", signed=True)
80
+
81
+
82
+ def _session_is_active(row: RefreshSessionRow, now: datetime, options: IdentityOptions) -> bool:
83
+ return (
84
+ row.revoked_at is None
85
+ and row.replaced_by_session_id is None
86
+ and now - row.last_used_at < options.refresh_inactivity_lifetime
87
+ and now - row.family_created_at < options.refresh_absolute_lifetime
88
+ )