python-ddd-framework 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. python_ddd_framework/__init__.py +449 -0
  2. python_ddd_framework/application/__init__.py +6 -0
  3. python_ddd_framework/application/build_spec.py +37 -0
  4. python_ddd_framework/application/builder.py +260 -0
  5. python_ddd_framework/application/composition.py +219 -0
  6. python_ddd_framework/application/runtime.py +549 -0
  7. python_ddd_framework/application/runtime_hooks.py +327 -0
  8. python_ddd_framework/application/state.py +216 -0
  9. python_ddd_framework/application_services/__init__.py +54 -0
  10. python_ddd_framework/application_services/catalog.py +500 -0
  11. python_ddd_framework/application_services/contracts.py +190 -0
  12. python_ddd_framework/application_services/dispatcher.py +423 -0
  13. python_ddd_framework/application_services/errors.py +89 -0
  14. python_ddd_framework/application_services/execution.py +199 -0
  15. python_ddd_framework/application_services/interceptors.py +44 -0
  16. python_ddd_framework/application_services/invocation.py +431 -0
  17. python_ddd_framework/application_services/policies.py +46 -0
  18. python_ddd_framework/application_services/seeding.py +72 -0
  19. python_ddd_framework/application_services/signature.py +49 -0
  20. python_ddd_framework/application_services/validation.py +84 -0
  21. python_ddd_framework/auditing/__init__.py +12 -0
  22. python_ddd_framework/auditing/contracts.py +39 -0
  23. python_ddd_framework/auditing/control.py +48 -0
  24. python_ddd_framework/auditing/sqlalchemy/__init__.py +6 -0
  25. python_ddd_framework/auditing/sqlalchemy/migrations/0001_auditing.py +45 -0
  26. python_ddd_framework/auditing/sqlalchemy/migrations/__init__.py +1 -0
  27. python_ddd_framework/auditing/sqlalchemy/models.py +31 -0
  28. python_ddd_framework/auditing/sqlalchemy/module.py +32 -0
  29. python_ddd_framework/auditing/sqlalchemy/store.py +63 -0
  30. python_ddd_framework/authorization/__init__.py +38 -0
  31. python_ddd_framework/authorization/catalog.py +63 -0
  32. python_ddd_framework/authorization/contracts.py +241 -0
  33. python_ddd_framework/authorization/definition_discovery.py +81 -0
  34. python_ddd_framework/authorization/definitions.py +43 -0
  35. python_ddd_framework/authorization/errors.py +40 -0
  36. python_ddd_framework/background_execution/__init__.py +17 -0
  37. python_ddd_framework/background_execution/application.py +41 -0
  38. python_ddd_framework/background_execution/child.py +123 -0
  39. python_ddd_framework/background_execution/contracts.py +48 -0
  40. python_ddd_framework/background_execution/lifecycle.py +22 -0
  41. python_ddd_framework/background_execution/local.py +127 -0
  42. python_ddd_framework/background_execution/locks.py +33 -0
  43. python_ddd_framework/background_execution/management.py +17 -0
  44. python_ddd_framework/background_execution/module.py +10 -0
  45. python_ddd_framework/background_execution/permissions.py +14 -0
  46. python_ddd_framework/background_execution/processes.py +331 -0
  47. python_ddd_framework/background_jobs/__init__.py +31 -0
  48. python_ddd_framework/background_jobs/catalog.py +340 -0
  49. python_ddd_framework/background_jobs/contracts.py +208 -0
  50. python_ddd_framework/background_jobs/declaration.py +67 -0
  51. python_ddd_framework/background_jobs/errors.py +28 -0
  52. python_ddd_framework/background_jobs/execution.py +203 -0
  53. python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
  54. python_ddd_framework/background_jobs/pgqueuer/__init__.py +6 -0
  55. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +86 -0
  56. python_ddd_framework/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +95 -0
  57. python_ddd_framework/background_jobs/pgqueuer/migrations/__init__.py +1 -0
  58. python_ddd_framework/background_jobs/pgqueuer/module.py +106 -0
  59. python_ddd_framework/background_jobs/pgqueuer/options.py +82 -0
  60. python_ddd_framework/background_jobs/pgqueuer/runtime.py +479 -0
  61. python_ddd_framework/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
  62. python_ddd_framework/background_jobs/pgqueuer/supervision.py +201 -0
  63. python_ddd_framework/background_workers/__init__.py +22 -0
  64. python_ddd_framework/background_workers/catalog.py +118 -0
  65. python_ddd_framework/background_workers/contracts.py +152 -0
  66. python_ddd_framework/background_workers/errors.py +20 -0
  67. python_ddd_framework/background_workers/execution.py +133 -0
  68. python_ddd_framework/background_workers/runtime.py +219 -0
  69. python_ddd_framework/caching/__init__.py +11 -0
  70. python_ddd_framework/caching/catalog.py +73 -0
  71. python_ddd_framework/caching/contracts.py +73 -0
  72. python_ddd_framework/caching/errors.py +20 -0
  73. python_ddd_framework/cli/__init__.py +105 -0
  74. python_ddd_framework/cli/development.py +85 -0
  75. python_ddd_framework/cli/errors.py +5 -0
  76. python_ddd_framework/cli/inspection.py +96 -0
  77. python_ddd_framework/cli/project.py +69 -0
  78. python_ddd_framework/cli/runtime.py +56 -0
  79. python_ddd_framework/configuration/__init__.py +21 -0
  80. python_ddd_framework/configuration/composition.py +65 -0
  81. python_ddd_framework/configuration/contracts.py +68 -0
  82. python_ddd_framework/configuration/dotenv_source.py +51 -0
  83. python_ddd_framework/configuration/environment_source.py +38 -0
  84. python_ddd_framework/configuration/immutability.py +124 -0
  85. python_ddd_framework/configuration/input_shape.py +58 -0
  86. python_ddd_framework/configuration/merge.py +103 -0
  87. python_ddd_framework/configuration/root.py +215 -0
  88. python_ddd_framework/configuration/sources.py +519 -0
  89. python_ddd_framework/configuration/values.py +427 -0
  90. python_ddd_framework/configuration/yaml_source.py +51 -0
  91. python_ddd_framework/developer_kit/__init__.py +1 -0
  92. python_ddd_framework/developer_kit/generation.py +182 -0
  93. python_ddd_framework/developer_kit/project_metadata.py +45 -0
  94. python_ddd_framework/developer_kit/source.py +53 -0
  95. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -0
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +70 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +41 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +20 -0
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +7 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +83 -0
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +84 -0
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +35 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +56 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +14 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +73 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +25 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +36 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +7 -0
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/orders.py.jinja +25 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +26 -0
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/__init__.py.jinja +1 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +49 -0
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
  131. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +6 -0
  132. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +9 -0
  133. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
  134. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
  135. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +38 -0
  136. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
  137. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +130 -0
  138. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/app.development.yaml.jinja +30 -0
  139. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
  140. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +20 -0
  141. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +84 -0
  142. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +185 -0
  143. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
  144. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
  145. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +47 -0
  146. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +37 -0
  147. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
  148. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +57 -0
  149. python_ddd_framework/developer_kit/wiring.py +162 -0
  150. python_ddd_framework/diagnostics/__init__.py +23 -0
  151. python_ddd_framework/diagnostics/journal.py +174 -0
  152. python_ddd_framework/diagnostics/model.py +83 -0
  153. python_ddd_framework/diagnostics/source.py +25 -0
  154. python_ddd_framework/distributed_lock/__init__.py +6 -0
  155. python_ddd_framework/distributed_lock/contracts.py +20 -0
  156. python_ddd_framework/distributed_lock/options.py +19 -0
  157. python_ddd_framework/domain/__init__.py +13 -0
  158. python_ddd_framework/domain/aggregates.py +64 -0
  159. python_ddd_framework/domain/entities.py +33 -0
  160. python_ddd_framework/domain/errors.py +12 -0
  161. python_ddd_framework/domain/value_objects.py +31 -0
  162. python_ddd_framework/errors/__init__.py +75 -0
  163. python_ddd_framework/errors/base.py +13 -0
  164. python_ddd_framework/errors/business.py +53 -0
  165. python_ddd_framework/errors/configuration.py +119 -0
  166. python_ddd_framework/errors/diagnostics.py +12 -0
  167. python_ddd_framework/errors/lifecycle.py +164 -0
  168. python_ddd_framework/errors/modularity.py +100 -0
  169. python_ddd_framework/errors/services.py +100 -0
  170. python_ddd_framework/events/__init__.py +25 -0
  171. python_ddd_framework/events/aggregate.py +39 -0
  172. python_ddd_framework/events/catalog.py +136 -0
  173. python_ddd_framework/events/contracts.py +39 -0
  174. python_ddd_framework/events/contribution.py +128 -0
  175. python_ddd_framework/events/discovery.py +105 -0
  176. python_ddd_framework/events/errors.py +48 -0
  177. python_ddd_framework/events/runtime.py +192 -0
  178. python_ddd_framework/fastapi/__init__.py +49 -0
  179. python_ddd_framework/fastapi/action.py +175 -0
  180. python_ddd_framework/fastapi/adapter.py +391 -0
  181. python_ddd_framework/fastapi/application_services.py +654 -0
  182. python_ddd_framework/fastapi/background.py +42 -0
  183. python_ddd_framework/fastapi/contracts.py +275 -0
  184. python_ddd_framework/fastapi/errors.py +32 -0
  185. python_ddd_framework/fastapi/filters.py +111 -0
  186. python_ddd_framework/fastapi/health.py +44 -0
  187. python_ddd_framework/fastapi/http.py +72 -0
  188. python_ddd_framework/fastapi/http_router.py +216 -0
  189. python_ddd_framework/fastapi/manual_action.py +105 -0
  190. python_ddd_framework/fastapi/middleware.py +93 -0
  191. python_ddd_framework/fastapi/parameters.py +97 -0
  192. python_ddd_framework/fastapi/realtime/__init__.py +13 -0
  193. python_ddd_framework/fastapi/realtime/authentication.py +181 -0
  194. python_ddd_framework/fastapi/realtime/connection.py +166 -0
  195. python_ddd_framework/fastapi/realtime/module.py +41 -0
  196. python_ddd_framework/fastapi/realtime/options.py +50 -0
  197. python_ddd_framework/fastapi/realtime/runtime.py +423 -0
  198. python_ddd_framework/fastapi/request_context.py +247 -0
  199. python_ddd_framework/fastapi/route_integrity.py +88 -0
  200. python_ddd_framework/fastapi/routing.py +424 -0
  201. python_ddd_framework/fastapi/server.py +182 -0
  202. python_ddd_framework/fastapi/settings.py +40 -0
  203. python_ddd_framework/fastapi/tracing.py +62 -0
  204. python_ddd_framework/fastapi/transfer.py +128 -0
  205. python_ddd_framework/fastapi/upload_limits.py +63 -0
  206. python_ddd_framework/fastapi/uploads.py +94 -0
  207. python_ddd_framework/hosted_services/__init__.py +29 -0
  208. python_ddd_framework/hosted_services/bridge.py +420 -0
  209. python_ddd_framework/hosted_services/catalog.py +265 -0
  210. python_ddd_framework/hosted_services/contracts.py +55 -0
  211. python_ddd_framework/hosted_services/errors.py +53 -0
  212. python_ddd_framework/hosted_services/options.py +17 -0
  213. python_ddd_framework/hosted_services/runtime.py +267 -0
  214. python_ddd_framework/hosted_services/state.py +40 -0
  215. python_ddd_framework/hosting/__init__.py +4 -0
  216. python_ddd_framework/hosting/instance.py +40 -0
  217. python_ddd_framework/identity/__init__.py +73 -0
  218. python_ddd_framework/identity/application.py +287 -0
  219. python_ddd_framework/identity/contracts.py +283 -0
  220. python_ddd_framework/identity/errors.py +28 -0
  221. python_ddd_framework/identity/http_api.py +90 -0
  222. python_ddd_framework/identity/module.py +49 -0
  223. python_ddd_framework/identity/passwords.py +34 -0
  224. python_ddd_framework/identity/permissions.py +14 -0
  225. python_ddd_framework/identity/services.py +108 -0
  226. python_ddd_framework/identity/sqlalchemy/__init__.py +5 -0
  227. python_ddd_framework/identity/sqlalchemy/migrations/0001_identity.py +143 -0
  228. python_ddd_framework/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
  229. python_ddd_framework/identity/sqlalchemy/migrations/__init__.py +1 -0
  230. python_ddd_framework/identity/sqlalchemy/models.py +85 -0
  231. python_ddd_framework/identity/sqlalchemy/module.py +62 -0
  232. python_ddd_framework/identity/sqlalchemy/stores.py +526 -0
  233. python_ddd_framework/identity/tokens.py +106 -0
  234. python_ddd_framework/invocation/__init__.py +3 -0
  235. python_ddd_framework/invocation/callables.py +171 -0
  236. python_ddd_framework/invocation/contracts.py +33 -0
  237. python_ddd_framework/invocation/entries.py +54 -0
  238. python_ddd_framework/invocation/function_runtime.py +48 -0
  239. python_ddd_framework/invocation/interception.py +193 -0
  240. python_ddd_framework/lifecycle/__init__.py +25 -0
  241. python_ddd_framework/lifecycle/composition.py +35 -0
  242. python_ddd_framework/lifecycle/context.py +31 -0
  243. python_ddd_framework/lifecycle/runtime.py +43 -0
  244. python_ddd_framework/lifecycle/state.py +20 -0
  245. python_ddd_framework/modularity/__init__.py +15 -0
  246. python_ddd_framework/modularity/contracts.py +91 -0
  247. python_ddd_framework/modularity/discovery.py +157 -0
  248. python_ddd_framework/modularity/graph.py +334 -0
  249. python_ddd_framework/modularity/registry.py +100 -0
  250. python_ddd_framework/modularity/selection.py +16 -0
  251. python_ddd_framework/notifications/__init__.py +19 -0
  252. python_ddd_framework/notifications/catalog.py +46 -0
  253. python_ddd_framework/notifications/contracts.py +107 -0
  254. python_ddd_framework/observability/__init__.py +5 -0
  255. python_ddd_framework/observability/context.py +43 -0
  256. python_ddd_framework/observability/export.py +73 -0
  257. python_ddd_framework/observability/formatting.py +89 -0
  258. python_ddd_framework/observability/logging.py +118 -0
  259. python_ddd_framework/observability/options.py +45 -0
  260. python_ddd_framework/observability/tracing.py +80 -0
  261. python_ddd_framework/options/__init__.py +19 -0
  262. python_ddd_framework/options/aliases.py +331 -0
  263. python_ddd_framework/options/contribution.py +62 -0
  264. python_ddd_framework/options/immutability.py +48 -0
  265. python_ddd_framework/options/input_keys.py +214 -0
  266. python_ddd_framework/options/issues.py +335 -0
  267. python_ddd_framework/options/models.py +114 -0
  268. python_ddd_framework/options/registry.py +280 -0
  269. python_ddd_framework/options/schema.py +488 -0
  270. python_ddd_framework/options/validation.py +120 -0
  271. python_ddd_framework/py.typed +0 -0
  272. python_ddd_framework/realtime/__init__.py +12 -0
  273. python_ddd_framework/realtime/contracts.py +28 -0
  274. python_ddd_framework/realtime/diagnostics.py +37 -0
  275. python_ddd_framework/realtime/messages.py +113 -0
  276. python_ddd_framework/redis/__init__.py +6 -0
  277. python_ddd_framework/redis/distributed_lock.py +212 -0
  278. python_ddd_framework/redis/lease_lock.py +36 -0
  279. python_ddd_framework/redis/module.py +50 -0
  280. python_ddd_framework/redis/notification_runtime.py +182 -0
  281. python_ddd_framework/redis/notifications.py +25 -0
  282. python_ddd_framework/redis/options.py +35 -0
  283. python_ddd_framework/redis/runtime.py +116 -0
  284. python_ddd_framework/services/__init__.py +41 -0
  285. python_ddd_framework/services/application_bindings.py +221 -0
  286. python_ddd_framework/services/arbitration.py +381 -0
  287. python_ddd_framework/services/binding.py +102 -0
  288. python_ddd_framework/services/contribution.py +438 -0
  289. python_ddd_framework/services/convention.py +301 -0
  290. python_ddd_framework/services/convention_contracts.py +101 -0
  291. python_ddd_framework/services/exposure.py +23 -0
  292. python_ddd_framework/services/fixed_lifetime.py +52 -0
  293. python_ddd_framework/services/framework_provider.py +178 -0
  294. python_ddd_framework/services/native_graph.py +201 -0
  295. python_ddd_framework/services/provider.py +204 -0
  296. python_ddd_framework/services/registration.py +101 -0
  297. python_ddd_framework/services/repository.py +38 -0
  298. python_ddd_framework/services/runtime.py +302 -0
  299. python_ddd_framework/settings/__init__.py +30 -0
  300. python_ddd_framework/settings/application.py +95 -0
  301. python_ddd_framework/settings/binding.py +33 -0
  302. python_ddd_framework/settings/catalog.py +121 -0
  303. python_ddd_framework/settings/changes.py +14 -0
  304. python_ddd_framework/settings/contracts.py +115 -0
  305. python_ddd_framework/settings/definition_discovery.py +71 -0
  306. python_ddd_framework/settings/definitions.py +41 -0
  307. python_ddd_framework/settings/errors.py +22 -0
  308. python_ddd_framework/settings/handlers.py +43 -0
  309. python_ddd_framework/settings/management.py +57 -0
  310. python_ddd_framework/settings/manager.py +89 -0
  311. python_ddd_framework/settings/module.py +10 -0
  312. python_ddd_framework/settings/notifications.py +39 -0
  313. python_ddd_framework/settings/permissions.py +14 -0
  314. python_ddd_framework/settings/provider.py +74 -0
  315. python_ddd_framework/settings/refresh.py +140 -0
  316. python_ddd_framework/settings/refresh_module.py +45 -0
  317. python_ddd_framework/settings/sqlalchemy/__init__.py +5 -0
  318. python_ddd_framework/settings/sqlalchemy/migrations/0001_settings.py +31 -0
  319. python_ddd_framework/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
  320. python_ddd_framework/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
  321. python_ddd_framework/settings/sqlalchemy/migrations/__init__.py +1 -0
  322. python_ddd_framework/settings/sqlalchemy/models.py +36 -0
  323. python_ddd_framework/settings/sqlalchemy/module.py +36 -0
  324. python_ddd_framework/settings/sqlalchemy/store.py +86 -0
  325. python_ddd_framework/settings/store.py +16 -0
  326. python_ddd_framework/settings/values.py +39 -0
  327. python_ddd_framework/sqlalchemy/__init__.py +54 -0
  328. python_ddd_framework/sqlalchemy/alembic_runtime/__init__.py +1 -0
  329. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +33 -0
  330. python_ddd_framework/sqlalchemy/alembic_runtime/script.py.mako +14 -0
  331. python_ddd_framework/sqlalchemy/auditing.py +48 -0
  332. python_ddd_framework/sqlalchemy/errors.py +85 -0
  333. python_ddd_framework/sqlalchemy/metadata.py +576 -0
  334. python_ddd_framework/sqlalchemy/migration.py +378 -0
  335. python_ddd_framework/sqlalchemy/migration_options.py +56 -0
  336. python_ddd_framework/sqlalchemy/module.py +51 -0
  337. python_ddd_framework/sqlalchemy/module_migration.py +151 -0
  338. python_ddd_framework/sqlalchemy/options.py +66 -0
  339. python_ddd_framework/sqlalchemy/repository.py +33 -0
  340. python_ddd_framework/sqlalchemy/runtime.py +85 -0
  341. python_ddd_framework/sqlalchemy/session_provider.py +45 -0
  342. python_ddd_framework/sqlalchemy/unit_of_work.py +97 -0
  343. python_ddd_framework/testing/__init__.py +5 -0
  344. python_ddd_framework/testing/runtime.py +112 -0
  345. python_ddd_framework/unit_of_work/__init__.py +14 -0
  346. python_ddd_framework/unit_of_work/contracts.py +223 -0
  347. python_ddd_framework/unit_of_work/errors.py +27 -0
  348. python_ddd_framework/unit_of_work/manager.py +210 -0
  349. python_ddd_framework/unit_of_work/options.py +92 -0
  350. python_ddd_framework-0.3.1.dist-info/METADATA +379 -0
  351. python_ddd_framework-0.3.1.dist-info/RECORD +355 -0
  352. python_ddd_framework-0.3.1.dist-info/WHEEL +4 -0
  353. python_ddd_framework-0.3.1.dist-info/entry_points.txt +9 -0
  354. python_ddd_framework-0.3.1.dist-info/licenses/LICENSE +7 -0
  355. python_ddd_framework-0.3.1.dist-info/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
@@ -0,0 +1,201 @@
1
+ """PgQueuer manager readiness、fatal completion 与 task ownership。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Awaitable, Callable
7
+ from dataclasses import dataclass, field
8
+
9
+ from ...errors.lifecycle import ApplicationRuntimeError
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class _PgQueuerManagerBinding:
14
+ component: str
15
+ task_name: str
16
+ ready: asyncio.Event
17
+ run: Callable[[], Awaitable[None]]
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class _PgQueuerManagerFailure:
22
+ component: str
23
+ error_type: str
24
+ _source: object = field(default_factory=object, repr=False, compare=False)
25
+
26
+
27
+ class _PgQueuerManagerError(ApplicationRuntimeError):
28
+ """保留 provider 启动失败的类型边界;运行期事实使用已有 ApplicationRuntimeError 契约。"""
29
+
30
+
31
+ class _PgQueuerManagerSupervisor:
32
+ """单个 provider runtime 的 manager owner;只发布脱敏失败事实。"""
33
+
34
+ __slots__ = (
35
+ "_failure",
36
+ "_reported_tasks",
37
+ "_startup_complete",
38
+ "_stopping",
39
+ "_task_components",
40
+ "_task_failures",
41
+ "_tasks",
42
+ )
43
+
44
+ def __init__(self) -> None:
45
+ self._failure: asyncio.Future[_PgQueuerManagerFailure] | None = None
46
+ self._startup_complete = False
47
+ self._stopping = False
48
+ self._task_components: dict[asyncio.Task[None], str] = {}
49
+ self._tasks: tuple[asyncio.Task[None], ...] = ()
50
+ self._reported_tasks: set[asyncio.Task[None]] = set()
51
+ self._task_failures: dict[asyncio.Task[None], _PgQueuerManagerFailure] = {}
52
+
53
+ async def start(self, bindings: tuple[_PgQueuerManagerBinding, ...]) -> None:
54
+ if self._failure is not None or self._tasks:
55
+ raise RuntimeError("PgQueuer manager supervisor may only start once")
56
+ if not bindings:
57
+ raise RuntimeError("PgQueuer manager supervisor requires an enabled role")
58
+
59
+ self._failure = asyncio.get_running_loop().create_future()
60
+ tasks_by_binding = {
61
+ asyncio.create_task(self._run(binding), name=binding.task_name): binding
62
+ for binding in bindings
63
+ }
64
+ self._tasks = tuple(tasks_by_binding)
65
+ self._task_components = {
66
+ task: binding.component for task, binding in tasks_by_binding.items()
67
+ }
68
+ readiness = {
69
+ asyncio.create_task(
70
+ binding.ready.wait(),
71
+ name=f"{binding.task_name}-readiness",
72
+ ): binding
73
+ for binding in bindings
74
+ }
75
+ try:
76
+ pending_readiness = set(readiness)
77
+ while pending_readiness:
78
+ done, _ = await asyncio.wait(
79
+ (*pending_readiness, *self._tasks),
80
+ return_when=asyncio.FIRST_COMPLETED,
81
+ )
82
+ failed = next((task for task in self._tasks if task in done), None)
83
+ if failed is not None:
84
+ binding = tasks_by_binding[failed]
85
+ raise self._manager_error(binding.component, failed)
86
+ pending_readiness.difference_update(done)
87
+
88
+ # readiness callback 返回只说明 bootstrap I/O 已成功;再让 manager 前进一步,
89
+ # 防止 callback 后立即失败却被误报为 Application 已启动。
90
+ self._startup_complete = True
91
+ await asyncio.sleep(0)
92
+ failed = next((task for task in self._tasks if task.done()), None)
93
+ if failed is not None:
94
+ self._startup_complete = False
95
+ binding = tasks_by_binding[failed]
96
+ raise self._manager_error(binding.component, failed)
97
+ except BaseException:
98
+ self._startup_complete = False
99
+ self.begin_stop()
100
+ self.cancel()
101
+ await self._collect_outcomes()
102
+ raise
103
+ finally:
104
+ for task in readiness:
105
+ task.cancel()
106
+ if readiness:
107
+ await asyncio.gather(*readiness, return_exceptions=True)
108
+
109
+ def begin_stop(self) -> None:
110
+ self._stopping = True
111
+
112
+ async def wait(self) -> None:
113
+ cancellation: asyncio.CancelledError | None = None
114
+ try:
115
+ outcomes = await self._collect_outcomes()
116
+ except asyncio.CancelledError as error:
117
+ # deadline 取消 gather 时,已失败的 peer 仍须进入终止聚合,不能被取消掩盖。
118
+ cancellation = error
119
+ outcomes = await self._collect_outcomes()
120
+ failures: list[BaseException] = []
121
+ for task, outcome in zip(self._tasks, outcomes, strict=True):
122
+ fact = self._task_failures.get(task)
123
+ if (
124
+ fact is not None
125
+ or (
126
+ isinstance(outcome, BaseException)
127
+ and not isinstance(outcome, asyncio.CancelledError)
128
+ )
129
+ ) and task not in self._reported_tasks:
130
+ manager_error = _PgQueuerManagerError(
131
+ component=self._task_components[task],
132
+ error_type=fact.error_type if fact is not None else type(outcome).__name__,
133
+ )
134
+ if fact is not None:
135
+ manager_error._source = fact._source
136
+ if isinstance(outcome, BaseException):
137
+ manager_error.__cause__ = outcome
138
+ failures.append(manager_error)
139
+ self._reported_tasks.add(task)
140
+ if failures:
141
+ raise BaseExceptionGroup(
142
+ "PgQueuer manager drain failed", tuple(failures)
143
+ ) from cancellation
144
+ if cancellation is not None:
145
+ raise cancellation
146
+
147
+ async def wait_for_failure(self) -> _PgQueuerManagerFailure:
148
+ failure = self._failure
149
+ if failure is None:
150
+ raise RuntimeError("PgQueuer manager supervisor is not started")
151
+ return await asyncio.shield(failure)
152
+
153
+ def cancel(self) -> None:
154
+ for task in self._tasks:
155
+ task.cancel()
156
+
157
+ async def close(self) -> None:
158
+ self.begin_stop()
159
+ # bootstrap 已报告的原始失败不会重复计数;取消 peer 时 finally 的失败仍须可观察。
160
+ await self.wait()
161
+ self._tasks = ()
162
+ self._task_components = {}
163
+ self._task_failures.clear()
164
+ self._reported_tasks.clear()
165
+
166
+ async def _run(self, binding: _PgQueuerManagerBinding) -> None:
167
+ try:
168
+ await binding.run()
169
+ except BaseException as original_error:
170
+ if self._startup_complete and not self._stopping:
171
+ self._publish_failure(binding.component, type(original_error).__name__)
172
+ raise
173
+ if self._startup_complete and not self._stopping:
174
+ self._publish_failure(binding.component, "UnexpectedCompletion")
175
+
176
+ def _publish_failure(self, component: str, error_type: str) -> None:
177
+ task = asyncio.current_task()
178
+ assert task is not None
179
+ fact = _PgQueuerManagerFailure(component=component, error_type=error_type)
180
+ self._task_failures[task] = fact
181
+ failure = self._failure
182
+ if failure is not None and not failure.done():
183
+ failure.set_result(fact)
184
+ # 发布不等于 Application 已接收;同源去重只能由最终终止聚合 owner 完成。
185
+
186
+ async def _collect_outcomes(self) -> tuple[object, ...]:
187
+ if not self._tasks:
188
+ return ()
189
+ return tuple(await asyncio.gather(*self._tasks, return_exceptions=True))
190
+
191
+ def _manager_error(self, component: str, task: asyncio.Task[None]) -> _PgQueuerManagerError:
192
+ self._reported_tasks.add(task)
193
+ if task.cancelled():
194
+ error_type = asyncio.CancelledError.__name__
195
+ else:
196
+ error = task.exception()
197
+ error_type = "UnexpectedCompletion" if error is None else type(error).__name__
198
+ failure = _PgQueuerManagerError(component=component, error_type=error_type)
199
+ if not task.cancelled():
200
+ failure.__cause__ = task.exception()
201
+ return failure
@@ -0,0 +1,22 @@
1
+ """Provider-neutral non-durable background worker capability。"""
2
+
3
+ from .catalog import BackgroundWorkerCatalog
4
+ from .contracts import (
5
+ BackgroundExecutionOptions,
6
+ BackgroundExecutionProfile,
7
+ BackgroundWorker,
8
+ BackgroundWorkerContext,
9
+ BackgroundWorkerDefinition,
10
+ )
11
+ from .errors import BackgroundWorkerDefinitionError, BackgroundWorkerRuntimeError
12
+
13
+ __all__ = (
14
+ "BackgroundExecutionOptions",
15
+ "BackgroundExecutionProfile",
16
+ "BackgroundWorker",
17
+ "BackgroundWorkerCatalog",
18
+ "BackgroundWorkerContext",
19
+ "BackgroundWorkerDefinition",
20
+ "BackgroundWorkerDefinitionError",
21
+ "BackgroundWorkerRuntimeError",
22
+ )
@@ -0,0 +1,118 @@
1
+ """Module-owned worker metadata 的 Application-local immutable catalog。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from collections.abc import Iterator, Mapping
8
+ from types import MappingProxyType
9
+
10
+ from ..modularity import AppModule, ModuleKey, ModuleRegistry
11
+ from .contracts import BackgroundWorker, BackgroundWorkerDefinition, _definition_from_type
12
+ from .errors import BackgroundWorkerDefinitionError
13
+
14
+
15
+ class BackgroundWorkerCatalog:
16
+ __slots__ = ("_declarations", "_definitions", "_fingerprint", "_owners")
17
+
18
+ def __init__(
19
+ self,
20
+ definitions: tuple[BackgroundWorkerDefinition, ...],
21
+ *,
22
+ owners: Mapping[str, ModuleKey],
23
+ declarations: tuple[tuple[type[AppModule], str, tuple[object, ...]], ...],
24
+ ) -> None:
25
+ self._definitions = MappingProxyType({item.name: item for item in definitions})
26
+ self._owners = MappingProxyType(dict(owners))
27
+ self._declarations = declarations
28
+ encoded = json.dumps(
29
+ [
30
+ {
31
+ "name": item.name,
32
+ "enabled": item.enabled,
33
+ "owner": str(self._owners[item.name]),
34
+ "interval_seconds": item.interval.total_seconds(),
35
+ "timeout_seconds": item.iteration_timeout.total_seconds(),
36
+ }
37
+ for item in definitions
38
+ ],
39
+ sort_keys=True,
40
+ separators=(",", ":"),
41
+ ).encode()
42
+ self._fingerprint = hashlib.sha256(encoded).hexdigest()
43
+
44
+ @property
45
+ def definitions(self) -> tuple[BackgroundWorkerDefinition, ...]:
46
+ return tuple(self._definitions.values())
47
+
48
+ @property
49
+ def fingerprint(self) -> str:
50
+ return self._fingerprint
51
+
52
+ def __iter__(self) -> Iterator[str]:
53
+ return iter(self._definitions)
54
+
55
+ def get(self, name: str) -> BackgroundWorkerDefinition | None:
56
+ return self._definitions.get(name)
57
+
58
+ def owner_for(self, name: str) -> ModuleKey:
59
+ return self._owners[name]
60
+
61
+ def _assert_unchanged(self) -> None:
62
+ for definition in self.definitions:
63
+ if isinstance(definition.worker_type, type) and (
64
+ _definition_from_type(definition.worker_type) != definition
65
+ ):
66
+ raise BackgroundWorkerDefinitionError(
67
+ worker=definition.name, reason="Worker class metadata changed after build"
68
+ )
69
+ for module_type, attribute, declared in self._declarations:
70
+ if module_type.__dict__.get(attribute, ()) is not declared:
71
+ raise BackgroundWorkerDefinitionError(
72
+ worker=f"{module_type.__module__}.{module_type.__qualname__}",
73
+ reason="Module background worker metadata changed after build",
74
+ )
75
+
76
+
77
+ def _build_background_worker_catalog(registry: ModuleRegistry) -> BackgroundWorkerCatalog:
78
+ definitions: list[BackgroundWorkerDefinition] = []
79
+ owners: dict[str, ModuleKey] = {}
80
+ declarations: list[tuple[type[AppModule], str, tuple[object, ...]]] = []
81
+ for descriptor in registry.dependency_order:
82
+ for attribute in ("background_workers", "background_worker_definitions"):
83
+ declared = descriptor.module_type.__dict__.get(attribute, ())
84
+ if not isinstance(declared, tuple):
85
+ raise BackgroundWorkerDefinitionError(
86
+ worker=str(descriptor.key),
87
+ reason=f"Module {attribute} must be an immutable tuple",
88
+ )
89
+ declarations.append((descriptor.module_type, attribute, declared))
90
+ for candidate in declared:
91
+ definition = _interpret_worker(candidate, attribute)
92
+ previous = owners.get(definition.name)
93
+ if previous is not None:
94
+ raise BackgroundWorkerDefinitionError(
95
+ worker=definition.name,
96
+ reason=f"worker is already owned by {previous}",
97
+ )
98
+ owners[definition.name] = descriptor.key
99
+ definitions.append(definition)
100
+ return BackgroundWorkerCatalog(
101
+ tuple(definitions),
102
+ owners=owners,
103
+ declarations=tuple(declarations),
104
+ )
105
+
106
+
107
+ def _interpret_worker(candidate: object, attribute: str) -> BackgroundWorkerDefinition:
108
+ if attribute == "background_workers":
109
+ if isinstance(candidate, type) and issubclass(candidate, BackgroundWorker):
110
+ return _definition_from_type(candidate)
111
+ elif isinstance(candidate, BackgroundWorkerDefinition) and not isinstance(
112
+ candidate.worker_type, type
113
+ ):
114
+ return candidate
115
+ raise BackgroundWorkerDefinitionError(
116
+ worker=attribute,
117
+ reason="background_workers requires Worker types; definitions require async functions",
118
+ )
@@ -0,0 +1,152 @@
1
+ """Non-durable worker contract 与 Host-owned execution options。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ from abc import ABC, abstractmethod
8
+ from collections.abc import Awaitable, Callable, Mapping
9
+ from dataclasses import dataclass, field
10
+ from datetime import timedelta
11
+ from enum import Enum
12
+ from typing import ClassVar, cast
13
+
14
+ from pydantic import ConfigDict, Field, field_serializer
15
+
16
+ from ..background_execution import BackgroundJobOptions, BackgroundTaskOptions
17
+ from ..invocation import BackgroundSystemCaller
18
+ from ..options import BaseOptions
19
+ from .errors import BackgroundWorkerDefinitionError
20
+
21
+
22
+ class BackgroundExecutionProfile(str, Enum):
23
+ """Host 对当前 Application background execution ownership 的显式选择。"""
24
+
25
+ DISABLED = "disabled"
26
+ EMBEDDED = "embedded"
27
+ DEDICATED = "dedicated"
28
+
29
+
30
+ class BackgroundExecutionOptions(BaseOptions):
31
+ """Application 准入和关闭策略;任务声明仍由 Module catalog 唯一拥有。"""
32
+
33
+ model_config = ConfigDict(extra="forbid")
34
+
35
+ profile: BackgroundExecutionProfile = BackgroundExecutionProfile.EMBEDDED
36
+ workers: Mapping[str, BackgroundTaskOptions] = Field(default_factory=dict)
37
+ jobs: Mapping[str, BackgroundJobOptions] = Field(default_factory=dict)
38
+ max_restarts: int = Field(default=3, ge=0)
39
+ restart_interval: timedelta = Field(default=timedelta(seconds=1), gt=timedelta(0))
40
+ graceful_shutdown_timeout: timedelta = Field(
41
+ default=timedelta(seconds=30),
42
+ gt=timedelta(0),
43
+ )
44
+
45
+ @field_serializer("workers", "jobs")
46
+ def _serialize_tasks(
47
+ self, value: Mapping[str, BackgroundJobOptions]
48
+ ) -> dict[str, BackgroundJobOptions]:
49
+ # 发布后的 Mapping 已冻结;原生 serializer 接收普通 dict 后继续处理内部 typed Options。
50
+ return dict(value)
51
+
52
+
53
+ class BackgroundWorker(ABC):
54
+ """一次 iteration 的实现边界;永久 loop 由 Application coordinator 独占。"""
55
+
56
+ name: ClassVar[str]
57
+ interval: ClassVar[timedelta]
58
+ iteration_timeout: ClassVar[timedelta]
59
+ enabled: ClassVar[bool] = True
60
+
61
+ @abstractmethod
62
+ async def run_iteration(self, context: BackgroundWorkerContext) -> None:
63
+ raise NotImplementedError
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class BackgroundWorkerContext:
68
+ name: str
69
+ system_caller: BackgroundSystemCaller
70
+ _stop_event: asyncio.Event = field(repr=False, compare=False)
71
+
72
+ @property
73
+ def stop_requested(self) -> bool:
74
+ return self._stop_event.is_set()
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class BackgroundWorkerDefinition:
79
+ name: str
80
+ worker_type: type[BackgroundWorker] | Callable[..., Awaitable[None]]
81
+ interval: timedelta
82
+ iteration_timeout: timedelta
83
+ enabled: bool = True
84
+
85
+ def __post_init__(self) -> None:
86
+ _validate_name(self.name)
87
+ if not isinstance(self.enabled, bool):
88
+ raise BackgroundWorkerDefinitionError(worker=self.name, reason="enabled must be bool")
89
+ is_class = isinstance(self.worker_type, type) and issubclass(
90
+ self.worker_type, BackgroundWorker
91
+ )
92
+ if not is_class and not inspect.iscoroutinefunction(self.worker_type):
93
+ raise BackgroundWorkerDefinitionError(
94
+ worker=self.name,
95
+ reason="worker_type must be a BackgroundWorker class or async function",
96
+ )
97
+ if isinstance(self.worker_type, type) and not inspect.iscoroutinefunction(
98
+ cast(type[BackgroundWorker], self.worker_type).run_iteration
99
+ ):
100
+ raise BackgroundWorkerDefinitionError(
101
+ worker=self.name,
102
+ reason="run_iteration must be async",
103
+ )
104
+ if not isinstance(self.interval, timedelta) or self.interval.total_seconds() <= 0:
105
+ raise BackgroundWorkerDefinitionError(
106
+ worker=self.name,
107
+ reason="interval must be a positive timedelta",
108
+ )
109
+ if (
110
+ not isinstance(self.iteration_timeout, timedelta)
111
+ or self.iteration_timeout.total_seconds() <= 0
112
+ ):
113
+ raise BackgroundWorkerDefinitionError(
114
+ worker=self.name,
115
+ reason="iteration_timeout must be a positive timedelta",
116
+ )
117
+
118
+
119
+ def _definition_from_type(candidate: type[BackgroundWorker]) -> BackgroundWorkerDefinition:
120
+ if inspect.isabstract(candidate):
121
+ raise BackgroundWorkerDefinitionError(
122
+ worker=candidate.__qualname__, reason="Worker must be concrete"
123
+ )
124
+ try:
125
+ return BackgroundWorkerDefinition(
126
+ name=candidate.name,
127
+ worker_type=candidate,
128
+ interval=candidate.interval,
129
+ iteration_timeout=candidate.iteration_timeout,
130
+ enabled=candidate.enabled,
131
+ )
132
+ except AttributeError as error:
133
+ raise BackgroundWorkerDefinitionError(
134
+ worker=candidate.__qualname__,
135
+ reason="Worker must declare name, interval and iteration_timeout",
136
+ ) from error
137
+
138
+
139
+ def _validate_name(name: str) -> None:
140
+ if (
141
+ not isinstance(name, str)
142
+ or not name
143
+ or name != name.strip()
144
+ or name.startswith(".")
145
+ or name.endswith(".")
146
+ or ".." in name
147
+ or any(not (character.isalnum() or character in "._-") for character in name)
148
+ ):
149
+ raise BackgroundWorkerDefinitionError(
150
+ worker=name if isinstance(name, str) else repr(name),
151
+ reason="name must be a trimmed stable identifier",
152
+ )
@@ -0,0 +1,20 @@
1
+ """Non-durable background worker definition 与 runtime failures。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..errors.base import FrameworkError
6
+
7
+
8
+ class BackgroundWorkerDefinitionError(FrameworkError):
9
+ def __init__(self, *, worker: str, reason: str) -> None:
10
+ self.worker = worker
11
+ self.reason = reason
12
+ super().__init__(f"Invalid background worker {worker!r}: {reason}")
13
+
14
+
15
+ class BackgroundWorkerRuntimeError(FrameworkError):
16
+ def __init__(self, *, worker: str, operation: str, error_type: str) -> None:
17
+ self.worker = worker
18
+ self.operation = operation
19
+ self.error_type = error_type
20
+ super().__init__(f"Background worker {worker!r} {operation} failed with {error_type}")
@@ -0,0 +1,133 @@
1
+ """单个 Worker 的 fixed-delay loop 与每次执行的原生作用域。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from contextlib import AsyncExitStack
8
+ from uuid import uuid4
9
+
10
+ from dishka import AsyncContainer, Scope
11
+
12
+ from ..application_services.contracts import _DEFAULT_CONNECTION_NAME
13
+ from ..application_services.errors import ApplicationInvocationRejectedError
14
+ from ..application_services.invocation import _InvocationRuntime
15
+ from ..authorization import CurrentUser
16
+ from ..background_execution.lifecycle import _TaskExecution
17
+ from ..invocation import BackgroundSystemCaller
18
+ from .contracts import BackgroundWorkerContext, BackgroundWorkerDefinition
19
+
20
+ _LOGGER = logging.getLogger(__name__)
21
+
22
+
23
+ class _WorkerExecution:
24
+ def __init__(
25
+ self,
26
+ definition: BackgroundWorkerDefinition,
27
+ container: AsyncContainer,
28
+ invocation_runtime: _InvocationRuntime,
29
+ caller: BackgroundSystemCaller,
30
+ ) -> None:
31
+ self._definition = definition
32
+ self._container = container
33
+ self._invocation_runtime = invocation_runtime
34
+ self._caller = caller
35
+ self._stop_event = asyncio.Event()
36
+ self._task: asyncio.Task[None] | None = None
37
+
38
+ @property
39
+ def binding(self) -> _TaskExecution:
40
+ return _TaskExecution(self.start, self._stop_event.set, self.wait, None, self.cancel)
41
+
42
+ async def start(self) -> None:
43
+ self._stop_event.clear()
44
+ self._task = asyncio.create_task(
45
+ self._worker_loop(self._definition),
46
+ name=f"python-ddd-framework-worker:{self._definition.name}",
47
+ )
48
+
49
+ async def wait(self) -> None:
50
+ if self._task is not None:
51
+ await self._task
52
+
53
+ def cancel(self) -> None:
54
+ if self._task is not None:
55
+ self._task.cancel()
56
+
57
+ async def _worker_loop(self, definition: BackgroundWorkerDefinition) -> None:
58
+ while not self._stop_event.is_set():
59
+ try:
60
+ await self._run_worker_iteration(definition)
61
+ except asyncio.CancelledError:
62
+ raise
63
+ except Exception as original_error:
64
+ if self._stop_event.is_set():
65
+ raise
66
+ # payload/secret 不进入日志;下一次 fixed-delay iteration 仍按 contract 执行。
67
+ _LOGGER.error(
68
+ "background worker iteration failed",
69
+ exc_info=original_error,
70
+ extra={
71
+ "background_worker": definition.name,
72
+ "background_exception_type": type(original_error).__name__,
73
+ },
74
+ )
75
+ if self._stop_event.is_set():
76
+ break
77
+ try:
78
+ await asyncio.wait_for(
79
+ self._stop_event.wait(),
80
+ timeout=definition.interval.total_seconds(),
81
+ )
82
+ except TimeoutError:
83
+ pass
84
+
85
+ async def _run_worker_iteration(self, definition: BackgroundWorkerDefinition) -> None:
86
+ container = self._container
87
+ context = BackgroundWorkerContext(
88
+ name=definition.name,
89
+ system_caller=self._caller,
90
+ _stop_event=self._stop_event,
91
+ )
92
+
93
+ # 原生 timeout 在当前 Worker Task 内执行、取消并等待清理;finally 失败直接传播。
94
+ # 不创建额外迭代 Task,调用许可、caller 和业务作用域保持同一 owner。
95
+ async with (
96
+ asyncio.timeout(definition.iteration_timeout.total_seconds()),
97
+ AsyncExitStack() as scope,
98
+ ):
99
+ # 关闭可能撤回尚未取得的许可;只处理 enter 的拒绝,不吞业务或 finally 异常。
100
+ try:
101
+ await scope.enter_async_context(self._invocation_runtime.outer_lease())
102
+ except ApplicationInvocationRejectedError:
103
+ return
104
+ token = self._invocation_runtime.bind_caller(CurrentUser.anonymous(), str(uuid4()))
105
+ try:
106
+ try:
107
+ async with container(scope=Scope.REQUEST) as request_container:
108
+ from ..invocation.interception import _EntryInterceptionCatalog
109
+
110
+ entries = await request_container.get(_EntryInterceptionCatalog)
111
+ await entries.invoke(
112
+ self._invocation_runtime,
113
+ request_container,
114
+ definition.worker_type,
115
+ "run_iteration",
116
+ (context,),
117
+ connection_name=_DEFAULT_CONNECTION_NAME,
118
+ )
119
+ except Exception as error:
120
+ if self._stop_event.is_set():
121
+ # 关闭中的异常可能来自被取消业务的 finally,不能按普通迭代失败吞掉。
122
+ raise
123
+ # lease/caller 尚有效;普通迭代失败在 owner 内记录,然后继续下一轮。
124
+ _LOGGER.error(
125
+ "background worker iteration failed",
126
+ exc_info=error,
127
+ extra={
128
+ "background_worker": definition.name,
129
+ "background_exception_type": type(error).__name__,
130
+ },
131
+ )
132
+ finally:
133
+ self._invocation_runtime.reset_caller(token)