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,174 @@
1
+ """单个 Application 拥有的 append-only diagnostic journal。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, replace
6
+ from time import perf_counter_ns
7
+ from uuid import UUID, uuid4
8
+
9
+ from ..errors import InvalidDiagnosticCodeError
10
+ from .model import (
11
+ DiagnosticMessage,
12
+ DiagnosticSnapshot,
13
+ EventStatus,
14
+ FailureRecord,
15
+ LifecyclePhase,
16
+ LifecycleStage,
17
+ PhaseEvent,
18
+ SourceLocation,
19
+ )
20
+
21
+ _DIAGNOSTIC_CODE_PUNCTUATION = frozenset("._-")
22
+ _MAX_DIAGNOSTIC_CODE_LENGTH = 128
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class _ActiveEvent:
27
+ phase: LifecyclePhase
28
+ stage: LifecycleStage
29
+ subject: str
30
+ source: SourceLocation | None
31
+ started_ns: int
32
+
33
+
34
+ class _DiagnosticJournal:
35
+ """Application-owned journal;外部只能取得不可变的时点快照。"""
36
+
37
+ __slots__ = ("_events", "_failure_order", "_failures", "_messages", "_sequence")
38
+
39
+ def __init__(self) -> None:
40
+ self._sequence = 0
41
+ self._events: list[PhaseEvent] = []
42
+ self._messages: list[DiagnosticMessage] = []
43
+ self._failure_order: list[UUID] = []
44
+ self._failures: dict[UUID, FailureRecord] = {}
45
+
46
+ def _next_sequence(self) -> int:
47
+ self._sequence += 1
48
+ return self._sequence
49
+
50
+ def start_event(
51
+ self,
52
+ *,
53
+ phase: LifecyclePhase,
54
+ stage: LifecycleStage,
55
+ subject: str,
56
+ source: SourceLocation | None,
57
+ ) -> _ActiveEvent:
58
+ started_ns = perf_counter_ns()
59
+ active_event = _ActiveEvent(phase, stage, subject, source, started_ns)
60
+ self._events.append(
61
+ PhaseEvent(
62
+ sequence=self._next_sequence(),
63
+ phase=phase,
64
+ stage=stage,
65
+ subject=subject,
66
+ status=EventStatus.STARTED,
67
+ started_monotonic_ns=started_ns,
68
+ duration_ns=None,
69
+ source=source,
70
+ failure_id=None,
71
+ )
72
+ )
73
+ return active_event
74
+
75
+ def finish_event(
76
+ self,
77
+ active_event: _ActiveEvent,
78
+ *,
79
+ status: EventStatus,
80
+ failure_id: UUID | None,
81
+ ) -> int:
82
+ # start 返回的 value object 是一次事件的唯一事实,finish 不重复传递上下文。
83
+ duration_ns = perf_counter_ns() - active_event.started_ns
84
+ self._events.append(
85
+ PhaseEvent(
86
+ sequence=self._next_sequence(),
87
+ phase=active_event.phase,
88
+ stage=active_event.stage,
89
+ subject=active_event.subject,
90
+ status=status,
91
+ started_monotonic_ns=active_event.started_ns,
92
+ duration_ns=duration_ns,
93
+ source=active_event.source,
94
+ failure_id=failure_id,
95
+ )
96
+ )
97
+ return duration_ns
98
+
99
+ def begin_failure(
100
+ self,
101
+ *,
102
+ error_type: str,
103
+ phase: LifecyclePhase,
104
+ stage: LifecycleStage,
105
+ subject: str | None,
106
+ message: str,
107
+ source: SourceLocation | None,
108
+ dependency_path: tuple[str, ...],
109
+ ) -> UUID:
110
+ failure_id = uuid4()
111
+ # 先发布不可变记录,rollback 后替换 cleanup 关联,snapshot 不出现悬空引用。
112
+ failure = FailureRecord(
113
+ id=failure_id,
114
+ error_type=error_type,
115
+ phase=phase,
116
+ stage=stage,
117
+ subject=subject,
118
+ message=message,
119
+ source=source,
120
+ dependency_path=dependency_path,
121
+ cleanup_failure_ids=(),
122
+ )
123
+ self._failure_order.append(failure_id)
124
+ self._failures[failure_id] = failure
125
+ return failure_id
126
+
127
+ def complete_failure(
128
+ self,
129
+ failure_id: UUID,
130
+ *,
131
+ cleanup_failure_ids: tuple[UUID, ...] = (),
132
+ ) -> None:
133
+ self._failures[failure_id] = replace(
134
+ self._failures[failure_id], cleanup_failure_ids=cleanup_failure_ids
135
+ )
136
+
137
+ def emit(
138
+ self,
139
+ *,
140
+ phase: LifecyclePhase,
141
+ stage: LifecycleStage,
142
+ subject: str,
143
+ code: str,
144
+ source: SourceLocation | None,
145
+ ) -> None:
146
+ if not _is_diagnostic_code(code):
147
+ raise InvalidDiagnosticCodeError(code, max_length=_MAX_DIAGNOSTIC_CODE_LENGTH)
148
+ self._messages.append(
149
+ DiagnosticMessage(
150
+ sequence=self._next_sequence(),
151
+ phase=phase,
152
+ stage=stage,
153
+ subject=subject,
154
+ code=code,
155
+ source=source,
156
+ )
157
+ )
158
+
159
+ def snapshot(self, application_id: UUID) -> DiagnosticSnapshot:
160
+ failures = tuple(self._failures[failure_id] for failure_id in self._failure_order)
161
+ return DiagnosticSnapshot(
162
+ application_id=application_id,
163
+ events=tuple(self._events),
164
+ failures=failures,
165
+ messages=tuple(self._messages),
166
+ )
167
+
168
+
169
+ def _is_diagnostic_code(code: str) -> bool:
170
+ if not code or len(code) > _MAX_DIAGNOSTIC_CODE_LENGTH or not code[0].isalpha():
171
+ return False
172
+ return all(
173
+ character.isalnum() or character in _DIAGNOSTIC_CODE_PUNCTUATION for character in code
174
+ )
@@ -0,0 +1,83 @@
1
+ """不可变 diagnostic contract;不拥有 journal mutation。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from uuid import UUID
8
+
9
+
10
+ class LifecyclePhase(str, Enum):
11
+ COMPOSITION = "composition"
12
+ RUNTIME = "runtime"
13
+ CLEANUP = "cleanup"
14
+
15
+
16
+ class LifecycleStage(str, Enum):
17
+ PRE_CONFIGURE = "pre_configure"
18
+ CONFIGURE = "configure"
19
+ POST_CONFIGURE = "post_configure"
20
+ PRE_INITIALIZE = "pre_initialize"
21
+ INITIALIZE = "initialize"
22
+ POST_INITIALIZE = "post_initialize"
23
+ SHUTDOWN = "shutdown"
24
+
25
+
26
+ class EventStatus(str, Enum):
27
+ STARTED = "started"
28
+ SUCCEEDED = "succeeded"
29
+ FAILED = "failed"
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class SourceLocation:
34
+ owner: str
35
+ file: str | None
36
+ line: int | None
37
+ symbol: str | None
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class PhaseEvent:
42
+ sequence: int
43
+ phase: LifecyclePhase
44
+ stage: LifecycleStage
45
+ subject: str
46
+ status: EventStatus
47
+ started_monotonic_ns: int
48
+ duration_ns: int | None
49
+ source: SourceLocation | None
50
+ failure_id: UUID | None
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class FailureRecord:
55
+ id: UUID
56
+ error_type: str
57
+ phase: LifecyclePhase
58
+ stage: LifecycleStage
59
+ subject: str | None
60
+ message: str
61
+ source: SourceLocation | None
62
+ dependency_path: tuple[str, ...]
63
+ cleanup_failure_ids: tuple[UUID, ...]
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class DiagnosticMessage:
68
+ sequence: int
69
+ phase: LifecyclePhase
70
+ stage: LifecycleStage
71
+ subject: str
72
+ code: str
73
+ source: SourceLocation | None
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class DiagnosticSnapshot:
78
+ """一次读取产生的不可变诊断视图,不随后续 lifecycle 变化。"""
79
+
80
+ application_id: UUID
81
+ events: tuple[PhaseEvent, ...]
82
+ failures: tuple[FailureRecord, ...]
83
+ messages: tuple[DiagnosticMessage, ...]
@@ -0,0 +1,25 @@
1
+ """贡献诊断的调用位置;depth 由公开贡献入口指定。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+
7
+ from .model import SourceLocation
8
+
9
+
10
+ def caller_source(owner: str, *, depth: int) -> SourceLocation:
11
+ frame = inspect.currentframe()
12
+ try:
13
+ for _ in range(depth):
14
+ frame = None if frame is None else frame.f_back
15
+ if frame is None:
16
+ return SourceLocation(owner=owner, file=None, line=None, symbol=None)
17
+ return SourceLocation(
18
+ owner=owner,
19
+ file=frame.f_code.co_filename,
20
+ line=frame.f_lineno,
21
+ symbol=frame.f_code.co_name,
22
+ )
23
+ finally:
24
+ # 不让 frame 引用延长 Module/composition 对象的生命周期。
25
+ del frame
@@ -0,0 +1,6 @@
1
+ """Provider-neutral 业务互斥与租约失败契约。"""
2
+
3
+ from .contracts import DistributedLock, DistributedLockError
4
+ from .options import DistributedLockOptions
5
+
6
+ __all__ = ("DistributedLock", "DistributedLockError", "DistributedLockOptions")
@@ -0,0 +1,20 @@
1
+ """锁的业务使用与错误边界;provider 不进入稳定契约。"""
2
+
3
+ from contextlib import AbstractAsyncContextManager
4
+ from typing import Protocol
5
+
6
+ from ..errors.base import FrameworkError
7
+
8
+
9
+ class DistributedLock(Protocol):
10
+ def acquire(self, key: str, /) -> AbstractAsyncContextManager[bool]:
11
+ """进入后返回是否取得锁;持有者必须配合取消并在退出上下文前完成清理。"""
12
+ ...
13
+
14
+
15
+ class DistributedLockError(FrameworkError):
16
+ def __init__(self, *, key: str, operation: str, error_type: str) -> None:
17
+ self.key = key
18
+ self.operation = operation
19
+ self.error_type = error_type
20
+ super().__init__(f"Distributed lock {key!r} {operation} failed with {error_type}")
@@ -0,0 +1,19 @@
1
+ """获取等待和租约时序的唯一默认值来源。"""
2
+
3
+ from datetime import timedelta
4
+
5
+ from pydantic import Field, model_validator
6
+
7
+ from ..options import BaseOptions
8
+
9
+
10
+ class DistributedLockOptions(BaseOptions):
11
+ wait_timeout: timedelta = Field(default=timedelta(seconds=5), ge=timedelta(0))
12
+ lease_timeout: timedelta = Field(default=timedelta(seconds=30), ge=timedelta(milliseconds=1))
13
+ renewal_interval: timedelta = Field(default=timedelta(seconds=10), ge=timedelta(milliseconds=1))
14
+
15
+ @model_validator(mode="after")
16
+ def _validate_renewal(self) -> "DistributedLockOptions":
17
+ if self.renewal_interval >= self.lease_timeout:
18
+ raise ValueError("renewal_interval must be shorter than lease_timeout")
19
+ return self
@@ -0,0 +1,13 @@
1
+ """Provider-neutral DDD foundations owned by Python DDD Framework."""
2
+
3
+ from .aggregates import AggregateRoot
4
+ from .entities import Entity
5
+ from .errors import OptimisticConcurrencyError
6
+ from .value_objects import ValueObject
7
+
8
+ __all__ = (
9
+ "AggregateRoot",
10
+ "Entity",
11
+ "OptimisticConcurrencyError",
12
+ "ValueObject",
13
+ )
@@ -0,0 +1,64 @@
1
+ """Aggregate identity, version and pending local-event ownership."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import is_dataclass
6
+ from typing import Generic, TypeVar
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from .entities import Entity
11
+ from .errors import OptimisticConcurrencyError
12
+
13
+ _TId = TypeVar("_TId")
14
+
15
+
16
+ class AggregateRoot(Entity[_TId], Generic[_TId]):
17
+ """一个 invocation 内聚合版本和待提交事件的唯一 mutable owner。"""
18
+
19
+ __slots__ = ("_local_events", "_version", "_version_staged")
20
+
21
+ def __init__(self, entity_id: _TId, *, version: int = 0) -> None:
22
+ super().__init__(entity_id)
23
+ if not isinstance(version, int) or isinstance(version, bool) or version < 0:
24
+ raise ValueError("aggregate version must be a non-negative integer")
25
+ self._version = version
26
+ self._version_staged = False
27
+ self._local_events: list[object] = []
28
+
29
+ @property
30
+ def version(self) -> int:
31
+ return self._version
32
+
33
+ def raise_local_event(self, event: object) -> None:
34
+ if not _is_immutable_event(event):
35
+ raise TypeError("aggregate local events must be immutable dataclass or Pydantic values")
36
+ self._local_events.append(event)
37
+
38
+ def release_local_events(self) -> tuple[object, ...]:
39
+ events = tuple(self._local_events)
40
+ self._local_events.clear()
41
+ return events
42
+
43
+ def stage_next_version(self, expected_version: int) -> int:
44
+ """Repository SPI:一次聚合实例只允许暂存一个下一版本。"""
45
+
46
+ if (
47
+ not isinstance(expected_version, int)
48
+ or isinstance(expected_version, bool)
49
+ or expected_version < 0
50
+ ):
51
+ raise ValueError("expected_version must be a non-negative integer")
52
+ if self._version_staged or self._version != expected_version:
53
+ raise OptimisticConcurrencyError
54
+ self._version += 1
55
+ self._version_staged = True
56
+ return self._version
57
+
58
+
59
+ def _is_immutable_event(event: object) -> bool:
60
+ event_type = type(event)
61
+ dataclass_parameters = getattr(event_type, "__dataclass_params__", None)
62
+ if is_dataclass(event) and bool(getattr(dataclass_parameters, "frozen", False)):
63
+ return True
64
+ return isinstance(event, BaseModel) and event.model_config.get("frozen") is True
@@ -0,0 +1,33 @@
1
+ """Entity identity semantics independent from persistence providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Generic, TypeVar
6
+
7
+ _TId = TypeVar("_TId")
8
+
9
+
10
+ class Entity(Generic[_TId]):
11
+ """Identity owner whose identifier cannot change after construction."""
12
+
13
+ __slots__ = ("_id",)
14
+
15
+ def __init__(self, entity_id: _TId) -> None:
16
+ if entity_id is None:
17
+ raise ValueError("entity id cannot be None")
18
+ self._id = entity_id
19
+
20
+ @property
21
+ def id(self) -> _TId:
22
+ return self._id
23
+
24
+ def __eq__(self, other: object) -> bool:
25
+ if self is other:
26
+ return True
27
+ if type(self) is not type(other):
28
+ return False
29
+ return self.id == other.id
30
+
31
+ def __hash__(self) -> int:
32
+ # 类型是 identity 的一部分,避免不同聚合偶然共用相同标识时发生碰撞语义。
33
+ return hash((type(self), self.id))
@@ -0,0 +1,12 @@
1
+ """DDD boundary failures safe for transport mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..errors import ResourceConflictError
6
+
7
+
8
+ class OptimisticConcurrencyError(ResourceConflictError):
9
+ """调用方版本与持久化 authority 不一致;不携带真实行版本或 SQL。"""
10
+
11
+ def __init__(self) -> None:
12
+ super().__init__("Aggregate version conflict")
@@ -0,0 +1,31 @@
1
+ """Pydantic-backed immutable domain values."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+
10
+ class ValueObject(BaseModel):
11
+ """按具体类型与字段值比较的领域值;它不是 transport DTO。"""
12
+
13
+ model_config = ConfigDict(frozen=True, extra="forbid")
14
+
15
+ @classmethod
16
+ def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
17
+ super().__pydantic_init_subclass__(**kwargs)
18
+ if cls.model_config.get("frozen") is not True or cls.model_config.get("extra") != "forbid":
19
+ raise TypeError("ValueObject subclasses must remain frozen and forbid extra fields")
20
+
21
+ def __eq__(self, other: object) -> bool:
22
+ if type(self) is not type(other):
23
+ return False
24
+ return self._field_values() == other._field_values()
25
+
26
+ def __hash__(self) -> int:
27
+ # 字段中含 list/dict 等 mutable value 时由 Python 原生 hash fail closed。
28
+ return hash((type(self), self._field_values()))
29
+
30
+ def _field_values(self) -> tuple[object, ...]:
31
+ return tuple(getattr(self, name) for name in type(self).model_fields)
@@ -0,0 +1,75 @@
1
+ """按 capability 组织的 Python DDD Framework 结构化错误公开边界。"""
2
+
3
+ from .base import FrameworkError, ResourceConflictError, ResourceNotFoundError
4
+ from .business import BusinessError, BusinessErrorDefinition, BusinessErrorField
5
+ from .configuration import (
6
+ ConfigurationSourceError,
7
+ DuplicateOptionDefinitionError,
8
+ InvalidOptionDefinitionError,
9
+ OptionsContributionClosedError,
10
+ OptionsValidationError,
11
+ UnknownOptionsError,
12
+ )
13
+ from .diagnostics import InvalidDiagnosticCodeError
14
+ from .lifecycle import (
15
+ ApplicationRuntimeError,
16
+ ApplicationStartError,
17
+ CompositionResourceError,
18
+ InvalidApplicationStateError,
19
+ LifecycleInvocationError,
20
+ LifecycleReturnValueError,
21
+ )
22
+ from .modularity import (
23
+ DuplicateModuleIdentityError,
24
+ GraphInvariantError,
25
+ InvalidModuleReferenceError,
26
+ ModuleCycleError,
27
+ ModuleInstantiationError,
28
+ ModuleResolutionError,
29
+ UnknownModuleError,
30
+ )
31
+ from .services import (
32
+ CatalogNativeGraphMismatchError,
33
+ ContainerCleanupError,
34
+ ContainerValidationError,
35
+ ConventionScanError,
36
+ InvalidConventionDefinitionError,
37
+ RegistrationConflictError,
38
+ ServiceContributorClosedError,
39
+ )
40
+
41
+ __all__ = (
42
+ "ApplicationRuntimeError",
43
+ "ApplicationStartError",
44
+ "BusinessError",
45
+ "BusinessErrorDefinition",
46
+ "BusinessErrorField",
47
+ "CatalogNativeGraphMismatchError",
48
+ "CompositionResourceError",
49
+ "ConfigurationSourceError",
50
+ "ContainerCleanupError",
51
+ "ContainerValidationError",
52
+ "ConventionScanError",
53
+ "DuplicateModuleIdentityError",
54
+ "DuplicateOptionDefinitionError",
55
+ "FrameworkError",
56
+ "GraphInvariantError",
57
+ "InvalidApplicationStateError",
58
+ "InvalidConventionDefinitionError",
59
+ "InvalidDiagnosticCodeError",
60
+ "InvalidModuleReferenceError",
61
+ "InvalidOptionDefinitionError",
62
+ "LifecycleInvocationError",
63
+ "LifecycleReturnValueError",
64
+ "ModuleCycleError",
65
+ "ModuleInstantiationError",
66
+ "ModuleResolutionError",
67
+ "OptionsContributionClosedError",
68
+ "OptionsValidationError",
69
+ "RegistrationConflictError",
70
+ "ResourceConflictError",
71
+ "ResourceNotFoundError",
72
+ "ServiceContributorClosedError",
73
+ "UnknownModuleError",
74
+ "UnknownOptionsError",
75
+ )
@@ -0,0 +1,13 @@
1
+ """所有 capability error 共享的最小命名根。"""
2
+
3
+
4
+ class FrameworkError(Exception):
5
+ """所有 Python DDD Framework 结构化错误的命名基类。"""
6
+
7
+
8
+ class ResourceConflictError(FrameworkError):
9
+ """可信业务边界可映射为 HTTP 409 的静态冲突。"""
10
+
11
+
12
+ class ResourceNotFoundError(FrameworkError):
13
+ """可信业务边界可映射为 HTTP 404 的静态缺失。"""
@@ -0,0 +1,53 @@
1
+ """业务模块集中定义的公开安全错误,不把异常正文或动态输入当作响应。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from .base import FrameworkError
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class BusinessErrorDefinition:
14
+ code: str
15
+ message: str
16
+
17
+ def __post_init__(self) -> None:
18
+ if (
19
+ not isinstance(self.code, str)
20
+ or not self.code
21
+ or any(not (part.isascii() and (part.isalnum() or part in "._")) for part in self.code)
22
+ ):
23
+ raise ValueError("business error code must be a stable non-empty identifier")
24
+ if not isinstance(self.message, str) or not self.message.strip():
25
+ raise ValueError("business error message must be a static safe message")
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class BusinessErrorField:
30
+ model_type: type[BaseModel]
31
+ name: str
32
+
33
+ def __post_init__(self) -> None:
34
+ # 字段必须来自静态 DTO contract,不能把运行时字典键或用户输入混入定位信息。
35
+ if not isinstance(self.model_type, type) or not issubclass(self.model_type, BaseModel):
36
+ raise TypeError("business error field requires a Pydantic model type")
37
+ if self.name not in self.model_type.model_fields:
38
+ raise ValueError("business error field must name a declared model field")
39
+
40
+
41
+ class BusinessError(FrameworkError):
42
+ def __init__(
43
+ self, definition: BusinessErrorDefinition, *, fields: tuple[BusinessErrorField, ...] = ()
44
+ ) -> None:
45
+ if not isinstance(definition, BusinessErrorDefinition):
46
+ raise TypeError("business error requires a static definition")
47
+ if not isinstance(fields, tuple) or any(
48
+ not isinstance(field, BusinessErrorField) for field in fields
49
+ ):
50
+ raise TypeError("business error fields must be an immutable tuple")
51
+ self.definition = definition
52
+ self.fields = fields
53
+ super().__init__(definition.code)