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,340 @@
1
+ """Durable job versions 与 Cron schedules 的 immutable Application catalog。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from collections.abc import Iterator, Mapping
8
+ from dataclasses import replace
9
+ from datetime import UTC, datetime
10
+ from types import MappingProxyType
11
+ from typing import TypeVar, cast
12
+
13
+ from pydantic import BaseModel, ValidationError
14
+
15
+ from ..modularity import AppModule, ModuleDescriptor, ModuleKey, ModuleRegistry
16
+ from ..modularity.discovery import _DiscoveredType, _is_concrete_implementation
17
+ from .contracts import BackgroundJobDefinition, BackgroundJobHandler, BackgroundJobSchedule
18
+ from .declaration import _definition_from_type
19
+ from .errors import BackgroundJobDefinitionError, BackgroundJobPayloadError
20
+
21
+ _TPayload = TypeVar("_TPayload", bound=BaseModel)
22
+
23
+
24
+ class BackgroundJobCatalog:
25
+ __slots__ = (
26
+ "_current",
27
+ "_declarations",
28
+ "_definitions",
29
+ "_fingerprint",
30
+ "_owners",
31
+ "_schedules",
32
+ )
33
+
34
+ def __init__(
35
+ self,
36
+ definitions: tuple[BackgroundJobDefinition[BaseModel], ...],
37
+ schedules: tuple[BackgroundJobSchedule, ...],
38
+ *,
39
+ declarations: tuple[tuple[type[AppModule], str, tuple[object, ...]], ...],
40
+ owners: Mapping[tuple[str, int], ModuleKey],
41
+ ) -> None:
42
+ self._definitions = MappingProxyType(
43
+ {(item.name, item.version): item for item in definitions}
44
+ )
45
+ self._current = MappingProxyType({item.name: item for item in definitions if item.current})
46
+ self._schedules = MappingProxyType({item.name: item for item in schedules})
47
+ self._declarations = declarations
48
+ self._owners = MappingProxyType(dict(owners))
49
+ encoded = json.dumps(
50
+ {
51
+ "definitions": [
52
+ {
53
+ "name": item.name,
54
+ "version": item.version,
55
+ "current": item.current,
56
+ "owner": str(owners[(item.name, item.version)]),
57
+ "timeout_seconds": item.timeout.total_seconds(),
58
+ "max_attempts": item.retry_policy.max_attempts,
59
+ "initial_delay_seconds": item.retry_policy.initial_delay.total_seconds(),
60
+ "max_delay_seconds": item.retry_policy.max_delay.total_seconds(),
61
+ "backoff_multiplier": item.retry_policy.backoff_multiplier,
62
+ }
63
+ for item in definitions
64
+ ],
65
+ "schedules": [
66
+ {
67
+ "name": item.name,
68
+ "cron": item.cron,
69
+ "job_name": item.job_name,
70
+ "job_version": item.job_version,
71
+ "payload": item.payload.decode(),
72
+ }
73
+ for item in schedules
74
+ ],
75
+ },
76
+ sort_keys=True,
77
+ separators=(",", ":"),
78
+ ).encode()
79
+ self._fingerprint = hashlib.sha256(encoded).hexdigest()
80
+
81
+ @property
82
+ def definitions(self) -> tuple[BackgroundJobDefinition[BaseModel], ...]:
83
+ return tuple(self._definitions.values())
84
+
85
+ @property
86
+ def schedules(self) -> tuple[BackgroundJobSchedule, ...]:
87
+ return tuple(self._schedules.values())
88
+
89
+ @property
90
+ def fingerprint(self) -> str:
91
+ return self._fingerprint
92
+
93
+ def __iter__(self) -> Iterator[tuple[str, int]]:
94
+ return iter(self._definitions)
95
+
96
+ def get(self, name: str, version: int) -> BackgroundJobDefinition[BaseModel] | None:
97
+ return self._definitions.get((name, version))
98
+
99
+ def current(self, name: str) -> BackgroundJobDefinition[BaseModel] | None:
100
+ return self._current.get(name)
101
+
102
+ def get_schedule(self, name: str) -> BackgroundJobSchedule | None:
103
+ return self._schedules.get(name)
104
+
105
+ def owns(self, definition: BackgroundJobDefinition[_TPayload]) -> bool:
106
+ return self._definitions.get((definition.name, definition.version)) is definition
107
+
108
+ def for_type(
109
+ self, job_type: type[BackgroundJobHandler[_TPayload]]
110
+ ) -> BackgroundJobDefinition[_TPayload]:
111
+ for definition in self.definitions:
112
+ if definition.handler_type is job_type:
113
+ # 组合时已从该闭合类型验证 payload;此处只查询冻结事实,不重解释类。
114
+ return cast(BackgroundJobDefinition[_TPayload], definition)
115
+ raise BackgroundJobDefinitionError(
116
+ job=job_type.__qualname__, reason="Job type is not owned by this Application catalog"
117
+ )
118
+
119
+ def owner_for(self, definition: BackgroundJobDefinition[BaseModel]) -> ModuleKey:
120
+ return self._owners[definition.name, definition.version]
121
+
122
+ def _assert_unchanged(self) -> None:
123
+ for definition in self.definitions:
124
+ if isinstance(definition.handler_type, type) and (
125
+ _definition_from_type(definition.handler_type) != definition
126
+ ):
127
+ raise BackgroundJobDefinitionError(
128
+ job=definition.name, reason="Job class metadata changed after build"
129
+ )
130
+ for module_type, attribute, declared in self._declarations:
131
+ if module_type.__dict__.get(attribute, ()) is not declared:
132
+ raise BackgroundJobDefinitionError(
133
+ job=f"{module_type.__module__}.{module_type.__qualname__}",
134
+ reason=f"Module {attribute} metadata changed after build",
135
+ )
136
+
137
+
138
+ def _build_background_job_catalog(
139
+ registry: ModuleRegistry, discoveries: tuple[_DiscoveredType, ...]
140
+ ) -> BackgroundJobCatalog:
141
+ builder = _BackgroundJobCatalogBuilder()
142
+ for descriptor in registry.dependency_order:
143
+ builder.add_module(descriptor)
144
+ seen: set[tuple[ModuleKey, type[object]]] = set()
145
+ for discovery in discoveries:
146
+ candidate = discovery.candidate
147
+ identity = (discovery.owner, candidate)
148
+ if (
149
+ identity not in seen
150
+ and issubclass(candidate, BackgroundJobHandler)
151
+ and _is_concrete_implementation(candidate)
152
+ ):
153
+ seen.add(identity)
154
+ builder.add_job_type(candidate, discovery.owner)
155
+ return builder.build()
156
+
157
+
158
+ class _BackgroundJobCatalogBuilder:
159
+ """拥有 catalog build 期间的 mutable arbitration state,产物仍为 immutable。"""
160
+
161
+ __slots__ = (
162
+ "_declarations",
163
+ "_definitions",
164
+ "_name_owners",
165
+ "_owners",
166
+ "_raw_schedules",
167
+ "_schedule_owners",
168
+ )
169
+
170
+ def __init__(self) -> None:
171
+ self._definitions: list[BackgroundJobDefinition[BaseModel]] = []
172
+ self._raw_schedules: list[BackgroundJobSchedule] = []
173
+ self._owners: dict[tuple[str, int], ModuleKey] = {}
174
+ self._name_owners: dict[str, ModuleKey] = {}
175
+ self._schedule_owners: dict[str, ModuleKey] = {}
176
+ self._declarations: list[tuple[type[AppModule], str, tuple[object, ...]]] = []
177
+
178
+ def add_module(self, descriptor: ModuleDescriptor) -> None:
179
+ declared_definitions = descriptor.module_type.__dict__.get("background_job_definitions", ())
180
+ declared_schedules = descriptor.module_type.__dict__.get("background_job_schedules", ())
181
+ _validate_metadata_tuple(
182
+ declared_definitions,
183
+ owner=str(descriptor.key),
184
+ attribute="background_job_definitions",
185
+ )
186
+ _validate_metadata_tuple(
187
+ declared_schedules,
188
+ owner=str(descriptor.key),
189
+ attribute="background_job_schedules",
190
+ )
191
+ self._declarations.extend(
192
+ (
193
+ (descriptor.module_type, "background_job_definitions", declared_definitions),
194
+ (descriptor.module_type, "background_job_schedules", declared_schedules),
195
+ )
196
+ )
197
+ for definition in declared_definitions:
198
+ if isinstance(definition, BackgroundJobDefinition) and isinstance(
199
+ definition.handler_type, type
200
+ ):
201
+ raise BackgroundJobDefinitionError(
202
+ job=definition.name,
203
+ reason="class Jobs must be discovered through Module scan_packages",
204
+ )
205
+ self._add_definition(definition, descriptor.key)
206
+ for schedule in declared_schedules:
207
+ self._add_schedule(schedule, descriptor.key)
208
+
209
+ def add_job_type(
210
+ self, candidate: type[BackgroundJobHandler[BaseModel]], owner: ModuleKey
211
+ ) -> None:
212
+ self._add_definition(_definition_from_type(candidate), owner)
213
+
214
+ def build(self) -> BackgroundJobCatalog:
215
+ by_name: dict[str, list[BackgroundJobDefinition[BaseModel]]] = {}
216
+ by_identity = {(item.name, item.version): item for item in self._definitions}
217
+ for definition in self._definitions:
218
+ by_name.setdefault(definition.name, []).append(definition)
219
+ for name, versions in by_name.items():
220
+ if sum(item.current for item in versions) != 1:
221
+ raise BackgroundJobDefinitionError(
222
+ job=name,
223
+ reason="job versions must declare exactly one current definition",
224
+ )
225
+ schedules = tuple(_canonicalize_schedule(item, by_identity) for item in self._raw_schedules)
226
+ return BackgroundJobCatalog(
227
+ tuple(self._definitions),
228
+ schedules,
229
+ declarations=tuple(self._declarations),
230
+ owners=self._owners,
231
+ )
232
+
233
+ def _add_definition(self, candidate: object, owner: ModuleKey) -> None:
234
+ if not isinstance(candidate, BackgroundJobDefinition):
235
+ raise BackgroundJobDefinitionError(
236
+ job=str(owner),
237
+ reason="Module background_job_definitions contains an invalid item",
238
+ )
239
+ definition = candidate
240
+ identity = (definition.name, definition.version)
241
+ if identity in self._owners:
242
+ raise BackgroundJobDefinitionError(
243
+ job=definition.name,
244
+ reason=f"version {definition.version} is declared more than once",
245
+ )
246
+ previous_owner = self._name_owners.get(definition.name)
247
+ if previous_owner is not None and previous_owner != owner:
248
+ raise BackgroundJobDefinitionError(
249
+ job=definition.name,
250
+ reason=f"all versions must be owned by {previous_owner}",
251
+ )
252
+ self._owners[identity] = owner
253
+ self._name_owners[definition.name] = owner
254
+ self._definitions.append(definition)
255
+
256
+ def _add_schedule(self, candidate: object, owner: ModuleKey) -> None:
257
+ if not isinstance(candidate, BackgroundJobSchedule):
258
+ raise BackgroundJobDefinitionError(
259
+ job=str(owner),
260
+ reason="Module background_job_schedules contains an invalid item",
261
+ )
262
+ schedule = candidate
263
+ if schedule.name in self._schedule_owners:
264
+ raise BackgroundJobDefinitionError(
265
+ job=schedule.name,
266
+ reason=f"schedule is already owned by {self._schedule_owners[schedule.name]}",
267
+ )
268
+ self._schedule_owners[schedule.name] = owner
269
+ self._raw_schedules.append(schedule)
270
+
271
+
272
+ def _validate_metadata_tuple(value: object, *, owner: str, attribute: str) -> None:
273
+ if not isinstance(value, tuple):
274
+ raise BackgroundJobDefinitionError(
275
+ job=owner,
276
+ reason=f"Module {attribute} must be an immutable tuple",
277
+ )
278
+
279
+
280
+ def _canonicalize_schedule(
281
+ schedule: BackgroundJobSchedule,
282
+ definitions: Mapping[tuple[str, int], BackgroundJobDefinition[BaseModel]],
283
+ ) -> BackgroundJobSchedule:
284
+ definition = definitions.get((schedule.job_name, schedule.job_version))
285
+ if definition is None:
286
+ raise BackgroundJobDefinitionError(
287
+ job=schedule.name,
288
+ reason="schedule target does not exist in the Application catalog",
289
+ )
290
+ cron = _normalize_cron(schedule.name, schedule.cron)
291
+ try:
292
+ payload = definition.payload_type.model_validate_json(schedule.payload)
293
+ except ValidationError:
294
+ raise BackgroundJobPayloadError(
295
+ job=definition.name,
296
+ version=definition.version,
297
+ reason="scheduled payload failed schema validation",
298
+ ) from None
299
+ canonical = json.dumps(
300
+ payload.model_dump(mode="json"),
301
+ sort_keys=True,
302
+ separators=(",", ":"),
303
+ ensure_ascii=False,
304
+ ).encode()
305
+ return replace(schedule, cron=cron, payload=canonical)
306
+
307
+
308
+ def _normalize_cron(schedule_name: str, expression: str) -> str:
309
+ normalized = " ".join(expression.split())
310
+ fields = normalized.split(" ")
311
+ if len(fields) not in (5, 6) or any(
312
+ not field or any(character not in "0123456789*/,-" for character in field)
313
+ for field in fields
314
+ ):
315
+ raise BackgroundJobDefinitionError(
316
+ job=schedule_name,
317
+ reason="cron must use numeric five-field or trailing-second six-field grammar",
318
+ )
319
+ try:
320
+ from croniter import croniter # type: ignore[import-untyped]
321
+ except ImportError:
322
+ raise BackgroundJobDefinitionError(
323
+ job=schedule_name,
324
+ reason="Cron schedules require the background-jobs extra",
325
+ ) from None
326
+ try:
327
+ valid = croniter.is_valid(normalized, strict=True)
328
+ if valid:
329
+ croniter(
330
+ normalized,
331
+ start_time=datetime.now(UTC),
332
+ ).get_next(datetime)
333
+ except (TypeError, ValueError):
334
+ valid = False
335
+ if not valid:
336
+ raise BackgroundJobDefinitionError(
337
+ job=schedule_name,
338
+ reason="cron expression is outside the supported UTC grammar",
339
+ )
340
+ return normalized
@@ -0,0 +1,208 @@
1
+ """Provider-neutral durable job identity、payload 与 enqueue contracts。"""
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
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timedelta
11
+ from typing import ClassVar, Generic, Protocol, TypeVar, cast
12
+
13
+ from pydantic import BaseModel
14
+
15
+ from ..invocation import BackgroundSystemCaller
16
+ from .errors import BackgroundJobDefinitionError
17
+
18
+ _TPayload = TypeVar("_TPayload", bound=BaseModel)
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class BackgroundJobContext:
23
+ job_id: str
24
+ name: str
25
+ version: int
26
+ attempt: int
27
+ scheduled_at: datetime
28
+ system_caller: BackgroundSystemCaller
29
+ _cancellation_event: asyncio.Event = field(repr=False, compare=False)
30
+
31
+ @property
32
+ def cancellation_requested(self) -> bool:
33
+ return self._cancellation_event.is_set()
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class BackgroundJobRetryPolicy:
38
+ max_attempts: int = 5
39
+ initial_delay: timedelta = timedelta(seconds=1)
40
+ max_delay: timedelta = timedelta(minutes=5)
41
+ backoff_multiplier: float = 2.0
42
+
43
+ def __post_init__(self) -> None:
44
+ if (
45
+ not isinstance(self.max_attempts, int)
46
+ or isinstance(self.max_attempts, bool)
47
+ or self.max_attempts < 1
48
+ ):
49
+ raise BackgroundJobDefinitionError(
50
+ job="<retry-policy>",
51
+ reason="max_attempts must be a positive integer",
52
+ )
53
+ if not isinstance(self.initial_delay, timedelta) or self.initial_delay.total_seconds() <= 0:
54
+ raise BackgroundJobDefinitionError(
55
+ job="<retry-policy>",
56
+ reason="initial_delay must be a positive timedelta",
57
+ )
58
+ if not isinstance(self.max_delay, timedelta) or self.max_delay < self.initial_delay:
59
+ raise BackgroundJobDefinitionError(
60
+ job="<retry-policy>",
61
+ reason="max_delay must be a timedelta no smaller than initial_delay",
62
+ )
63
+ if (
64
+ not isinstance(self.backoff_multiplier, (int, float))
65
+ or isinstance(self.backoff_multiplier, bool)
66
+ or self.backoff_multiplier < 1
67
+ ):
68
+ raise BackgroundJobDefinitionError(
69
+ job="<retry-policy>",
70
+ reason="backoff_multiplier must be at least 1",
71
+ )
72
+
73
+ def delay_for_attempt(self, attempt: int) -> timedelta:
74
+ if not isinstance(attempt, int) or isinstance(attempt, bool) or attempt < 0:
75
+ raise ValueError("attempt must be a non-negative integer")
76
+ seconds = self.initial_delay.total_seconds() * (self.backoff_multiplier**attempt)
77
+ return timedelta(seconds=min(seconds, self.max_delay.total_seconds()))
78
+
79
+
80
+ class BackgroundJobHandler(ABC, Generic[_TPayload]):
81
+ """Module 扫描的任务声明;queue loop/retry 仍由 provider 独占。"""
82
+
83
+ name: ClassVar[str]
84
+ version: ClassVar[int]
85
+ timeout: ClassVar[timedelta]
86
+ current: ClassVar[bool] = False
87
+ retry_policy: ClassVar[BackgroundJobRetryPolicy] = BackgroundJobRetryPolicy()
88
+
89
+ @abstractmethod
90
+ async def execute(self, payload: _TPayload, context: BackgroundJobContext) -> None:
91
+ raise NotImplementedError
92
+
93
+
94
+ @dataclass(frozen=True, slots=True)
95
+ class BackgroundJobDefinition(Generic[_TPayload]):
96
+ name: str
97
+ version: int
98
+ payload_type: type[_TPayload]
99
+ handler_type: type[BackgroundJobHandler[_TPayload]] | Callable[..., Awaitable[None]]
100
+ timeout: timedelta
101
+ current: bool = False
102
+ retry_policy: BackgroundJobRetryPolicy = BackgroundJobHandler.retry_policy
103
+
104
+ def __post_init__(self) -> None:
105
+ _validate_name(self.name)
106
+ if not isinstance(self.version, int) or isinstance(self.version, bool) or self.version < 1:
107
+ raise BackgroundJobDefinitionError(
108
+ job=self.name,
109
+ reason="version must be a positive integer",
110
+ )
111
+ if not isinstance(self.payload_type, type) or not issubclass(self.payload_type, BaseModel):
112
+ raise BackgroundJobDefinitionError(
113
+ job=self.name,
114
+ reason="payload_type must be a Pydantic BaseModel class",
115
+ )
116
+ is_class = isinstance(self.handler_type, type) and issubclass(
117
+ self.handler_type, BackgroundJobHandler
118
+ )
119
+ if not is_class and not inspect.iscoroutinefunction(self.handler_type):
120
+ raise BackgroundJobDefinitionError(
121
+ job=self.name,
122
+ reason="handler_type must be a BackgroundJobHandler class or async function",
123
+ )
124
+ if isinstance(self.handler_type, type) and not inspect.iscoroutinefunction(
125
+ cast(type[BackgroundJobHandler[_TPayload]], self.handler_type).execute
126
+ ):
127
+ raise BackgroundJobDefinitionError(
128
+ job=self.name,
129
+ reason="execute must be async",
130
+ )
131
+ if not isinstance(self.timeout, timedelta) or self.timeout.total_seconds() <= 0:
132
+ raise BackgroundJobDefinitionError(
133
+ job=self.name,
134
+ reason="timeout must be a positive timedelta",
135
+ )
136
+ if not isinstance(self.current, bool):
137
+ raise BackgroundJobDefinitionError(job=self.name, reason="current must be bool")
138
+ if not isinstance(self.retry_policy, BackgroundJobRetryPolicy):
139
+ raise BackgroundJobDefinitionError(
140
+ job=self.name,
141
+ reason="retry_policy must be BackgroundJobRetryPolicy",
142
+ )
143
+
144
+
145
+ @dataclass(frozen=True, slots=True)
146
+ class BackgroundJobSchedule:
147
+ name: str
148
+ cron: str
149
+ job_name: str
150
+ job_version: int
151
+ payload: bytes
152
+
153
+ def __post_init__(self) -> None:
154
+ _validate_name(self.name)
155
+ _validate_name(self.job_name)
156
+ if (
157
+ not isinstance(self.job_version, int)
158
+ or isinstance(self.job_version, bool)
159
+ or self.job_version < 1
160
+ ):
161
+ raise BackgroundJobDefinitionError(
162
+ job=self.name,
163
+ reason="job_version must be a positive integer",
164
+ )
165
+ if not isinstance(self.cron, str) or not self.cron.strip():
166
+ raise BackgroundJobDefinitionError(
167
+ job=self.name,
168
+ reason="cron must be a non-empty string",
169
+ )
170
+ if not isinstance(self.payload, bytes):
171
+ raise BackgroundJobDefinitionError(
172
+ job=self.name,
173
+ reason="payload must be immutable UTF-8 JSON bytes",
174
+ )
175
+ try:
176
+ self.payload.decode("utf-8")
177
+ except UnicodeDecodeError:
178
+ raise BackgroundJobDefinitionError(
179
+ job=self.name,
180
+ reason="payload must be UTF-8 JSON bytes",
181
+ ) from None
182
+
183
+
184
+ class BackgroundJobEnqueuer(Protocol):
185
+ async def enqueue(
186
+ self,
187
+ definition: type[BackgroundJobHandler[_TPayload]] | BackgroundJobDefinition[_TPayload],
188
+ payload: _TPayload,
189
+ *,
190
+ priority: int = 0,
191
+ delay: timedelta | None = None,
192
+ ) -> str: ...
193
+
194
+
195
+ def _validate_name(name: str) -> None:
196
+ if (
197
+ not isinstance(name, str)
198
+ or not name
199
+ or name != name.strip()
200
+ or name.startswith(".")
201
+ or name.endswith(".")
202
+ or ".." in name
203
+ or any(not (character.isalnum() or character in "._-") for character in name)
204
+ ):
205
+ raise BackgroundJobDefinitionError(
206
+ job=name if isinstance(name, str) else repr(name),
207
+ reason="name must be a trimmed stable identifier",
208
+ )
@@ -0,0 +1,67 @@
1
+ """从闭合 Job 类型解释不可变声明;不修改共享类或创建另一份任务目录。"""
2
+
3
+ from collections.abc import Mapping
4
+ from types import get_original_bases
5
+ from typing import TypeVar, cast, get_args, get_origin
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from python_ddd_framework.modularity.discovery import _is_concrete_implementation
10
+
11
+ from .contracts import BackgroundJobDefinition, BackgroundJobHandler
12
+ from .errors import BackgroundJobDefinitionError
13
+
14
+ _TPayload = TypeVar("_TPayload", bound=BaseModel)
15
+
16
+
17
+ def _definition_from_type(
18
+ candidate: type[BackgroundJobHandler[_TPayload]],
19
+ ) -> BackgroundJobDefinition[_TPayload]:
20
+ payloads = _payload_types(candidate, {})
21
+ if len(payloads) != 1:
22
+ raise BackgroundJobDefinitionError(
23
+ job=candidate.__qualname__,
24
+ reason="Job requires one closed BackgroundJobHandler payload model",
25
+ )
26
+ payload = next(iter(payloads))
27
+ if (
28
+ not isinstance(payload, type)
29
+ or not issubclass(payload, BaseModel)
30
+ or not _is_concrete_implementation(payload)
31
+ ):
32
+ raise BackgroundJobDefinitionError(
33
+ job=candidate.__qualname__, reason="Job payload must be a closed Pydantic model"
34
+ )
35
+ if hasattr(candidate, "enabled"):
36
+ raise BackgroundJobDefinitionError(
37
+ job=candidate.__qualname__, reason="Job declarations do not support enabled"
38
+ )
39
+ try:
40
+ return BackgroundJobDefinition(
41
+ name=candidate.name,
42
+ version=candidate.version,
43
+ payload_type=cast(type[_TPayload], payload),
44
+ handler_type=candidate,
45
+ timeout=candidate.timeout,
46
+ current=candidate.current,
47
+ retry_policy=candidate.retry_policy,
48
+ )
49
+ except AttributeError as error:
50
+ raise BackgroundJobDefinitionError(
51
+ job=candidate.__qualname__, reason="Job must declare name, version and timeout"
52
+ ) from error
53
+
54
+
55
+ def _payload_types(candidate: object, substitutions: Mapping[object, object]) -> set[object]:
56
+ origin = get_origin(candidate) or candidate
57
+ arguments = tuple(substitutions.get(arg, arg) for arg in get_args(candidate))
58
+ if origin is BackgroundJobHandler:
59
+ return set(arguments)
60
+ if not isinstance(origin, type):
61
+ return set()
62
+ parameters = cast(tuple[TypeVar, ...], getattr(origin, "__parameters__", ()))
63
+ resolved: dict[object, object] = dict(substitutions)
64
+ resolved.update(zip(parameters, arguments, strict=False))
65
+ return {
66
+ payload for base in get_original_bases(origin) for payload in _payload_types(base, resolved)
67
+ }
@@ -0,0 +1,28 @@
1
+ """Durable background job definition、payload 与 provider-neutral failures。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..errors.base import FrameworkError
6
+
7
+
8
+ class BackgroundJobDefinitionError(FrameworkError):
9
+ def __init__(self, *, job: str, reason: str) -> None:
10
+ self.job = job
11
+ self.reason = reason
12
+ super().__init__(f"Invalid background job {job!r}: {reason}")
13
+
14
+
15
+ class BackgroundJobPayloadError(FrameworkError):
16
+ def __init__(self, *, job: str, version: int, reason: str) -> None:
17
+ self.job = job
18
+ self.version = version
19
+ self.reason = reason
20
+ super().__init__(f"Invalid payload for background job {job!r} v{version}: {reason}")
21
+
22
+
23
+ class BackgroundJobExecutionError(FrameworkError):
24
+ def __init__(self, *, job: str, version: int, error_type: str) -> None:
25
+ self.job = job
26
+ self.version = version
27
+ self.error_type = error_type
28
+ super().__init__(f"Background job {job!r} v{version} failed with {error_type}")