python-platform 0.1.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 (344) hide show
  1. python_platform/__init__.py +445 -0
  2. python_platform/application/__init__.py +6 -0
  3. python_platform/application/build_spec.py +37 -0
  4. python_platform/application/builder.py +260 -0
  5. python_platform/application/composition.py +219 -0
  6. python_platform/application/runtime.py +546 -0
  7. python_platform/application/runtime_hooks.py +327 -0
  8. python_platform/application/state.py +216 -0
  9. python_platform/application_services/__init__.py +54 -0
  10. python_platform/application_services/catalog.py +500 -0
  11. python_platform/application_services/contracts.py +188 -0
  12. python_platform/application_services/dispatcher.py +423 -0
  13. python_platform/application_services/errors.py +89 -0
  14. python_platform/application_services/execution.py +199 -0
  15. python_platform/application_services/interceptors.py +44 -0
  16. python_platform/application_services/invocation.py +427 -0
  17. python_platform/application_services/policies.py +46 -0
  18. python_platform/application_services/seeding.py +72 -0
  19. python_platform/application_services/signature.py +49 -0
  20. python_platform/application_services/validation.py +84 -0
  21. python_platform/auditing/__init__.py +12 -0
  22. python_platform/auditing/contracts.py +39 -0
  23. python_platform/auditing/control.py +48 -0
  24. python_platform/auditing/sqlalchemy/__init__.py +6 -0
  25. python_platform/auditing/sqlalchemy/migrations/0001_platform_auditing.py +45 -0
  26. python_platform/auditing/sqlalchemy/migrations/__init__.py +1 -0
  27. python_platform/auditing/sqlalchemy/models.py +31 -0
  28. python_platform/auditing/sqlalchemy/module.py +32 -0
  29. python_platform/auditing/sqlalchemy/store.py +63 -0
  30. python_platform/authorization/__init__.py +38 -0
  31. python_platform/authorization/catalog.py +63 -0
  32. python_platform/authorization/contracts.py +241 -0
  33. python_platform/authorization/definition_discovery.py +81 -0
  34. python_platform/authorization/definitions.py +43 -0
  35. python_platform/authorization/errors.py +40 -0
  36. python_platform/background_execution/__init__.py +15 -0
  37. python_platform/background_execution/application.py +41 -0
  38. python_platform/background_execution/child.py +123 -0
  39. python_platform/background_execution/contracts.py +39 -0
  40. python_platform/background_execution/lifecycle.py +22 -0
  41. python_platform/background_execution/local.py +127 -0
  42. python_platform/background_execution/locks.py +31 -0
  43. python_platform/background_execution/management.py +17 -0
  44. python_platform/background_execution/module.py +10 -0
  45. python_platform/background_execution/permissions.py +14 -0
  46. python_platform/background_execution/processes.py +331 -0
  47. python_platform/background_jobs/__init__.py +31 -0
  48. python_platform/background_jobs/catalog.py +287 -0
  49. python_platform/background_jobs/contracts.py +202 -0
  50. python_platform/background_jobs/errors.py +28 -0
  51. python_platform/background_jobs/execution.py +201 -0
  52. python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
  53. python_platform/background_jobs/pgqueuer/__init__.py +6 -0
  54. python_platform/background_jobs/pgqueuer/enqueue.py +82 -0
  55. python_platform/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +63 -0
  56. python_platform/background_jobs/pgqueuer/migrations/__init__.py +1 -0
  57. python_platform/background_jobs/pgqueuer/module.py +102 -0
  58. python_platform/background_jobs/pgqueuer/options.py +70 -0
  59. python_platform/background_jobs/pgqueuer/runtime.py +479 -0
  60. python_platform/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
  61. python_platform/background_jobs/pgqueuer/supervision.py +201 -0
  62. python_platform/background_workers/__init__.py +22 -0
  63. python_platform/background_workers/catalog.py +96 -0
  64. python_platform/background_workers/contracts.py +124 -0
  65. python_platform/background_workers/errors.py +20 -0
  66. python_platform/background_workers/execution.py +133 -0
  67. python_platform/background_workers/runtime.py +203 -0
  68. python_platform/caching/__init__.py +11 -0
  69. python_platform/caching/catalog.py +73 -0
  70. python_platform/caching/contracts.py +73 -0
  71. python_platform/caching/errors.py +20 -0
  72. python_platform/cli/__init__.py +87 -0
  73. python_platform/cli/errors.py +5 -0
  74. python_platform/cli/project.py +69 -0
  75. python_platform/cli/runtime.py +68 -0
  76. python_platform/configuration/__init__.py +21 -0
  77. python_platform/configuration/composition.py +65 -0
  78. python_platform/configuration/contracts.py +68 -0
  79. python_platform/configuration/dotenv_source.py +51 -0
  80. python_platform/configuration/environment_source.py +38 -0
  81. python_platform/configuration/immutability.py +124 -0
  82. python_platform/configuration/input_shape.py +58 -0
  83. python_platform/configuration/merge.py +103 -0
  84. python_platform/configuration/root.py +215 -0
  85. python_platform/configuration/sources.py +519 -0
  86. python_platform/configuration/values.py +427 -0
  87. python_platform/configuration/yaml_source.py +51 -0
  88. python_platform/developer_kit/__init__.py +1 -0
  89. python_platform/developer_kit/generation.py +182 -0
  90. python_platform/developer_kit/project_metadata.py +45 -0
  91. python_platform/developer_kit/source.py +53 -0
  92. python_platform/developer_kit/templates/module/cookiecutter.json +1 -0
  93. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
  94. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
  95. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
  96. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
  97. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +46 -0
  98. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +29 -0
  99. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +8 -0
  100. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +81 -0
  101. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +86 -0
  102. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
  103. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
  104. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +36 -0
  105. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
  106. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
  107. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +57 -0
  108. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +12 -0
  109. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
  110. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
  111. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
  112. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
  113. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
  114. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
  115. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
  116. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +74 -0
  117. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +58 -0
  118. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +37 -0
  119. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
  120. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
  121. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models.py.jinja +26 -0
  122. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +32 -0
  123. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repository.py.jinja +49 -0
  124. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
  125. python_platform/developer_kit/templates/project/cookiecutter.json +6 -0
  126. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +8 -0
  127. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.env.jinja +13 -0
  128. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
  129. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
  130. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
  131. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +86 -0
  132. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/app.yaml.jinja +16 -0
  133. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
  134. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +13 -0
  135. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
  136. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
  137. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +52 -0
  138. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +38 -0
  139. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
  140. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +101 -0
  141. python_platform/developer_kit/wiring.py +85 -0
  142. python_platform/diagnostics/__init__.py +23 -0
  143. python_platform/diagnostics/journal.py +174 -0
  144. python_platform/diagnostics/model.py +83 -0
  145. python_platform/diagnostics/source.py +25 -0
  146. python_platform/distributed_lock/__init__.py +6 -0
  147. python_platform/distributed_lock/contracts.py +20 -0
  148. python_platform/distributed_lock/options.py +19 -0
  149. python_platform/domain/__init__.py +13 -0
  150. python_platform/domain/aggregates.py +64 -0
  151. python_platform/domain/entities.py +33 -0
  152. python_platform/domain/errors.py +12 -0
  153. python_platform/domain/value_objects.py +31 -0
  154. python_platform/errors/__init__.py +75 -0
  155. python_platform/errors/base.py +13 -0
  156. python_platform/errors/business.py +53 -0
  157. python_platform/errors/configuration.py +119 -0
  158. python_platform/errors/diagnostics.py +12 -0
  159. python_platform/errors/lifecycle.py +164 -0
  160. python_platform/errors/modularity.py +100 -0
  161. python_platform/errors/services.py +100 -0
  162. python_platform/events/__init__.py +25 -0
  163. python_platform/events/aggregate.py +39 -0
  164. python_platform/events/catalog.py +136 -0
  165. python_platform/events/contracts.py +39 -0
  166. python_platform/events/contribution.py +128 -0
  167. python_platform/events/discovery.py +105 -0
  168. python_platform/events/errors.py +48 -0
  169. python_platform/events/runtime.py +192 -0
  170. python_platform/fastapi/__init__.py +49 -0
  171. python_platform/fastapi/action.py +175 -0
  172. python_platform/fastapi/adapter.py +381 -0
  173. python_platform/fastapi/application_services.py +647 -0
  174. python_platform/fastapi/background.py +42 -0
  175. python_platform/fastapi/contracts.py +278 -0
  176. python_platform/fastapi/errors.py +32 -0
  177. python_platform/fastapi/filters.py +111 -0
  178. python_platform/fastapi/health.py +44 -0
  179. python_platform/fastapi/http.py +55 -0
  180. python_platform/fastapi/http_router.py +212 -0
  181. python_platform/fastapi/manual_action.py +105 -0
  182. python_platform/fastapi/middleware.py +93 -0
  183. python_platform/fastapi/parameters.py +97 -0
  184. python_platform/fastapi/realtime/__init__.py +13 -0
  185. python_platform/fastapi/realtime/authentication.py +181 -0
  186. python_platform/fastapi/realtime/connection.py +166 -0
  187. python_platform/fastapi/realtime/module.py +41 -0
  188. python_platform/fastapi/realtime/options.py +50 -0
  189. python_platform/fastapi/realtime/runtime.py +423 -0
  190. python_platform/fastapi/request_context.py +244 -0
  191. python_platform/fastapi/route_integrity.py +88 -0
  192. python_platform/fastapi/routing.py +410 -0
  193. python_platform/fastapi/server.py +181 -0
  194. python_platform/fastapi/settings.py +40 -0
  195. python_platform/fastapi/tracing.py +62 -0
  196. python_platform/fastapi/transfer.py +128 -0
  197. python_platform/fastapi/upload_limits.py +63 -0
  198. python_platform/fastapi/uploads.py +94 -0
  199. python_platform/hosted_services/__init__.py +29 -0
  200. python_platform/hosted_services/bridge.py +420 -0
  201. python_platform/hosted_services/catalog.py +259 -0
  202. python_platform/hosted_services/contracts.py +55 -0
  203. python_platform/hosted_services/errors.py +53 -0
  204. python_platform/hosted_services/options.py +17 -0
  205. python_platform/hosted_services/runtime.py +267 -0
  206. python_platform/hosted_services/state.py +40 -0
  207. python_platform/hosting/__init__.py +4 -0
  208. python_platform/hosting/instance.py +40 -0
  209. python_platform/identity/__init__.py +73 -0
  210. python_platform/identity/application.py +287 -0
  211. python_platform/identity/contracts.py +283 -0
  212. python_platform/identity/errors.py +28 -0
  213. python_platform/identity/http_api.py +90 -0
  214. python_platform/identity/module.py +46 -0
  215. python_platform/identity/passwords.py +34 -0
  216. python_platform/identity/permissions.py +14 -0
  217. python_platform/identity/services.py +108 -0
  218. python_platform/identity/sqlalchemy/__init__.py +5 -0
  219. python_platform/identity/sqlalchemy/migrations/0001_platform_identity.py +151 -0
  220. python_platform/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
  221. python_platform/identity/sqlalchemy/migrations/__init__.py +1 -0
  222. python_platform/identity/sqlalchemy/models.py +85 -0
  223. python_platform/identity/sqlalchemy/module.py +62 -0
  224. python_platform/identity/sqlalchemy/stores.py +526 -0
  225. python_platform/identity/tokens.py +106 -0
  226. python_platform/invocation/__init__.py +3 -0
  227. python_platform/invocation/callables.py +171 -0
  228. python_platform/invocation/contracts.py +33 -0
  229. python_platform/invocation/entries.py +54 -0
  230. python_platform/invocation/function_runtime.py +48 -0
  231. python_platform/invocation/interception.py +184 -0
  232. python_platform/lifecycle/__init__.py +25 -0
  233. python_platform/lifecycle/composition.py +35 -0
  234. python_platform/lifecycle/context.py +31 -0
  235. python_platform/lifecycle/runtime.py +43 -0
  236. python_platform/lifecycle/state.py +20 -0
  237. python_platform/modularity/__init__.py +15 -0
  238. python_platform/modularity/contracts.py +90 -0
  239. python_platform/modularity/discovery.py +143 -0
  240. python_platform/modularity/graph.py +334 -0
  241. python_platform/modularity/registry.py +100 -0
  242. python_platform/modularity/selection.py +16 -0
  243. python_platform/notifications/__init__.py +19 -0
  244. python_platform/notifications/catalog.py +46 -0
  245. python_platform/notifications/contracts.py +107 -0
  246. python_platform/observability/__init__.py +5 -0
  247. python_platform/observability/context.py +43 -0
  248. python_platform/observability/export.py +73 -0
  249. python_platform/observability/formatting.py +89 -0
  250. python_platform/observability/logging.py +118 -0
  251. python_platform/observability/options.py +45 -0
  252. python_platform/observability/tracing.py +80 -0
  253. python_platform/options/__init__.py +19 -0
  254. python_platform/options/aliases.py +331 -0
  255. python_platform/options/contribution.py +62 -0
  256. python_platform/options/immutability.py +48 -0
  257. python_platform/options/input_keys.py +214 -0
  258. python_platform/options/issues.py +335 -0
  259. python_platform/options/models.py +114 -0
  260. python_platform/options/registry.py +280 -0
  261. python_platform/options/schema.py +488 -0
  262. python_platform/options/validation.py +120 -0
  263. python_platform/py.typed +0 -0
  264. python_platform/realtime/__init__.py +12 -0
  265. python_platform/realtime/contracts.py +28 -0
  266. python_platform/realtime/diagnostics.py +37 -0
  267. python_platform/realtime/messages.py +113 -0
  268. python_platform/redis/__init__.py +6 -0
  269. python_platform/redis/distributed_lock.py +212 -0
  270. python_platform/redis/lease_lock.py +36 -0
  271. python_platform/redis/module.py +50 -0
  272. python_platform/redis/notification_runtime.py +182 -0
  273. python_platform/redis/notifications.py +25 -0
  274. python_platform/redis/options.py +35 -0
  275. python_platform/redis/runtime.py +116 -0
  276. python_platform/services/__init__.py +39 -0
  277. python_platform/services/application_bindings.py +221 -0
  278. python_platform/services/arbitration.py +381 -0
  279. python_platform/services/binding.py +102 -0
  280. python_platform/services/contribution.py +438 -0
  281. python_platform/services/convention.py +292 -0
  282. python_platform/services/convention_contracts.py +94 -0
  283. python_platform/services/exposure.py +23 -0
  284. python_platform/services/framework_provider.py +178 -0
  285. python_platform/services/native_graph.py +172 -0
  286. python_platform/services/provider.py +204 -0
  287. python_platform/services/registration.py +101 -0
  288. python_platform/services/runtime.py +298 -0
  289. python_platform/settings/__init__.py +30 -0
  290. python_platform/settings/application.py +95 -0
  291. python_platform/settings/binding.py +33 -0
  292. python_platform/settings/catalog.py +121 -0
  293. python_platform/settings/changes.py +14 -0
  294. python_platform/settings/contracts.py +115 -0
  295. python_platform/settings/definition_discovery.py +71 -0
  296. python_platform/settings/definitions.py +41 -0
  297. python_platform/settings/errors.py +22 -0
  298. python_platform/settings/handlers.py +43 -0
  299. python_platform/settings/management.py +57 -0
  300. python_platform/settings/manager.py +89 -0
  301. python_platform/settings/module.py +7 -0
  302. python_platform/settings/notifications.py +39 -0
  303. python_platform/settings/permissions.py +14 -0
  304. python_platform/settings/provider.py +74 -0
  305. python_platform/settings/refresh.py +140 -0
  306. python_platform/settings/refresh_module.py +45 -0
  307. python_platform/settings/sqlalchemy/__init__.py +5 -0
  308. python_platform/settings/sqlalchemy/migrations/0001_platform_settings.py +31 -0
  309. python_platform/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
  310. python_platform/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
  311. python_platform/settings/sqlalchemy/migrations/__init__.py +1 -0
  312. python_platform/settings/sqlalchemy/models.py +36 -0
  313. python_platform/settings/sqlalchemy/module.py +36 -0
  314. python_platform/settings/sqlalchemy/store.py +86 -0
  315. python_platform/settings/store.py +16 -0
  316. python_platform/settings/values.py +39 -0
  317. python_platform/sqlalchemy/__init__.py +51 -0
  318. python_platform/sqlalchemy/alembic_runtime/__init__.py +1 -0
  319. python_platform/sqlalchemy/alembic_runtime/env.py +27 -0
  320. python_platform/sqlalchemy/alembic_runtime/script.py.mako +14 -0
  321. python_platform/sqlalchemy/auditing.py +48 -0
  322. python_platform/sqlalchemy/errors.py +85 -0
  323. python_platform/sqlalchemy/metadata.py +538 -0
  324. python_platform/sqlalchemy/migration.py +346 -0
  325. python_platform/sqlalchemy/module.py +48 -0
  326. python_platform/sqlalchemy/module_migration.py +148 -0
  327. python_platform/sqlalchemy/options.py +66 -0
  328. python_platform/sqlalchemy/repository.py +33 -0
  329. python_platform/sqlalchemy/runtime.py +85 -0
  330. python_platform/sqlalchemy/session_provider.py +45 -0
  331. python_platform/sqlalchemy/unit_of_work.py +97 -0
  332. python_platform/testing/__init__.py +5 -0
  333. python_platform/testing/runtime.py +112 -0
  334. python_platform/unit_of_work/__init__.py +14 -0
  335. python_platform/unit_of_work/contracts.py +223 -0
  336. python_platform/unit_of_work/errors.py +27 -0
  337. python_platform/unit_of_work/manager.py +210 -0
  338. python_platform/unit_of_work/options.py +92 -0
  339. python_platform-0.1.1.dist-info/METADATA +122 -0
  340. python_platform-0.1.1.dist-info/RECORD +344 -0
  341. python_platform-0.1.1.dist-info/WHEEL +4 -0
  342. python_platform-0.1.1.dist-info/entry_points.txt +9 -0
  343. python_platform-0.1.1.dist-info/licenses/LICENSE +7 -0
  344. python_platform-0.1.1.dist-info/licenses/src/python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
@@ -0,0 +1,39 @@
1
+ """Aggregate pending events to UoW-owned local event queues."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypeVar
6
+
7
+ from ..domain import AggregateRoot
8
+ from ..unit_of_work.contracts import UnitOfWork
9
+ from .runtime import LocalEventBus
10
+
11
+ _TId = TypeVar("_TId")
12
+
13
+
14
+ class AggregateEventCollector:
15
+ """同一事件进入 DOMAIN 与 AFTER_COMMIT 队列,聚合只释放一次。"""
16
+
17
+ __slots__ = ("_events",)
18
+
19
+ def __init__(self, events: LocalEventBus) -> None:
20
+ self._events = events
21
+
22
+ def collect(self, aggregate: AggregateRoot[_TId]) -> tuple[object, ...]:
23
+ if not isinstance(aggregate, AggregateRoot):
24
+ raise TypeError("aggregate must be AggregateRoot")
25
+ # 发布资格先于提取;拒绝非事务发布不能消耗聚合尚未交付的事件。
26
+ work = self._events._manager.require_current(transactional=True)
27
+ return _collect_aggregate_events(work, aggregate)
28
+
29
+
30
+ def _collect_aggregate_events(
31
+ work: UnitOfWork, aggregate: AggregateRoot[_TId]
32
+ ) -> tuple[object, ...]:
33
+ # 两个公开入口先验证事务 owner;收集不触发 flush,也不恢复已释放事件或自动重放。
34
+ assert work._event_queue is not None
35
+ released = aggregate.release_local_events()
36
+ for event in released:
37
+ work._event_queue.publish_domain(event)
38
+ work._event_queue.publish_after_commit(event)
39
+ return released
@@ -0,0 +1,136 @@
1
+ """Local Event phase、subscription provenance 与 immutable catalog。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Awaitable, Callable, Mapping
7
+ from dataclasses import dataclass
8
+ from types import MappingProxyType
9
+ from typing import TYPE_CHECKING
10
+
11
+ from dishka import BaseScope, Scope
12
+
13
+ from ..diagnostics import SourceLocation
14
+ from ..modularity import ModuleKey, ModuleRegistry
15
+ from .contracts import LocalEventPhase
16
+ from .errors import EventContributionError
17
+
18
+ if TYPE_CHECKING:
19
+ from .contribution import EventContributionRegistry
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class LocalEventSubscription:
24
+ event_type: type[object]
25
+ handler_type: type[object] | Callable[..., Awaitable[None]]
26
+ phase: LocalEventPhase
27
+ owner: ModuleKey
28
+ source: SourceLocation
29
+ reason: str
30
+ declaration_index: int
31
+ order: int
32
+
33
+
34
+ class LocalEventCatalog:
35
+ __slots__ = ("_by_phase_and_type", "_subscriptions")
36
+
37
+ def __init__(self, subscriptions: tuple[LocalEventSubscription, ...]) -> None:
38
+ grouped: dict[tuple[LocalEventPhase, type[object]], list[LocalEventSubscription]] = {}
39
+ for subscription in subscriptions:
40
+ grouped.setdefault((subscription.phase, subscription.event_type), []).append(
41
+ subscription
42
+ )
43
+ self._subscriptions = subscriptions
44
+ self._by_phase_and_type: Mapping[
45
+ tuple[LocalEventPhase, type[object]], tuple[LocalEventSubscription, ...]
46
+ ] = MappingProxyType({key: tuple(values) for key, values in grouped.items()})
47
+
48
+ @property
49
+ def subscriptions(self) -> tuple[LocalEventSubscription, ...]:
50
+ return self._subscriptions
51
+
52
+ def for_event(
53
+ self,
54
+ event_type: type[object],
55
+ phase: LocalEventPhase,
56
+ ) -> tuple[LocalEventSubscription, ...]:
57
+ return self._by_phase_and_type.get((phase, event_type), ())
58
+
59
+
60
+ def _build_event_catalog(
61
+ registry: ModuleRegistry,
62
+ contributions: EventContributionRegistry,
63
+ ) -> LocalEventCatalog:
64
+ module_positions = {
65
+ descriptor.key: index for index, descriptor in enumerate(registry.dependency_order)
66
+ }
67
+ entries = sorted(
68
+ contributions.freeze(),
69
+ key=lambda item: (module_positions[item.owner], not item.automatic, item.declaration_index),
70
+ )
71
+ seen: dict[tuple[type[object], object, LocalEventPhase], SourceLocation] = {}
72
+ subscriptions: list[LocalEventSubscription] = []
73
+ for entry in entries:
74
+ identity = (entry.event_type, entry.handler_type, entry.phase)
75
+ if identity in seen:
76
+ raise EventContributionError(
77
+ reason=(
78
+ f"duplicate {entry.phase.value} subscription for "
79
+ f"{entry.event_type.__qualname__}/{entry.handler_type.__qualname__}; "
80
+ f"first={seen[identity]}, duplicate={entry.source}"
81
+ )
82
+ )
83
+ seen[identity] = entry.source
84
+ handler = (
85
+ inspect.getattr_static(entry.handler_type, "handle", None)
86
+ if isinstance(entry.handler_type, type)
87
+ else entry.handler_type
88
+ )
89
+ if isinstance(handler, (staticmethod, classmethod)) or not inspect.iscoroutinefunction(
90
+ handler
91
+ ):
92
+ raise EventContributionError(
93
+ reason=(
94
+ f"{entry.handler_type.__module__}.{entry.handler_type.__qualname__}.handle "
95
+ "must be an instance async method"
96
+ )
97
+ )
98
+ subscriptions.append(
99
+ LocalEventSubscription(
100
+ event_type=entry.event_type,
101
+ handler_type=entry.handler_type,
102
+ phase=entry.phase,
103
+ owner=entry.owner,
104
+ source=entry.source,
105
+ reason=entry.reason,
106
+ declaration_index=entry.declaration_index,
107
+ order=len(subscriptions),
108
+ )
109
+ )
110
+ return LocalEventCatalog(tuple(subscriptions))
111
+
112
+
113
+ def _validate_event_scopes(
114
+ catalog: LocalEventCatalog,
115
+ accepted_single_scopes: Mapping[type[object], BaseScope],
116
+ ) -> None:
117
+ # 原生 alias 的生命周期只有最终 Dishka 图才能确定,不在贡献阶段猜测。
118
+ for subscription in catalog.subscriptions:
119
+ expected_scope = (
120
+ Scope.ACTION if subscription.phase is LocalEventPhase.DOMAIN else Scope.REQUEST
121
+ )
122
+ actual_scope = (
123
+ accepted_single_scopes.get(subscription.handler_type)
124
+ if isinstance(subscription.handler_type, type)
125
+ else expected_scope
126
+ )
127
+ if actual_scope is not expected_scope:
128
+ actual_label = "unregistered" if actual_scope is None else actual_scope.name
129
+ raise EventContributionError(
130
+ reason=(
131
+ f"{subscription.handler_type.__module__}."
132
+ f"{subscription.handler_type.__qualname__} must be "
133
+ f"independently resolvable from an accepted single registration in "
134
+ f"{expected_scope.name} scope, got {actual_label}"
135
+ )
136
+ )
@@ -0,0 +1,39 @@
1
+ """Local Event 稳定 phase contract。"""
2
+
3
+ import inspect
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import TypeVar
8
+
9
+ from .errors import EventContributionError
10
+
11
+
12
+ class LocalEventPhase(str, Enum):
13
+ DOMAIN = "domain"
14
+ AFTER_COMMIT = "after_commit"
15
+
16
+
17
+ _EVENT_HANDLER_ATTRIBUTE = "__python_platform_local_event_handler__"
18
+ _THandler = TypeVar("_THandler")
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class _LocalEventHandlerDefinition:
23
+ phase: LocalEventPhase
24
+
25
+
26
+ def local_event_handler(*, phase: LocalEventPhase) -> Callable[[type[_THandler]], type[_THandler]]:
27
+ """只保存 class-local phase;事件类型在 Application build 由 handle 注解解释。"""
28
+ if not isinstance(phase, LocalEventPhase):
29
+ raise EventContributionError(reason="phase must be LocalEventPhase")
30
+
31
+ def decorate(handler: type[_THandler]) -> type[_THandler]:
32
+ if not isinstance(handler, type) or inspect.isabstract(handler):
33
+ raise EventContributionError(reason="local_event_handler requires a concrete class")
34
+ if _EVENT_HANDLER_ATTRIBUTE in handler.__dict__:
35
+ raise EventContributionError(reason="local_event_handler is already declared")
36
+ setattr(handler, _EVENT_HANDLER_ATTRIBUTE, _LocalEventHandlerDefinition(phase))
37
+ return handler
38
+
39
+ return decorate
@@ -0,0 +1,128 @@
1
+ """一次 Application composition 的 Local Event subscription collector。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Awaitable, Callable
7
+ from dataclasses import dataclass
8
+
9
+ from ..diagnostics import LifecycleStage, SourceLocation
10
+ from ..diagnostics.source import caller_source
11
+ from ..modularity import ModuleDescriptor, ModuleKey
12
+ from .contracts import LocalEventPhase
13
+ from .errors import EventContributionError
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class _EventContribution:
18
+ event_type: type[object]
19
+ handler_type: type[object] | Callable[..., Awaitable[None]]
20
+ phase: LocalEventPhase
21
+ owner: ModuleKey
22
+ source: SourceLocation
23
+ reason: str
24
+ declaration_index: int
25
+ automatic: bool = False
26
+
27
+
28
+ class EventContributor:
29
+ """只在当前 Module.configure 有效,避免 runtime 或 global subscription mutation。"""
30
+
31
+ __slots__ = ("_active", "_descriptor", "_registry", "_stage")
32
+
33
+ def __init__(
34
+ self,
35
+ registry: EventContributionRegistry,
36
+ *,
37
+ descriptor: ModuleDescriptor,
38
+ stage: LifecycleStage,
39
+ ) -> None:
40
+ self._registry = registry
41
+ self._descriptor = descriptor
42
+ self._stage = stage
43
+ self._active = True
44
+
45
+ def subscribe(
46
+ self,
47
+ event_type: type[object],
48
+ handler_type: type[object] | Callable[..., Awaitable[None]],
49
+ phase: LocalEventPhase,
50
+ *,
51
+ reason: str,
52
+ ) -> None:
53
+ if not self._active:
54
+ raise EventContributionError(
55
+ reason=f"event contributor is closed for {self._stage.value}"
56
+ )
57
+ self._registry.add(
58
+ event_type=event_type,
59
+ handler_type=handler_type,
60
+ phase=phase,
61
+ descriptor=self._descriptor,
62
+ source=caller_source(str(self._descriptor.key), depth=2),
63
+ reason=reason,
64
+ )
65
+
66
+ def _close(self) -> None:
67
+ self._active = False
68
+
69
+
70
+ class EventContributionRegistry:
71
+ """Application-owned append-only subscription state,freeze 后只读。"""
72
+
73
+ __slots__ = ("_counts", "_entries", "_frozen")
74
+
75
+ def __init__(self) -> None:
76
+ self._entries: list[_EventContribution] = []
77
+ self._counts: dict[ModuleKey, int] = {}
78
+ self._frozen = False
79
+
80
+ def contributor(
81
+ self,
82
+ *,
83
+ descriptor: ModuleDescriptor,
84
+ stage: LifecycleStage,
85
+ ) -> EventContributor:
86
+ return EventContributor(self, descriptor=descriptor, stage=stage)
87
+
88
+ def add(
89
+ self,
90
+ *,
91
+ event_type: type[object],
92
+ handler_type: type[object] | Callable[..., Awaitable[None]],
93
+ phase: LocalEventPhase,
94
+ descriptor: ModuleDescriptor,
95
+ source: SourceLocation,
96
+ reason: str,
97
+ automatic: bool = False,
98
+ ) -> None:
99
+ if self._frozen:
100
+ raise EventContributionError(reason="event contribution registry is already frozen")
101
+ if not isinstance(event_type, type) or not (
102
+ isinstance(handler_type, type) or inspect.iscoroutinefunction(handler_type)
103
+ ):
104
+ raise EventContributionError(
105
+ reason="event_type must be a class; handler must be a class or async function"
106
+ )
107
+ if not isinstance(phase, LocalEventPhase):
108
+ raise EventContributionError(reason="phase must be LocalEventPhase")
109
+ if not isinstance(reason, str) or not (normalized_reason := reason.strip()):
110
+ raise EventContributionError(reason="subscription reason must be a non-empty string")
111
+ declaration_index = self._counts.get(descriptor.key, 0)
112
+ self._counts[descriptor.key] = declaration_index + 1
113
+ self._entries.append(
114
+ _EventContribution(
115
+ event_type=event_type,
116
+ handler_type=handler_type,
117
+ phase=phase,
118
+ owner=descriptor.key,
119
+ source=source,
120
+ reason=normalized_reason,
121
+ declaration_index=declaration_index,
122
+ automatic=automatic,
123
+ )
124
+ )
125
+
126
+ def freeze(self) -> tuple[_EventContribution, ...]:
127
+ self._frozen = True
128
+ return tuple(self._entries)
@@ -0,0 +1,105 @@
1
+ """解释冻结发现结果,形成一次 Application 的有序自动订阅贡献。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import types
7
+ from collections.abc import Callable
8
+ from typing import Any, Union, cast, get_args, get_origin
9
+
10
+ from dishka import Scope
11
+
12
+ from ..invocation.callables import _resolved_parameter_hints
13
+ from ..modularity import ModuleKey, ModuleRegistry
14
+ from ..modularity.discovery import _DiscoveredType
15
+ from .contracts import _EVENT_HANDLER_ATTRIBUTE, LocalEventPhase, _LocalEventHandlerDefinition
16
+ from .contribution import EventContributionRegistry
17
+ from .errors import EventContributionError
18
+
19
+
20
+ def _discover_event_handlers(
21
+ registry: ModuleRegistry,
22
+ discoveries: tuple[_DiscoveredType, ...],
23
+ contributions: EventContributionRegistry,
24
+ ) -> dict[type[object], Scope]:
25
+ positions = {item.key: index for index, item in enumerate(registry.dependency_order)}
26
+ owners: dict[type[object], ModuleKey] = {}
27
+ scopes: dict[type[object], Scope] = {}
28
+ for discovery in sorted(
29
+ discoveries,
30
+ key=lambda item: (
31
+ positions[item.owner],
32
+ f"{item.candidate.__module__}.{item.candidate.__qualname__}",
33
+ ),
34
+ ):
35
+ candidate = discovery.candidate
36
+ metadata = candidate.__dict__.get(_EVENT_HANDLER_ATTRIBUTE)
37
+ if metadata is None or inspect.isabstract(candidate):
38
+ continue
39
+ if not isinstance(metadata, _LocalEventHandlerDefinition):
40
+ raise EventContributionError(reason=f"{discovery.source}: invalid event metadata")
41
+ if candidate in owners:
42
+ if owners[candidate] != discovery.owner:
43
+ raise EventContributionError(
44
+ reason=f"{discovery.source}: handler is already owned by {owners[candidate]}"
45
+ )
46
+ continue
47
+ owners[candidate] = discovery.owner
48
+ scopes[candidate] = (
49
+ Scope.ACTION if metadata.phase is LocalEventPhase.DOMAIN else Scope.REQUEST
50
+ )
51
+ for event_type in _handler_event_types(candidate):
52
+ contributions.add(
53
+ event_type=event_type,
54
+ handler_type=candidate,
55
+ phase=metadata.phase,
56
+ descriptor=registry.descriptors[discovery.owner],
57
+ source=discovery.source,
58
+ reason="local_event_handler declaration",
59
+ automatic=True,
60
+ )
61
+ return scopes
62
+
63
+
64
+ def _handler_event_types(candidate: type[object]) -> tuple[type[object], ...]:
65
+ method = inspect.getattr_static(candidate, "handle", None)
66
+ if isinstance(method, (classmethod, staticmethod)) or not inspect.iscoroutinefunction(method):
67
+ raise EventContributionError(
68
+ reason=f"{candidate.__qualname__}.handle must be an instance async method"
69
+ )
70
+ parameters = tuple(inspect.signature(method).parameters.values())
71
+ if len(parameters) < 2 or parameters[1].kind not in (
72
+ inspect.Parameter.POSITIONAL_ONLY,
73
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
74
+ ):
75
+ raise EventContributionError(
76
+ reason=f"{candidate.__qualname__}.handle requires an event argument"
77
+ )
78
+ event = parameters[1]
79
+ try:
80
+ hints = _resolved_parameter_hints(
81
+ cast(Callable[..., object], method), {event.name: event.annotation}
82
+ )
83
+ except TypeError as error:
84
+ raise EventContributionError(
85
+ reason=f"{candidate.__qualname__}.handle: unresolved event annotation"
86
+ ) from error
87
+ annotation = hints[event.name]
88
+ choices = (
89
+ get_args(annotation)
90
+ if get_origin(annotation) in (Union, types.UnionType)
91
+ else (annotation,)
92
+ )
93
+ if any(
94
+ not isinstance(item, type)
95
+ or item in (Any, type(None), inspect.Parameter.empty)
96
+ or get_origin(item) is not None
97
+ for item in choices
98
+ ):
99
+ raise EventContributionError(
100
+ reason=(
101
+ f"{candidate.__qualname__}.handle requires concrete event types, "
102
+ "without Any/None/generics"
103
+ )
104
+ )
105
+ return cast(tuple[type[object], ...], choices)
@@ -0,0 +1,48 @@
1
+ """Local Event contribution、cycle 与 phase-specific failure contracts。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+
7
+ from ..errors.base import FrameworkError
8
+
9
+
10
+ class EventContributionError(FrameworkError):
11
+ def __init__(self, *, reason: str) -> None:
12
+ self.reason = reason
13
+ super().__init__(f"Invalid local event contribution: {reason}")
14
+
15
+
16
+ class EventCycleError(FrameworkError):
17
+ def __init__(self, *, event_type: type[object], ancestry: tuple[type[object], ...]) -> None:
18
+ self.event_type = event_type
19
+ self.ancestry = ancestry
20
+ rendered = " -> ".join(item.__qualname__ for item in (*ancestry, event_type))
21
+ super().__init__(f"Local event cycle detected: {rendered}")
22
+
23
+
24
+ class LocalEventHandlerError(FrameworkError):
25
+ def __init__(
26
+ self,
27
+ *,
28
+ event_type: type[object],
29
+ handler_type: type[object] | Callable[..., Awaitable[None]],
30
+ original_error: BaseException,
31
+ committed: bool,
32
+ ) -> None:
33
+ self.event_type = event_type
34
+ self.handler_type = handler_type
35
+ self.original_error = original_error
36
+ self.committed = committed
37
+ super().__init__(
38
+ f"Local event handler {handler_type.__module__}.{handler_type.__qualname__} failed "
39
+ f"for {event_type.__module__}.{event_type.__qualname__} "
40
+ f"(committed={committed}, error={type(original_error).__name__})"
41
+ )
42
+
43
+
44
+ class PostCommitEventError(FrameworkError):
45
+ def __init__(self, *, failures: tuple[LocalEventHandlerError, ...]) -> None:
46
+ self.failures = failures
47
+ self.committed = True
48
+ super().__init__(f"Post-commit local event dispatch failed ({len(failures)} failure(s))")
@@ -0,0 +1,192 @@
1
+ """UoW-owned FIFO envelopes 与 domain/post-commit dispatch。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ from dataclasses import dataclass
7
+ from typing import Protocol
8
+ from uuid import UUID, uuid4
9
+
10
+ from dishka import AsyncContainer, Scope
11
+
12
+ from ..invocation import ApplicationInvocationContext
13
+ from ..unit_of_work.manager import UnitOfWorkManager
14
+ from .catalog import LocalEventCatalog, LocalEventSubscription
15
+ from .contracts import LocalEventPhase
16
+ from .errors import EventCycleError, LocalEventHandlerError, PostCommitEventError
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class _EventEnvelope:
21
+ envelope_id: UUID
22
+ invocation_id: UUID
23
+ parent_envelope_id: UUID | None
24
+ event: object
25
+ event_type: type[object]
26
+ ancestry: tuple[type[object], ...]
27
+
28
+
29
+ class _EventHandler(Protocol):
30
+ async def handle(self, event: object) -> None: ...
31
+
32
+
33
+ class _LocalEventQueue:
34
+ """队列只归一个具体 UoW;handler resolution 仍使用 Dishka 原生 scope。"""
35
+
36
+ __slots__ = (
37
+ "_action_container",
38
+ "_after_commit",
39
+ "_catalog",
40
+ "_context",
41
+ "_current",
42
+ "_domain",
43
+ )
44
+
45
+ def __init__(
46
+ self,
47
+ container: AsyncContainer,
48
+ context: ApplicationInvocationContext,
49
+ catalog: LocalEventCatalog,
50
+ ) -> None:
51
+ self._action_container = container
52
+ self._context = context
53
+ self._catalog = catalog
54
+ self._domain: deque[_EventEnvelope] = deque()
55
+ self._after_commit: deque[_EventEnvelope] = deque()
56
+ self._current: _EventEnvelope | None = None
57
+
58
+ def publish_domain(self, event: object) -> None:
59
+ self._publish(event, self._domain)
60
+
61
+ def publish_after_commit(self, event: object) -> None:
62
+ self._publish(event, self._after_commit)
63
+
64
+ async def _dispatch_domain(self) -> None:
65
+ request = (
66
+ self._action_container.parent_container
67
+ if self._action_container.scope is Scope.ACTION
68
+ else self._action_container
69
+ )
70
+ assert request is not None
71
+ # 每个 UoW 的 handler scope 独立;手动 UoW 的原调用 ACTION 可能已返回。
72
+ from ..application_services.invocation import _managed_action
73
+
74
+ async with _managed_action(request, self._context, committed=lambda: False) as action:
75
+ await self._drain_domain(action)
76
+
77
+ async def _drain_domain(self, action: AsyncContainer) -> None:
78
+ while self._domain:
79
+ envelope = self._domain.popleft()
80
+ await self._dispatch_envelope(
81
+ envelope,
82
+ action,
83
+ LocalEventPhase.DOMAIN,
84
+ committed=False,
85
+ )
86
+
87
+ async def _dispatch_after_commit(self) -> None:
88
+ request_container = (
89
+ self._action_container.parent_container
90
+ if self._action_container.scope is Scope.ACTION
91
+ else self._action_container
92
+ )
93
+ if request_container is None or request_container.registry.scope is not Scope.REQUEST:
94
+ raise RuntimeError("ACTION container has no REQUEST parent for post-commit dispatch")
95
+ failures: list[LocalEventHandlerError] = []
96
+ while self._after_commit:
97
+ envelope = self._after_commit.popleft()
98
+ for subscription in self._catalog.for_event(
99
+ envelope.event_type,
100
+ LocalEventPhase.AFTER_COMMIT,
101
+ ):
102
+ try:
103
+ await self._invoke_handler(
104
+ request_container,
105
+ subscription,
106
+ envelope,
107
+ committed=True,
108
+ )
109
+ except LocalEventHandlerError as failure:
110
+ failures.append(failure)
111
+ if failures:
112
+ raise PostCommitEventError(failures=tuple(failures))
113
+
114
+ def _publish(self, event: object, queue: deque[_EventEnvelope]) -> None:
115
+ event_type = type(event)
116
+ parent = self._current
117
+ ancestry = parent.ancestry if parent is not None else ()
118
+ if event_type in ancestry:
119
+ raise EventCycleError(event_type=event_type, ancestry=ancestry)
120
+ queue.append(
121
+ _EventEnvelope(
122
+ envelope_id=uuid4(),
123
+ invocation_id=self._context.invocation_id,
124
+ parent_envelope_id=parent.envelope_id if parent is not None else None,
125
+ event=event,
126
+ event_type=event_type,
127
+ ancestry=(*ancestry, event_type),
128
+ )
129
+ )
130
+
131
+ async def _dispatch_envelope(
132
+ self,
133
+ envelope: _EventEnvelope,
134
+ container: AsyncContainer,
135
+ phase: LocalEventPhase,
136
+ *,
137
+ committed: bool,
138
+ ) -> None:
139
+ for subscription in self._catalog.for_event(envelope.event_type, phase):
140
+ await self._invoke_handler(
141
+ container,
142
+ subscription,
143
+ envelope,
144
+ committed=committed,
145
+ )
146
+
147
+ async def _invoke_handler(
148
+ self,
149
+ container: AsyncContainer,
150
+ subscription: LocalEventSubscription,
151
+ envelope: _EventEnvelope,
152
+ *,
153
+ committed: bool,
154
+ ) -> None:
155
+ previous = self._current
156
+ self._current = envelope
157
+ try:
158
+ from ..application_services.invocation import _InvocationRuntime
159
+ from ..invocation.interception import _EntryInterceptionCatalog
160
+
161
+ entries = await container.get(_EntryInterceptionCatalog)
162
+ runtime = await container.get(_InvocationRuntime)
163
+ await entries.invoke(
164
+ runtime, container, subscription.handler_type, "handle", (envelope.event,)
165
+ )
166
+ except Exception as original_error:
167
+ error = LocalEventHandlerError(
168
+ event_type=envelope.event_type,
169
+ handler_type=subscription.handler_type,
170
+ original_error=original_error,
171
+ committed=committed,
172
+ )
173
+ raise error from original_error
174
+ finally:
175
+ self._current = previous
176
+
177
+
178
+ class LocalEventBus:
179
+ """发布能力解析具体 UoW 的队列;注入本身不创建 UoW 或资源。"""
180
+
181
+ def __init__(self, manager: UnitOfWorkManager) -> None:
182
+ self._manager = manager
183
+
184
+ def publish_domain(self, event: object) -> None:
185
+ work = self._manager.require_current(transactional=True)
186
+ assert work._event_queue is not None
187
+ work._event_queue.publish_domain(event)
188
+
189
+ def publish_after_commit(self, event: object) -> None:
190
+ work = self._manager.require_current(transactional=True)
191
+ assert work._event_queue is not None
192
+ work._event_queue.publish_after_commit(event)