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,90 @@
1
+ """Module declaration、identity 与显式 dependency contracts。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from types import MappingProxyType
8
+ from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias
9
+
10
+ from ..diagnostics import SourceLocation
11
+
12
+ if TYPE_CHECKING:
13
+ from ..background_jobs import BackgroundJobDefinition, BackgroundJobSchedule
14
+ from ..background_workers import BackgroundWorkerDefinition
15
+ from ..caching import CacheDefinition
16
+ from ..hosted_services import HostedService
17
+ from ..lifecycle import (
18
+ ConfigureContext,
19
+ InitializeContext,
20
+ PostConfigureContext,
21
+ PostInitializeContext,
22
+ PreConfigureContext,
23
+ PreInitializeContext,
24
+ ShutdownContext,
25
+ )
26
+ from ..notifications import NotificationDefinition
27
+ from ..settings.handlers import SettingHandlerDefinition
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ModuleRef:
32
+ target: str
33
+
34
+
35
+ @dataclass(frozen=True, order=True, slots=True)
36
+ class ModuleKey:
37
+ module: str
38
+ qualname: str
39
+
40
+ def __str__(self) -> str:
41
+ return f"{self.module}:{self.qualname}"
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class ModuleDependencyEdge:
46
+ """保留一次显式依赖声明的原始引用与 owner source。"""
47
+
48
+ owner: ModuleKey
49
+ dependency: ModuleKey
50
+ reference: str
51
+ source: SourceLocation
52
+
53
+
54
+ class AppModule:
55
+ """模块 Template Method;每次 Application build 都创建独立实例。"""
56
+
57
+ dependencies: ClassVar[tuple[type[AppModule] | ModuleRef, ...]] = ()
58
+ scan_packages: ClassVar[tuple[str, ...]] = ()
59
+ configuration_defaults: ClassVar[Mapping[str, object]] = MappingProxyType({})
60
+ cache_definitions: ClassVar[tuple[CacheDefinition[Any], ...]] = ()
61
+ setting_handler_definitions: ClassVar[tuple[SettingHandlerDefinition[Any], ...]] = ()
62
+ notification_definitions: ClassVar[tuple[NotificationDefinition[Any], ...]] = ()
63
+ background_worker_definitions: ClassVar[tuple[BackgroundWorkerDefinition, ...]] = ()
64
+ background_job_definitions: ClassVar[tuple[BackgroundJobDefinition[Any], ...]] = ()
65
+ background_job_schedules: ClassVar[tuple[BackgroundJobSchedule, ...]] = ()
66
+ hosted_services: ClassVar[tuple[type[HostedService], ...]] = ()
67
+
68
+ def pre_configure(self, context: PreConfigureContext) -> None:
69
+ pass
70
+
71
+ def configure(self, context: ConfigureContext) -> None:
72
+ pass
73
+
74
+ def post_configure(self, context: PostConfigureContext) -> None:
75
+ pass
76
+
77
+ async def pre_initialize(self, context: PreInitializeContext) -> None:
78
+ pass
79
+
80
+ async def initialize(self, context: InitializeContext) -> None:
81
+ pass
82
+
83
+ async def post_initialize(self, context: PostInitializeContext) -> None:
84
+ pass
85
+
86
+ async def shutdown(self, context: ShutdownContext) -> None:
87
+ pass
88
+
89
+
90
+ ModuleDependency: TypeAlias = type[AppModule] | ModuleRef
@@ -0,0 +1,143 @@
1
+ """冻结 Module 扫描范围的类型发现;能力解释复用同一 Application-local 结果。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import inspect
7
+ import pkgutil
8
+ from collections.abc import Mapping
9
+ from dataclasses import dataclass
10
+ from types import MappingProxyType, ModuleType
11
+
12
+ from ..diagnostics import SourceLocation
13
+ from ..errors import ConventionScanError
14
+ from .contracts import ModuleKey
15
+ from .registry import ModuleDescriptor, ModuleRegistry
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class _DiscoveredType:
20
+ owner: ModuleKey
21
+ package: str
22
+ candidate: type[object]
23
+ source: SourceLocation
24
+
25
+
26
+ def _discover_types(
27
+ registry: ModuleRegistry, packages: Mapping[ModuleKey, tuple[str, ...]]
28
+ ) -> tuple[_DiscoveredType, ...]:
29
+ return tuple(
30
+ _DiscoveredType(
31
+ descriptor.key, package, candidate, _class_source(candidate, descriptor.key)
32
+ )
33
+ for descriptor in registry.dependency_order
34
+ for package in packages[descriptor.key]
35
+ for module in _import_modules(descriptor, package)
36
+ for candidate in _module_candidates(module)
37
+ )
38
+
39
+
40
+ def _import_modules(
41
+ descriptor: ModuleDescriptor,
42
+ package_name: str,
43
+ ) -> tuple[ModuleType, ...]:
44
+ try:
45
+ root = importlib.import_module(package_name)
46
+ module_names = {root.__name__}
47
+ package_path = getattr(root, "__path__", None)
48
+ if package_path is not None:
49
+ module_names.update(
50
+ item.name
51
+ for item in pkgutil.walk_packages(
52
+ package_path,
53
+ f"{root.__name__}.",
54
+ onerror=_raise_scan_import_error,
55
+ )
56
+ )
57
+ return tuple(importlib.import_module(name) for name in sorted(module_names))
58
+ except Exception as original_error:
59
+ error = ConventionScanError(
60
+ owner=descriptor.key,
61
+ package=package_name,
62
+ source=descriptor.declaration_source,
63
+ reason="unable to import controlled convention scan package",
64
+ original_error=original_error,
65
+ )
66
+ raise error from original_error
67
+
68
+
69
+ def _module_candidates(module: ModuleType) -> tuple[type[object], ...]:
70
+ return tuple(
71
+ value
72
+ for value in vars(module).values()
73
+ if isinstance(value, type) and value.__module__ == module.__name__
74
+ )
75
+
76
+
77
+ def _class_source(candidate: type[object], owner: ModuleKey) -> SourceLocation:
78
+ file_name: str | None = None
79
+ line: int | None = None
80
+ try:
81
+ file_name = inspect.getsourcefile(candidate)
82
+ _, line = inspect.getsourcelines(candidate)
83
+ except (OSError, TypeError):
84
+ pass
85
+ return SourceLocation(
86
+ owner=str(owner),
87
+ file=file_name,
88
+ line=line,
89
+ symbol=f"{candidate.__module__}.{candidate.__qualname__}",
90
+ )
91
+
92
+
93
+ def _raise_scan_import_error(module_name: str) -> None:
94
+ raise ImportError(f"unable to import convention scan package {module_name}")
95
+
96
+
97
+ def _snapshot_scan_packages(
98
+ registry: ModuleRegistry,
99
+ ) -> Mapping[ModuleKey, tuple[str, ...]]:
100
+ snapshots = {
101
+ descriptor.key: _validate_scan_packages(descriptor)
102
+ for descriptor in registry.dependency_order
103
+ }
104
+ return MappingProxyType(snapshots)
105
+
106
+
107
+ def _validate_scan_packages(descriptor: ModuleDescriptor) -> tuple[str, ...]:
108
+ packages = descriptor.module_type.scan_packages
109
+ if not isinstance(packages, tuple):
110
+ raise ConventionScanError(
111
+ owner=descriptor.key,
112
+ package=None,
113
+ source=descriptor.declaration_source,
114
+ reason="scan_packages metadata must be an immutable tuple",
115
+ original_error=None,
116
+ )
117
+ seen: set[str] = set()
118
+ snapshot: list[str] = []
119
+ for package in packages:
120
+ if (
121
+ not isinstance(package, str)
122
+ or not package
123
+ or any(not segment.isidentifier() for segment in package.split("."))
124
+ ):
125
+ raise ConventionScanError(
126
+ owner=descriptor.key,
127
+ package=package if isinstance(package, str) else None,
128
+ source=descriptor.declaration_source,
129
+ reason="scan package must be a dotted Python module name",
130
+ original_error=None,
131
+ )
132
+ if package in seen:
133
+ raise ConventionScanError(
134
+ owner=descriptor.key,
135
+ package=package,
136
+ source=descriptor.declaration_source,
137
+ reason="scan package is declared more than once",
138
+ original_error=None,
139
+ )
140
+ seen.add(package)
141
+ snapshot.append(package)
142
+ # 不保留 Module class attribute 的 tuple identity,快照只归当前 build 所有。
143
+ return tuple(snapshot)
@@ -0,0 +1,334 @@
1
+ """Module reference resolution、graph validation 与 instance construction。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import inspect
7
+ from collections.abc import Mapping
8
+ from graphlib import CycleError, TopologicalSorter
9
+ from itertools import pairwise
10
+ from typing import TypeGuard
11
+
12
+ from ..diagnostics import SourceLocation
13
+ from ..errors import (
14
+ DuplicateModuleIdentityError,
15
+ GraphInvariantError,
16
+ InvalidModuleReferenceError,
17
+ ModuleCycleError,
18
+ ModuleInstantiationError,
19
+ ModuleResolutionError,
20
+ )
21
+ from .contracts import AppModule, ModuleDependency, ModuleDependencyEdge, ModuleKey, ModuleRef
22
+ from .registry import ModuleDescriptor, ModuleRegistry
23
+
24
+
25
+ class _ModuleGraphBuilder:
26
+ """拥有一次 graph discovery 的全部 mutable state,成功后只发布 immutable registry。"""
27
+
28
+ __slots__ = (
29
+ "_dependency_edges",
30
+ "_dependency_paths",
31
+ "_discovery_indices",
32
+ "_module_types",
33
+ "_predecessors",
34
+ "_sources",
35
+ "_startup",
36
+ )
37
+
38
+ def __init__(self, startup: type[AppModule] | ModuleRef) -> None:
39
+ self._startup = startup
40
+ self._module_types: dict[ModuleKey, type[AppModule]] = {}
41
+ self._discovery_indices: dict[ModuleKey, int] = {}
42
+ self._predecessors: dict[ModuleKey, tuple[ModuleKey, ...]] = {}
43
+ self._dependency_paths: dict[ModuleKey, tuple[ModuleKey, ...]] = {}
44
+ self._dependency_edges: dict[tuple[ModuleKey, ModuleKey], ModuleDependencyEdge] = {}
45
+ self._sources: dict[ModuleKey, SourceLocation] = {}
46
+
47
+ def build(self) -> ModuleRegistry:
48
+ startup_type, startup_key = self._resolve_module(
49
+ self._startup, owner=None, owner_source=None, resolution_chain=()
50
+ )
51
+ self._discover(startup_type, startup_key, (), ())
52
+ dependency_keys = self._stable_dependency_order(startup_key)
53
+ instances = self._instantiate(dependency_keys)
54
+ descriptors = {
55
+ key: ModuleDescriptor(
56
+ key=key,
57
+ module_type=self._module_types[key],
58
+ instance=instances[key],
59
+ dependencies=self._predecessors[key],
60
+ dependency_path=self._dependency_paths[key],
61
+ dependency_edges=tuple(
62
+ self._dependency_edges[(key, dependency)]
63
+ for dependency in self._predecessors[key]
64
+ ),
65
+ discovery_index=self._discovery_indices[key],
66
+ declaration_source=self._sources[key],
67
+ )
68
+ for key in dependency_keys
69
+ }
70
+ dependency_order = tuple(descriptors[key] for key in dependency_keys)
71
+ return ModuleRegistry(
72
+ startup=descriptors[startup_key],
73
+ descriptors=descriptors,
74
+ dependency_order=dependency_order,
75
+ )
76
+
77
+ def _discover(
78
+ self,
79
+ module_type: type[AppModule],
80
+ key: ModuleKey,
81
+ resolution_chain: tuple[str, ...],
82
+ owner_path: tuple[ModuleKey, ...],
83
+ ) -> None:
84
+ existing = self._module_types.get(key)
85
+ if existing is not None:
86
+ if existing is not module_type:
87
+ raise DuplicateModuleIdentityError(
88
+ key=key, first_type=existing, second_type=module_type
89
+ )
90
+ return
91
+
92
+ self._module_types[key] = module_type
93
+ self._discovery_indices[key] = len(self._discovery_indices)
94
+ self._dependency_paths[key] = (*owner_path, key)
95
+ source = self._module_source(module_type, key)
96
+ self._sources[key] = source
97
+
98
+ declared_dependencies = module_type.dependencies
99
+ if not isinstance(declared_dependencies, tuple):
100
+ raise InvalidModuleReferenceError(
101
+ owner=key,
102
+ reference=type(declared_dependencies).__name__,
103
+ reason="dependencies metadata must be an immutable tuple",
104
+ source=source,
105
+ )
106
+
107
+ normalized_dependencies: list[ModuleKey] = []
108
+ seen_dependencies: set[ModuleKey] = set()
109
+ child_chain = (*resolution_chain, str(key))
110
+ for dependency in declared_dependencies:
111
+ dependency_type, dependency_key = self._resolve_module(
112
+ dependency,
113
+ owner=key,
114
+ owner_source=source,
115
+ resolution_chain=child_chain,
116
+ )
117
+ if dependency_key not in seen_dependencies:
118
+ normalized_dependencies.append(dependency_key)
119
+ seen_dependencies.add(dependency_key)
120
+ self._dependency_edges[(key, dependency_key)] = ModuleDependencyEdge(
121
+ owner=key,
122
+ dependency=dependency_key,
123
+ reference=self._dependency_reference(dependency, dependency_key),
124
+ source=source,
125
+ )
126
+ self._discover(
127
+ dependency_type,
128
+ dependency_key,
129
+ child_chain,
130
+ self._dependency_paths[key],
131
+ )
132
+ self._predecessors[key] = tuple(normalized_dependencies)
133
+
134
+ def _instantiate(self, dependency_keys: tuple[ModuleKey, ...]) -> dict[ModuleKey, AppModule]:
135
+ # graph 完整验证后才实例化,constructor 失败也不会触发 lifecycle hook。
136
+ instances: dict[ModuleKey, AppModule] = {}
137
+ for key in dependency_keys:
138
+ module_type = self._module_types[key]
139
+ try:
140
+ instance = module_type()
141
+ except Exception as original_error:
142
+ error = ModuleInstantiationError(
143
+ module=key,
144
+ source=self._sources[key],
145
+ original_error=original_error,
146
+ )
147
+ raise error from original_error
148
+ instances[key] = instance
149
+ return instances
150
+
151
+ def _stable_dependency_order(self, startup: ModuleKey) -> tuple[ModuleKey, ...]:
152
+ sorter: TopologicalSorter[ModuleKey] = TopologicalSorter(self._predecessors)
153
+ try:
154
+ sorter.prepare()
155
+ except CycleError as cycle_error:
156
+ raw_path = self._cycle_from_error(cycle_error)
157
+ path = self._normalize_cycle(raw_path)
158
+ try:
159
+ edges = tuple(
160
+ self._dependency_edges[(owner, dependency)]
161
+ for owner, dependency in pairwise(path)
162
+ )
163
+ except KeyError as missing_edge:
164
+ raise GraphInvariantError(
165
+ "cycle path had no matching dependency edge"
166
+ ) from missing_edge
167
+ raise ModuleCycleError(startup=startup, path=path, edges=edges) from cycle_error
168
+
169
+ order: list[ModuleKey] = []
170
+ while sorter.is_active():
171
+ ready = tuple(sorted(sorter.get_ready(), key=self._discovery_indices.__getitem__))
172
+ order.extend(ready)
173
+ sorter.done(*ready)
174
+ positions = {key: index for index, key in enumerate(order)}
175
+ for owner, dependencies in self._predecessors.items():
176
+ if any(positions[dependency] >= positions[owner] for dependency in dependencies):
177
+ raise GraphInvariantError("dependency order placed an owner before its dependency")
178
+ return tuple(order)
179
+
180
+ def _normalize_cycle(self, raw_path: tuple[ModuleKey, ...]) -> tuple[ModuleKey, ...]:
181
+ # graphlib 报告 predecessor 方向;公开错误固定为 owner -> declared dependency 方向。
182
+ candidates = (raw_path, tuple(reversed(raw_path)))
183
+ declared_path = next(
184
+ (
185
+ candidate
186
+ for candidate in candidates
187
+ if self._follows_declarations(candidate, self._predecessors)
188
+ ),
189
+ None,
190
+ )
191
+ if declared_path is None:
192
+ raise GraphInvariantError(
193
+ "cycle direction could not be matched to dependency declarations"
194
+ )
195
+ open_path = declared_path[:-1]
196
+ start_index = min(
197
+ range(len(open_path)), key=lambda index: self._discovery_indices[open_path[index]]
198
+ )
199
+ rotated = open_path[start_index:] + open_path[:start_index]
200
+ return (*rotated, rotated[0])
201
+
202
+ @staticmethod
203
+ def _module_key(module_type: type[AppModule]) -> ModuleKey:
204
+ return ModuleKey(module=module_type.__module__, qualname=module_type.__qualname__)
205
+
206
+ @staticmethod
207
+ def _module_source(module_type: type[AppModule], key: ModuleKey) -> SourceLocation:
208
+ file_name: str | None = None
209
+ line: int | None = None
210
+ try:
211
+ file_name = inspect.getsourcefile(module_type)
212
+ _, line = inspect.getsourcelines(module_type)
213
+ except (OSError, TypeError):
214
+ pass
215
+ return SourceLocation(str(key), file_name, line, module_type.__qualname__)
216
+
217
+ @staticmethod
218
+ def _is_module_type(value: object) -> TypeGuard[type[AppModule]]:
219
+ return isinstance(value, type) and issubclass(value, AppModule)
220
+
221
+ @classmethod
222
+ def _resolve_module(
223
+ cls,
224
+ dependency: object,
225
+ *,
226
+ owner: ModuleKey | None,
227
+ owner_source: SourceLocation | None,
228
+ resolution_chain: tuple[str, ...],
229
+ ) -> tuple[type[AppModule], ModuleKey]:
230
+ # 动态 import 是不可信边界;收窄为 AppModule type 后图算法不再重复猜测类型。
231
+ if cls._is_module_type(dependency):
232
+ return dependency, cls._module_key(dependency)
233
+ if not isinstance(dependency, ModuleRef):
234
+ raise InvalidModuleReferenceError(
235
+ owner=owner,
236
+ reference=type(dependency).__name__,
237
+ reason="dependency must be an AppModule type or ModuleRef",
238
+ source=owner_source,
239
+ )
240
+ module_name, qualname = cls._parse_module_ref(
241
+ dependency.target, owner=owner, owner_source=owner_source
242
+ )
243
+ try:
244
+ resolved: object = importlib.import_module(module_name)
245
+ for part in qualname.split("."):
246
+ resolved = getattr(resolved, part)
247
+ except Exception as original_error:
248
+ error = ModuleResolutionError(
249
+ owner=owner,
250
+ reference=dependency.target,
251
+ resolution_chain=(*resolution_chain, dependency.target),
252
+ source=owner_source,
253
+ original_error=original_error,
254
+ )
255
+ raise error from original_error
256
+ if not cls._is_module_type(resolved):
257
+ raise InvalidModuleReferenceError(
258
+ owner=owner,
259
+ reference=dependency.target,
260
+ reason="resolved target must be an AppModule class",
261
+ source=owner_source,
262
+ )
263
+ return resolved, cls._module_key(resolved)
264
+
265
+ @staticmethod
266
+ def _parse_module_ref(
267
+ target: object,
268
+ *,
269
+ owner: ModuleKey | None,
270
+ owner_source: SourceLocation | None,
271
+ ) -> tuple[str, str]:
272
+ if not isinstance(target, str):
273
+ raise InvalidModuleReferenceError(
274
+ owner=owner,
275
+ reference=type(target).__name__,
276
+ reason="ModuleRef.target must be a string",
277
+ source=owner_source,
278
+ )
279
+ if target.count(":") != 1:
280
+ raise InvalidModuleReferenceError(
281
+ owner=owner,
282
+ reference=target,
283
+ reason="target must contain exactly one ':' separator",
284
+ source=owner_source,
285
+ )
286
+ module_name, qualname = target.split(":")
287
+ if not module_name or not qualname:
288
+ raise InvalidModuleReferenceError(
289
+ owner=owner,
290
+ reference=target,
291
+ reason="module and qualified name must both be non-empty",
292
+ source=owner_source,
293
+ )
294
+ if any(not part.isidentifier() for part in module_name.split(".")):
295
+ raise InvalidModuleReferenceError(
296
+ owner=owner,
297
+ reference=target,
298
+ reason="each module-name segment must be a Python identifier",
299
+ source=owner_source,
300
+ )
301
+ if any(not part.isidentifier() for part in qualname.split(".")):
302
+ raise InvalidModuleReferenceError(
303
+ owner=owner,
304
+ reference=target,
305
+ reason="each qualified-name segment must be a Python identifier",
306
+ source=owner_source,
307
+ )
308
+ return module_name, qualname
309
+
310
+ @staticmethod
311
+ def _dependency_reference(dependency: ModuleDependency, dependency_key: ModuleKey) -> str:
312
+ return dependency.target if isinstance(dependency, ModuleRef) else str(dependency_key)
313
+
314
+ @staticmethod
315
+ def _cycle_from_error(error: CycleError) -> tuple[ModuleKey, ...]:
316
+ if len(error.args) < 2 or not isinstance(error.args[1], (list, tuple)):
317
+ raise GraphInvariantError("graphlib did not provide a cycle path") from error
318
+ nodes: list[ModuleKey] = []
319
+ for item in error.args[1]:
320
+ if not isinstance(item, ModuleKey):
321
+ raise GraphInvariantError(
322
+ "graphlib cycle contained a non-ModuleKey node"
323
+ ) from error
324
+ nodes.append(item)
325
+ if len(nodes) < 2 or nodes[0] != nodes[-1]:
326
+ raise GraphInvariantError("graphlib cycle path was not closed") from error
327
+ return tuple(nodes)
328
+
329
+ @staticmethod
330
+ def _follows_declarations(
331
+ path: tuple[ModuleKey, ...],
332
+ predecessors: Mapping[ModuleKey, tuple[ModuleKey, ...]],
333
+ ) -> bool:
334
+ return all(path[index + 1] in predecessors[path[index]] for index in range(len(path) - 1))
@@ -0,0 +1,100 @@
1
+ """单个 Application 的不可变 Module registry 与 graph snapshot。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Mapping
6
+ from dataclasses import dataclass
7
+ from types import MappingProxyType
8
+
9
+ from ..diagnostics import SourceLocation
10
+ from ..errors import UnknownModuleError
11
+ from .contracts import AppModule, ModuleDependencyEdge, ModuleKey
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class ModuleDescriptor:
16
+ key: ModuleKey
17
+ module_type: type[AppModule]
18
+ instance: AppModule
19
+ dependencies: tuple[ModuleKey, ...]
20
+ dependency_path: tuple[ModuleKey, ...]
21
+ dependency_edges: tuple[ModuleDependencyEdge, ...]
22
+ discovery_index: int
23
+ declaration_source: SourceLocation
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class ModuleGraphSnapshot:
28
+ startup: ModuleKey
29
+ dependencies: Mapping[ModuleKey, tuple[ModuleKey, ...]]
30
+ dependency_paths: Mapping[ModuleKey, tuple[ModuleKey, ...]]
31
+ dependency_edges: tuple[ModuleDependencyEdge, ...]
32
+ dependency_order: tuple[ModuleKey, ...]
33
+ shutdown_order: tuple[ModuleKey, ...]
34
+
35
+
36
+ class ModuleRegistry:
37
+ """当前 Application 的不可变 Module 索引和稳定生命周期顺序。"""
38
+
39
+ __slots__ = ("_dependency_order", "_descriptors", "_shutdown_order", "_snapshot", "_startup")
40
+
41
+ def __init__(
42
+ self,
43
+ *,
44
+ startup: ModuleDescriptor,
45
+ descriptors: Mapping[ModuleKey, ModuleDescriptor],
46
+ dependency_order: tuple[ModuleDescriptor, ...],
47
+ ) -> None:
48
+ descriptor_copy = dict(descriptors)
49
+ self._startup = startup
50
+ self._descriptors: Mapping[ModuleKey, ModuleDescriptor] = MappingProxyType(descriptor_copy)
51
+ self._dependency_order = dependency_order
52
+ self._shutdown_order = tuple(reversed(dependency_order))
53
+ dependency_map: Mapping[ModuleKey, tuple[ModuleKey, ...]] = MappingProxyType(
54
+ {descriptor.key: descriptor.dependencies for descriptor in dependency_order}
55
+ )
56
+ dependency_path_map: Mapping[ModuleKey, tuple[ModuleKey, ...]] = MappingProxyType(
57
+ {descriptor.key: descriptor.dependency_path for descriptor in dependency_order}
58
+ )
59
+ self._snapshot = ModuleGraphSnapshot(
60
+ startup=startup.key,
61
+ dependencies=dependency_map,
62
+ dependency_paths=dependency_path_map,
63
+ dependency_edges=tuple(
64
+ edge for descriptor in dependency_order for edge in descriptor.dependency_edges
65
+ ),
66
+ dependency_order=tuple(descriptor.key for descriptor in dependency_order),
67
+ shutdown_order=tuple(descriptor.key for descriptor in self._shutdown_order),
68
+ )
69
+
70
+ @property
71
+ def startup(self) -> ModuleDescriptor:
72
+ return self._startup
73
+
74
+ @property
75
+ def descriptors(self) -> Mapping[ModuleKey, ModuleDescriptor]:
76
+ return self._descriptors
77
+
78
+ @property
79
+ def dependency_order(self) -> tuple[ModuleDescriptor, ...]:
80
+ return self._dependency_order
81
+
82
+ @property
83
+ def shutdown_order(self) -> tuple[ModuleDescriptor, ...]:
84
+ return self._shutdown_order
85
+
86
+ @property
87
+ def snapshot(self) -> ModuleGraphSnapshot:
88
+ return self._snapshot
89
+
90
+ def __getitem__(self, key: ModuleKey) -> ModuleDescriptor:
91
+ try:
92
+ return self._descriptors[key]
93
+ except KeyError:
94
+ raise UnknownModuleError(key) from None
95
+
96
+ def __iter__(self) -> Iterator[ModuleKey]:
97
+ return iter(self._descriptors)
98
+
99
+ def __len__(self) -> int:
100
+ return len(self._descriptors)
@@ -0,0 +1,16 @@
1
+ """显式模块操作消费最终依赖图,不按包名前缀推断业务归属。"""
2
+
3
+ from .contracts import AppModule, ModuleKey
4
+ from .registry import ModuleRegistry
5
+
6
+
7
+ def _dependency_keys(registry: ModuleRegistry, module: type[AppModule]) -> frozenset[ModuleKey]:
8
+ key = ModuleKey(module.__module__, module.__qualname__)
9
+ pending = [registry[key].key]
10
+ selected: set[ModuleKey] = set()
11
+ while pending:
12
+ current = pending.pop()
13
+ if current not in selected:
14
+ selected.add(current)
15
+ pending.extend(registry[current].dependencies)
16
+ return frozenset(selected)