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
@@ -3,231 +3,50 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from datetime import datetime, timedelta
6
- from hashlib import blake2b
7
6
  from uuid import UUID
8
7
 
9
- from sqlalchemy import func, select
10
- from sqlalchemy.exc import IntegrityError
11
- from sqlalchemy.ext.asyncio import AsyncSession
8
+ from sqlalchemy import select
12
9
 
13
10
  from ...authorization import CurrentUser, RolePermissionVersion
14
- from ...sqlalchemy import FrameworkAsyncSession, SqlAlchemySessionProvider
11
+ from ...options import Options
12
+ from ...sqlalchemy import FrameworkAsyncSession, SqlAlchemyModelCatalog
15
13
  from ...sqlalchemy.runtime import _SqlAlchemyEngineRegistry
16
14
  from ..contracts import (
17
- IdentityRole,
15
+ IDENTITY_USER_EXTENSION,
16
+ IdentityOptions,
18
17
  IdentityUser,
19
18
  RefreshRotationResult,
20
19
  RefreshRotationStatus,
21
20
  RefreshSession,
22
- RoleView,
23
- UserView,
24
21
  )
25
- from ..errors import IdentityConflictError, IdentityResourceNotFoundError
26
- from ..tokens import REFRESH_ABSOLUTE_LIFETIME, REFRESH_INACTIVITY_LIFETIME
27
22
  from .models import (
28
23
  RefreshSessionRow,
29
24
  RolePermissionRow,
30
25
  RoleRow,
31
26
  UserRoleRow,
32
- UserRow,
27
+ UserRowValues,
28
+ )
29
+ from .repository import _user_value
30
+ from .session_security import (
31
+ _lock_identity_user,
32
+ _lock_refresh_family,
33
+ _revoke_family,
34
+ _session_is_active,
33
35
  )
34
-
35
- _REFRESH_FAMILY_LOCK_PERSON = b"pyplat.rfam.v1"
36
-
37
-
38
- class SqlAlchemyIdentityRepository:
39
- __slots__ = ("_provider",)
40
-
41
- def __init__(self, provider: SqlAlchemySessionProvider) -> None:
42
- self._provider = provider
43
-
44
- async def find_user_by_normalized_username(
45
- self,
46
- normalized_username: str,
47
- ) -> IdentityUser | None:
48
- async with self._provider.operation("find_user_by_normalized_username"):
49
- session = await self._provider.get_session()
50
- statement = select(UserRow).where(UserRow.normalized_username == normalized_username)
51
- row = await session.scalar(statement)
52
- return None if row is None else _user_value(row)
53
-
54
- async def create_user(self, user: IdentityUser) -> None:
55
- async with self._provider.operation("create_user"):
56
- session = await self._provider.get_session()
57
- session.add(
58
- UserRow(
59
- user_id=user.user_id,
60
- username=user.username,
61
- normalized_username=user.normalized_username,
62
- password_hash=user.password_hash,
63
- is_active=user.is_active,
64
- failed_access_count=user.failed_access_count,
65
- lockout_end=user.lockout_end,
66
- permission_version=user.permission_version,
67
- )
68
- )
69
- await self._flush_unique("user")
70
-
71
- async def list_users(self) -> tuple[UserView, ...]:
72
- async with self._provider.operation("list_users"):
73
- session = await self._provider.get_session()
74
- rows = tuple(
75
- (await session.scalars(select(UserRow).order_by(UserRow.normalized_username))).all()
76
- )
77
- role_rows = (
78
- await session.execute(
79
- select(UserRoleRow.user_id, UserRoleRow.role_id).order_by(
80
- UserRoleRow.user_id, UserRoleRow.role_id
81
- )
82
- )
83
- ).all()
84
- roles_by_user: dict[UUID, list[UUID]] = {}
85
- for user_id, role_id in role_rows:
86
- roles_by_user.setdefault(user_id, []).append(role_id)
87
- return tuple(
88
- UserView(
89
- user_id=row.user_id,
90
- username=row.username,
91
- is_active=row.is_active,
92
- role_ids=tuple(roles_by_user.get(row.user_id, ())),
93
- )
94
- for row in rows
95
- )
96
-
97
- async def create_role(self, role: IdentityRole) -> None:
98
- async with self._provider.operation("create_role"):
99
- session = await self._provider.get_session()
100
- session.add(
101
- RoleRow(
102
- role_id=role.role_id,
103
- name=role.name,
104
- normalized_name=role.normalized_name,
105
- permission_version=role.permission_version,
106
- )
107
- )
108
- await self._flush_unique("role")
109
-
110
- async def find_role_by_normalized_name(self, normalized_name: str) -> IdentityRole | None:
111
- async with self._provider.operation("find_role_by_normalized_name"):
112
- session = await self._provider.get_session()
113
- row = await session.scalar(
114
- select(RoleRow).where(RoleRow.normalized_name == normalized_name)
115
- )
116
- return None if row is None else _role_value(row)
117
-
118
- async def list_roles(self) -> tuple[RoleView, ...]:
119
- async with self._provider.operation("list_roles"):
120
- session = await self._provider.get_session()
121
- roles = tuple(
122
- (await session.scalars(select(RoleRow).order_by(RoleRow.normalized_name))).all()
123
- )
124
- permission_rows = (
125
- await session.execute(
126
- select(RolePermissionRow.role_id, RolePermissionRow.permission_name).order_by(
127
- RolePermissionRow.role_id, RolePermissionRow.permission_name
128
- )
129
- )
130
- ).all()
131
- permissions_by_role: dict[UUID, list[str]] = {}
132
- for role_id, permission_name in permission_rows:
133
- permissions_by_role.setdefault(role_id, []).append(permission_name)
134
- return tuple(
135
- RoleView(
136
- role_id=row.role_id,
137
- name=row.name,
138
- permissions=tuple(permissions_by_role.get(row.role_id, ())),
139
- )
140
- for row in roles
141
- )
142
-
143
- async def set_user_roles(self, user_id: UUID, role_ids: tuple[UUID, ...]) -> None:
144
- async with self._provider.operation("set_user_roles"):
145
- session = await self._provider.get_session()
146
- user = await self._require_user(user_id, for_update=True)
147
- requested = frozenset(role_ids)
148
- roles = tuple(
149
- (
150
- await session.scalars(
151
- select(RoleRow).where(RoleRow.role_id.in_(requested)).with_for_update()
152
- )
153
- ).all()
154
- )
155
- if len(roles) != len(requested):
156
- raise IdentityResourceNotFoundError(resource="role")
157
- existing = tuple(
158
- (
159
- await session.scalars(
160
- select(UserRoleRow).where(UserRoleRow.user_id == user_id).with_for_update()
161
- )
162
- ).all()
163
- )
164
- current = frozenset(item.role_id for item in existing)
165
- if current == requested:
166
- return
167
- for item in existing:
168
- if item.role_id not in requested:
169
- await session.delete(item)
170
- for role_id in sorted(requested - current):
171
- session.add(UserRoleRow(user_id=user_id, role_id=role_id))
172
- user.permission_version += 1
173
-
174
- async def set_role_permissions(self, role_id: UUID, permissions: tuple[str, ...]) -> None:
175
- async with self._provider.operation("set_role_permissions"):
176
- session = await self._provider.get_session()
177
- role = await self._require_role(role_id, for_update=True)
178
- requested = frozenset(permissions)
179
- existing = tuple(
180
- (
181
- await session.scalars(
182
- select(RolePermissionRow)
183
- .where(RolePermissionRow.role_id == role_id)
184
- .with_for_update()
185
- )
186
- ).all()
187
- )
188
- current = frozenset(item.permission_name for item in existing)
189
- if current == requested:
190
- return
191
- for item in existing:
192
- if item.permission_name not in requested:
193
- await session.delete(item)
194
- for permission in sorted(requested - current):
195
- session.add(RolePermissionRow(role_id=role_id, permission_name=permission))
196
- role.permission_version += 1
197
-
198
- async def _require_user(self, user_id: UUID, *, for_update: bool) -> UserRow:
199
- session = await self._provider.get_session()
200
- statement = select(UserRow).where(UserRow.user_id == user_id)
201
- if for_update:
202
- statement = statement.with_for_update()
203
- row = await session.scalar(statement)
204
- if row is None:
205
- raise IdentityResourceNotFoundError(resource="user")
206
- return row
207
-
208
- async def _require_role(self, role_id: UUID, *, for_update: bool) -> RoleRow:
209
- session = await self._provider.get_session()
210
- statement = select(RoleRow).where(RoleRow.role_id == role_id)
211
- if for_update:
212
- statement = statement.with_for_update()
213
- row = await session.scalar(statement)
214
- if row is None:
215
- raise IdentityResourceNotFoundError(resource="role")
216
- return row
217
-
218
- async def _flush_unique(self, resource: str) -> None:
219
- session = await self._provider.get_session()
220
- try:
221
- await session.flush()
222
- except IntegrityError:
223
- raise IdentityConflictError(resource=resource) from None
224
36
 
225
37
 
226
38
  class SqlAlchemyIdentityAccessStore:
227
- __slots__ = ("_engines",)
39
+ __slots__ = ("_engines", "_options", "_users")
228
40
 
229
- def __init__(self, engines: _SqlAlchemyEngineRegistry) -> None:
41
+ def __init__(
42
+ self,
43
+ engines: _SqlAlchemyEngineRegistry,
44
+ models: SqlAlchemyModelCatalog,
45
+ options: Options[IdentityOptions],
46
+ ) -> None:
230
47
  self._engines = engines
48
+ self._users = models.extension_model(IDENTITY_USER_EXTENSION, UserRowValues)
49
+ self._options = options.value
231
50
 
232
51
  async def load_current_user(
233
52
  self,
@@ -237,7 +56,9 @@ class SqlAlchemyIdentityAccessStore:
237
56
  ) -> CurrentUser | None:
238
57
  async with self._session() as session:
239
58
  user = await session.scalar(
240
- select(UserRow).where(UserRow.user_id == user_id, UserRow.is_active.is_(True))
59
+ select(self._users.model).where(
60
+ self._users.model.user_id == user_id, self._users.model.is_active.is_(True)
61
+ )
241
62
  )
242
63
  refresh = await session.scalar(
243
64
  select(RefreshSessionRow).where(
@@ -245,7 +66,11 @@ class SqlAlchemyIdentityAccessStore:
245
66
  RefreshSessionRow.user_id == user_id,
246
67
  )
247
68
  )
248
- if user is None or refresh is None or not _session_is_active(refresh, now):
69
+ if (
70
+ user is None
71
+ or refresh is None
72
+ or not _session_is_active(refresh, now, self._options)
73
+ ):
249
74
  return None
250
75
  role_rows = (
251
76
  await session.execute(
@@ -300,17 +125,20 @@ class SqlAlchemyIdentityAccessStore:
300
125
 
301
126
 
302
127
  class SqlAlchemyIdentityLoginStore:
303
- __slots__ = ("_engines",)
128
+ __slots__ = ("_engines", "_users")
304
129
 
305
- def __init__(self, engines: _SqlAlchemyEngineRegistry) -> None:
130
+ def __init__(self, engines: _SqlAlchemyEngineRegistry, models: SqlAlchemyModelCatalog) -> None:
306
131
  self._engines = engines
132
+ self._users = models.extension_model(IDENTITY_USER_EXTENSION, UserRowValues)
307
133
 
308
134
  async def find_user(self, normalized_username: str) -> IdentityUser | None:
309
135
  async with self._session() as session:
310
136
  row = await session.scalar(
311
- select(UserRow).where(UserRow.normalized_username == normalized_username)
137
+ select(self._users.model).where(
138
+ self._users.model.normalized_username == normalized_username
139
+ )
312
140
  )
313
- return None if row is None else _user_value(row)
141
+ return None if row is None else _user_value(row, self._users.read(row))
314
142
 
315
143
  async def record_failure(
316
144
  self,
@@ -321,8 +149,11 @@ class SqlAlchemyIdentityLoginStore:
321
149
  lockout_duration_seconds: int,
322
150
  ) -> None:
323
151
  async with self._session() as session, session.begin():
152
+ await _lock_identity_user(session, user_id)
324
153
  row = await session.scalar(
325
- select(UserRow).where(UserRow.user_id == user_id).with_for_update()
154
+ select(self._users.model)
155
+ .where(self._users.model.user_id == user_id)
156
+ .with_for_update()
326
157
  )
327
158
  if row is None or not row.is_active:
328
159
  return
@@ -349,8 +180,11 @@ class SqlAlchemyIdentityLoginStore:
349
180
  now: datetime,
350
181
  ) -> bool:
351
182
  async with self._session() as database, database.begin():
183
+ await _lock_identity_user(database, user_id)
352
184
  row = await database.scalar(
353
- select(UserRow).where(UserRow.user_id == user_id).with_for_update()
185
+ select(self._users.model)
186
+ .where(self._users.model.user_id == user_id)
187
+ .with_for_update()
354
188
  )
355
189
  if (
356
190
  row is None
@@ -371,10 +205,13 @@ class SqlAlchemyIdentityLoginStore:
371
205
 
372
206
 
373
207
  class SqlAlchemyRefreshSessionStore:
374
- __slots__ = ("_engines",)
208
+ __slots__ = ("_engines", "_options")
375
209
 
376
- def __init__(self, engines: _SqlAlchemyEngineRegistry) -> None:
210
+ def __init__(
211
+ self, engines: _SqlAlchemyEngineRegistry, options: Options[IdentityOptions]
212
+ ) -> None:
377
213
  self._engines = engines
214
+ self._options = options.value
378
215
 
379
216
  async def rotate(
380
217
  self,
@@ -385,13 +222,17 @@ class SqlAlchemyRefreshSessionStore:
385
222
  now: datetime,
386
223
  ) -> RefreshRotationResult:
387
224
  async with self._session() as session, session.begin():
388
- family_id = await session.scalar(
389
- select(RefreshSessionRow.family_id).where(
390
- RefreshSessionRow.token_hash == token_hash
225
+ identity = (
226
+ await session.execute(
227
+ select(RefreshSessionRow.user_id, RefreshSessionRow.family_id).where(
228
+ RefreshSessionRow.token_hash == token_hash
229
+ )
391
230
  )
392
- )
393
- if family_id is None:
231
+ ).one_or_none()
232
+ if identity is None:
394
233
  return RefreshRotationResult(RefreshRotationStatus.NOT_FOUND, None)
234
+ user_id, family_id = identity
235
+ await _lock_identity_user(session, user_id)
395
236
  await _lock_refresh_family(session, family_id)
396
237
  current = await session.scalar(
397
238
  select(RefreshSessionRow)
@@ -404,7 +245,7 @@ class SqlAlchemyRefreshSessionStore:
404
245
  if current.revoked_at is not None or current.replaced_by_session_id is not None:
405
246
  await _revoke_family(session, current.family_id, now=now)
406
247
  return RefreshRotationResult(RefreshRotationStatus.REUSED, None)
407
- if not _session_is_active(current, now):
248
+ if not _session_is_active(current, now, self._options):
408
249
  await _revoke_family(session, current.family_id, now=now)
409
250
  return RefreshRotationResult(RefreshRotationStatus.EXPIRED, None)
410
251
 
@@ -425,13 +266,17 @@ class SqlAlchemyRefreshSessionStore:
425
266
 
426
267
  async def revoke(self, token_hash: str, *, now: datetime) -> None:
427
268
  async with self._session() as session, session.begin():
428
- family_id = await session.scalar(
429
- select(RefreshSessionRow.family_id).where(
430
- RefreshSessionRow.token_hash == token_hash
269
+ identity = (
270
+ await session.execute(
271
+ select(RefreshSessionRow.user_id, RefreshSessionRow.family_id).where(
272
+ RefreshSessionRow.token_hash == token_hash
273
+ )
431
274
  )
432
- )
433
- if family_id is None:
275
+ ).one_or_none()
276
+ if identity is None:
434
277
  return
278
+ user_id, family_id = identity
279
+ await _lock_identity_user(session, user_id)
435
280
  await _lock_refresh_family(session, family_id)
436
281
  current = await session.scalar(
437
282
  select(RefreshSessionRow)
@@ -446,72 +291,6 @@ class SqlAlchemyRefreshSessionStore:
446
291
  return FrameworkAsyncSession(self._engines.get("default"), expire_on_commit=False)
447
292
 
448
293
 
449
- async def _revoke_family(
450
- session: AsyncSession,
451
- family_id: UUID,
452
- *,
453
- now: datetime,
454
- ) -> None:
455
- rows = tuple(
456
- (
457
- await session.scalars(
458
- select(RefreshSessionRow)
459
- .where(RefreshSessionRow.family_id == family_id)
460
- .with_for_update()
461
- )
462
- ).all()
463
- )
464
- for row in rows:
465
- if row.revoked_at is None:
466
- row.revoked_at = now
467
-
468
-
469
- async def _lock_refresh_family(session: AsyncSession, family_id: UUID) -> None:
470
- # family 没有可锁的稳定 parent row;transaction advisory lock 在同一短事务内提供
471
- # family -> token/family rows 的统一顺序,并随 commit/rollback 自动释放。
472
- await session.scalar(select(func.pg_advisory_xact_lock(_refresh_family_lock_key(family_id))))
473
-
474
-
475
- def _refresh_family_lock_key(family_id: UUID) -> int:
476
- digest = blake2b(
477
- family_id.bytes,
478
- digest_size=8,
479
- person=_REFRESH_FAMILY_LOCK_PERSON,
480
- ).digest()
481
- return int.from_bytes(digest, byteorder="big", signed=True)
482
-
483
-
484
- def _session_is_active(row: RefreshSessionRow, now: datetime) -> bool:
485
- return (
486
- row.revoked_at is None
487
- and row.replaced_by_session_id is None
488
- and now - row.last_used_at < REFRESH_INACTIVITY_LIFETIME
489
- and now - row.family_created_at < REFRESH_ABSOLUTE_LIFETIME
490
- )
491
-
492
-
493
- def _user_value(row: UserRow) -> IdentityUser:
494
- return IdentityUser(
495
- user_id=row.user_id,
496
- username=row.username,
497
- normalized_username=row.normalized_username,
498
- password_hash=row.password_hash,
499
- is_active=row.is_active,
500
- failed_access_count=row.failed_access_count,
501
- lockout_end=row.lockout_end,
502
- permission_version=row.permission_version,
503
- )
504
-
505
-
506
- def _role_value(row: RoleRow) -> IdentityRole:
507
- return IdentityRole(
508
- role_id=row.role_id,
509
- name=row.name,
510
- normalized_name=row.normalized_name,
511
- permission_version=row.permission_version,
512
- )
513
-
514
-
515
294
  def _refresh_row(value: RefreshSession) -> RefreshSessionRow:
516
295
  return RefreshSessionRow(
517
296
  session_id=value.session_id,
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import secrets
7
7
  from dataclasses import dataclass
8
- from datetime import UTC, datetime, timedelta
8
+ from datetime import UTC, datetime
9
9
  from uuid import UUID, uuid4
10
10
 
11
11
  import jwt
@@ -14,9 +14,6 @@ from ..options import Options
14
14
  from .contracts import IdentityOptions
15
15
  from .errors import InvalidCredentialsError
16
16
 
17
- ACCESS_TOKEN_LIFETIME = timedelta(minutes=15)
18
- REFRESH_INACTIVITY_LIFETIME = timedelta(days=30)
19
- REFRESH_ABSOLUTE_LIFETIME = timedelta(days=90)
20
17
  ACCESS_TOKEN_TYPE = "access"
21
18
  BEARER_TOKEN_TYPE = "Bearer"
22
19
 
@@ -43,7 +40,7 @@ class JwtTokenService:
43
40
  "jti": str(uuid4()),
44
41
  "iat": issued,
45
42
  "nbf": issued,
46
- "exp": issued + ACCESS_TOKEN_LIFETIME,
43
+ "exp": issued + self._options.access_token_lifetime,
47
44
  "typ": ACCESS_TOKEN_TYPE,
48
45
  }
49
46
  key = self._options.keys[self._options.active_kid].get_secret_value()
@@ -6,13 +6,15 @@ from collections.abc import Mapping
6
6
 
7
7
  from pydantic import ValidationError
8
8
 
9
- from .catalog import ApplicationServiceRegistration, _CompiledMethod
10
- from .errors import ApplicationServiceValidationError, ApplicationServiceValidationIssue
9
+ from ..application_services.errors import (
10
+ ApplicationServiceValidationError,
11
+ ApplicationServiceValidationIssue,
12
+ )
13
+ from ..invocation.methods import _CompiledMethod
11
14
 
12
15
 
13
16
  async def _validate_arguments(
14
17
  method: _CompiledMethod,
15
- registration: ApplicationServiceRegistration,
16
18
  args: tuple[object, ...],
17
19
  kwargs: dict[str, object],
18
20
  ) -> tuple[tuple[object, ...], dict[str, object]]:
@@ -22,7 +24,7 @@ async def _validate_arguments(
22
24
  except ValidationError as original_error:
23
25
  validation_error = _sanitize_validation_error(
24
26
  original_error,
25
- registration=registration,
27
+ service=method.descriptor.service,
26
28
  method_name=method.name,
27
29
  parameter_names=method.parameter_names,
28
30
  )
@@ -33,7 +35,7 @@ async def _validate_arguments(
33
35
  def _sanitize_validation_error(
34
36
  error: ValidationError,
35
37
  *,
36
- registration: ApplicationServiceRegistration,
38
+ service: type[object],
37
39
  method_name: str,
38
40
  parameter_names: frozenset[str],
39
41
  ) -> ApplicationServiceValidationError:
@@ -51,7 +53,7 @@ def _sanitize_validation_error(
51
53
  )
52
54
  )
53
55
  return ApplicationServiceValidationError(
54
- service=registration.contract,
56
+ service=service,
55
57
  method_name=method_name,
56
58
  issues=issues,
57
59
  )
@@ -0,0 +1,61 @@
1
+ """拦截规则归调用能力持有,贡献有效期复用模块 hook 屏障。"""
2
+
3
+ import inspect
4
+ from collections.abc import Callable
5
+
6
+ from ..application_services.errors import ApplicationServiceDefinitionError
7
+ from ..diagnostics.source import caller_source
8
+ from ..invocation.interceptors import (
9
+ InterceptionStage,
10
+ Interceptor,
11
+ InterceptorRegistration,
12
+ _InterceptorRule,
13
+ )
14
+ from ..invocation.methods import ServiceMethod
15
+ from ..lifecycle import ConfigureContext, PostConfigureContext
16
+ from .module import InvocationModule
17
+
18
+
19
+ def add_interceptor(
20
+ context: ConfigureContext | PostConfigureContext,
21
+ interceptor: type[Interceptor],
22
+ *,
23
+ match: Callable[[ServiceMethod], bool],
24
+ before: InterceptionStage = InterceptionStage.METHOD,
25
+ ) -> None:
26
+ context.services.require_active()
27
+ context.modules.require_dependency(
28
+ context.descriptor.key,
29
+ InvocationModule,
30
+ source=context.descriptor.declaration_source,
31
+ )
32
+ if (
33
+ not isinstance(interceptor, type)
34
+ or not issubclass(interceptor, Interceptor)
35
+ or inspect.isabstract(interceptor)
36
+ or not inspect.iscoroutinefunction(interceptor.intercept)
37
+ ):
38
+ raise ApplicationServiceDefinitionError(
39
+ symbol=str(interceptor),
40
+ reason="interceptor must implement async intercept",
41
+ )
42
+ if (
43
+ not callable(match)
44
+ or inspect.iscoroutinefunction(match)
45
+ or not isinstance(before, InterceptionStage)
46
+ ):
47
+ raise ApplicationServiceDefinitionError(
48
+ symbol=interceptor.__qualname__,
49
+ reason="interceptor requires a synchronous match and valid position",
50
+ )
51
+ context.modules.instance(InvocationModule)._rules.append(
52
+ _InterceptorRule(
53
+ InterceptorRegistration(
54
+ interceptor,
55
+ before,
56
+ context.descriptor.key,
57
+ caller_source(str(context.descriptor.key), depth=2),
58
+ ),
59
+ match,
60
+ )
61
+ )