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,427 @@
1
+ """Configuration value canonicalization、deep immutability 与统一脱敏。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import keyword
6
+ from collections.abc import Callable, Mapping
7
+ from datetime import date, datetime, time, timedelta
8
+ from decimal import Decimal
9
+ from enum import Enum
10
+ from pathlib import Path
11
+ from types import MappingProxyType
12
+ from typing import ClassVar, Final, cast
13
+ from uuid import UUID
14
+
15
+ from pydantic import Secret, SecretBytes, SecretStr
16
+
17
+ MISSING: Final = object()
18
+ REDACTION_MARKER: Final = "********"
19
+ _SECRET_NAME_SUFFIXES: Final = (
20
+ "access_key",
21
+ "api_key",
22
+ "apikey",
23
+ "credential",
24
+ "credentials",
25
+ "password",
26
+ "passwd",
27
+ "private_key",
28
+ "secret",
29
+ "token",
30
+ )
31
+
32
+
33
+ class _ImmutableSecretMixin:
34
+ __slots__ = ()
35
+ _wrapped_secret_type: ClassVar[type[object]]
36
+
37
+ def __setattr__(self, name: str, value: object) -> None:
38
+ # Pydantic Secret 只保证显示脱敏,发布后还必须阻止改写其内部 payload。
39
+ if hasattr(self, "_secret_value"):
40
+ raise TypeError("published secret values are immutable")
41
+ super().__setattr__(name, value)
42
+
43
+ def __eq__(self, other: object) -> bool:
44
+ return (
45
+ isinstance(other, _ImmutableSecretMixin)
46
+ and self._wrapped_secret_type is other._wrapped_secret_type
47
+ and secret_payload(self) == secret_payload(other)
48
+ )
49
+
50
+ def __hash__(self) -> int:
51
+ return hash((self._wrapped_secret_type, secret_payload(self)))
52
+
53
+
54
+ class _FrozenSecretStr(_ImmutableSecretMixin, SecretStr):
55
+ __slots__ = ()
56
+ _wrapped_secret_type = SecretStr
57
+
58
+
59
+ class _FrozenSecretBytes(_ImmutableSecretMixin, SecretBytes):
60
+ __slots__ = ()
61
+ _wrapped_secret_type = SecretBytes
62
+
63
+
64
+ class _FrozenSecret(_ImmutableSecretMixin, Secret[object]):
65
+ __slots__ = ()
66
+ _wrapped_secret_type = Secret
67
+
68
+
69
+ def normalize_mapping(
70
+ values: Mapping[str, object],
71
+ *,
72
+ mapping_paths: frozenset[tuple[str | int | None, ...]] = frozenset(),
73
+ path: tuple[str | int | None, ...] = (),
74
+ pending: bool = False,
75
+ strict_paths: frozenset[tuple[str, ...]] = frozenset(),
76
+ merge_fields: bool = False,
77
+ ) -> dict[str, object]:
78
+ normalized: dict[str, object] = {}
79
+ pending_here = pending and not any(path[: len(prefix)] == prefix for prefix in strict_paths)
80
+ for raw_key, raw_value in values.items():
81
+ if not isinstance(raw_key, str):
82
+ raise TypeError("configuration mapping keys must be strings")
83
+ if path in mapping_paths:
84
+ key = raw_key
85
+ elif pending_here:
86
+ key = raw_key.casefold()
87
+ else:
88
+ key = canonical_segment(raw_key)
89
+ child_path = (*path, key)
90
+ child = normalize_value(
91
+ raw_value,
92
+ mapping_paths=mapping_paths,
93
+ path=child_path,
94
+ pending=pending,
95
+ strict_paths=strict_paths,
96
+ merge_fields=merge_fields,
97
+ )
98
+ if key in normalized and not pending_here:
99
+ _merge_source_fields(normalized, key, child, child_path, mapping_paths, merge_fields)
100
+ else:
101
+ normalized[key] = child
102
+ return normalized
103
+
104
+
105
+ def normalize_value(
106
+ value: object,
107
+ *,
108
+ mapping_paths: frozenset[tuple[str | int | None, ...]] = frozenset(),
109
+ path: tuple[str | int | None, ...] = (),
110
+ pending: bool = False,
111
+ strict_paths: frozenset[tuple[str, ...]] = frozenset(),
112
+ merge_fields: bool = False,
113
+ ) -> object:
114
+ if isinstance(value, Mapping):
115
+ return normalize_mapping(
116
+ cast(Mapping[str, object], value),
117
+ mapping_paths=mapping_paths,
118
+ path=path,
119
+ pending=pending,
120
+ strict_paths=strict_paths,
121
+ merge_fields=merge_fields,
122
+ )
123
+ if isinstance(value, (list, tuple)):
124
+ return tuple(
125
+ normalize_value(
126
+ item,
127
+ mapping_paths=mapping_paths,
128
+ path=(*path, index),
129
+ pending=pending,
130
+ strict_paths=strict_paths,
131
+ merge_fields=merge_fields,
132
+ )
133
+ for index, item in enumerate(value)
134
+ )
135
+ if isinstance(value, (set, frozenset)):
136
+ return frozenset(
137
+ normalize_value(
138
+ item,
139
+ mapping_paths=mapping_paths,
140
+ path=(*path, index),
141
+ pending=pending,
142
+ strict_paths=strict_paths,
143
+ merge_fields=merge_fields,
144
+ )
145
+ for index, item in enumerate(value)
146
+ )
147
+ if isinstance(value, bytearray):
148
+ return bytes(value)
149
+ if is_supported_scalar(value):
150
+ return value
151
+ raise TypeError(f"unsupported mutable or opaque configuration value {type(value).__name__}")
152
+
153
+
154
+ def _merge_source_fields(
155
+ target: dict[str, object],
156
+ key: str,
157
+ incoming: object,
158
+ path: tuple[str | int | None, ...],
159
+ mapping_paths: frozenset[tuple[str | int | None, ...]],
160
+ allowed: bool,
161
+ ) -> None:
162
+ current = target[key]
163
+ if not allowed or not isinstance(current, dict) or not isinstance(incoming, Mapping):
164
+ raise ValueError("duplicate canonical configuration field")
165
+ # 原生环境来源允许 JSON 对象加嵌套变量。合并字段片段,不折叠数据键身份。
166
+ for name, value in incoming.items():
167
+ if name in current and path not in mapping_paths:
168
+ _merge_source_fields(current, name, value, (*path, name), mapping_paths, allowed)
169
+ else:
170
+ current[name] = value
171
+
172
+
173
+ def _capture_input_mapping(values: Mapping[str, object], *, root: bool = True) -> dict[str, object]:
174
+ # 来源只读取一次。类型声明晚于来源读取,数据键必须在这份快照里保留原貌。
175
+ result: dict[str, object] = {}
176
+ for name, value in values.items():
177
+ if not isinstance(name, str):
178
+ raise TypeError("configuration mapping keys must be strings")
179
+ key = canonical_segment(name) if root else name
180
+ if key in result:
181
+ raise ValueError("duplicate canonical configuration field")
182
+ result[key] = _capture_input_value(value)
183
+ return result
184
+
185
+
186
+ def _capture_input_value(value: object) -> object:
187
+ if isinstance(value, Mapping):
188
+ return _capture_input_mapping(value, root=False)
189
+ if isinstance(value, (list, tuple)):
190
+ return tuple(_capture_input_value(item) for item in value)
191
+ if isinstance(value, (set, frozenset)):
192
+ return frozenset(_capture_input_value(item) for item in value)
193
+ if isinstance(value, bytearray):
194
+ return bytes(value)
195
+ if is_supported_scalar(value):
196
+ return value
197
+ raise TypeError("unsupported mutable or opaque configuration value")
198
+
199
+
200
+ def freeze_value(value: object) -> object:
201
+ if isinstance(value, (Secret, SecretStr, SecretBytes)):
202
+ return _freeze_secret_value(value)
203
+ if isinstance(value, Mapping):
204
+ return MappingProxyType({key: freeze_value(item) for key, item in value.items()})
205
+ if isinstance(value, tuple):
206
+ return tuple(freeze_value(item) for item in value)
207
+ if isinstance(value, frozenset):
208
+ return frozenset(freeze_value(item) for item in value)
209
+ if is_supported_scalar(value):
210
+ return value
211
+ raise TypeError(f"unsupported mutable or opaque configuration value {type(value).__name__}")
212
+
213
+
214
+ def _freeze_secret_value(value: object) -> object:
215
+ if isinstance(value, _ImmutableSecretMixin):
216
+ return value
217
+ if isinstance(value, SecretStr):
218
+ frozen_value: object = value.get_secret_value()
219
+ base_frozen_type: type[object] = _FrozenSecretStr
220
+ base_type: type[object] = SecretStr
221
+ elif isinstance(value, SecretBytes):
222
+ frozen_value = value.get_secret_value()
223
+ base_frozen_type = _FrozenSecretBytes
224
+ base_type = SecretBytes
225
+ elif isinstance(value, Secret):
226
+ frozen_value = freeze_value(value.get_secret_value())
227
+ base_frozen_type = _FrozenSecret
228
+ base_type = Secret
229
+ else:
230
+ raise TypeError(f"unsupported secret value {type(value).__name__}")
231
+
232
+ concrete_type = type(value)
233
+ frozen_type = (
234
+ base_frozen_type
235
+ if concrete_type is base_type
236
+ else type(
237
+ f"_Frozen{concrete_type.__name__}",
238
+ (_ImmutableSecretMixin, concrete_type),
239
+ {
240
+ "__slots__": (),
241
+ "__module__": __name__,
242
+ "_wrapped_secret_type": concrete_type,
243
+ },
244
+ )
245
+ )
246
+ factory = cast(Callable[[object], object], frozen_type)
247
+ return factory(frozen_value)
248
+
249
+
250
+ def is_frozen_secret(value: object) -> bool:
251
+ return isinstance(value, _ImmutableSecretMixin)
252
+
253
+
254
+ def secret_payload(value: object) -> object:
255
+ if isinstance(value, (Secret, SecretStr, SecretBytes)):
256
+ return value.get_secret_value()
257
+ raise TypeError(f"unsupported secret value {type(value).__name__}")
258
+
259
+
260
+ def redact_value(
261
+ value: object,
262
+ *,
263
+ path: tuple[str, ...],
264
+ secret_paths: frozenset[str],
265
+ ) -> tuple[object, bool]:
266
+ canonical = ".".join(path)
267
+ if (
268
+ canonical in secret_paths
269
+ or any(canonical.startswith(f"{secret_path}.") for secret_path in secret_paths)
270
+ or _looks_secret(path)
271
+ or isinstance(value, (Secret, SecretStr, SecretBytes))
272
+ ):
273
+ return REDACTION_MARKER, True
274
+ if isinstance(value, Mapping):
275
+ redacted = False
276
+ sanitized: dict[str, object] = {}
277
+ for key, item in value.items():
278
+ sanitized_item, item_redacted = redact_value(
279
+ item,
280
+ path=(*path, key),
281
+ secret_paths=secret_paths,
282
+ )
283
+ sanitized[key] = sanitized_item
284
+ redacted = redacted or item_redacted
285
+ return MappingProxyType(sanitized), redacted
286
+ if isinstance(value, tuple):
287
+ items: list[object] = []
288
+ redacted = False
289
+ for item in value:
290
+ sanitized_item, item_redacted = redact_value(
291
+ item,
292
+ path=path,
293
+ secret_paths=secret_paths,
294
+ )
295
+ items.append(sanitized_item)
296
+ redacted = redacted or item_redacted
297
+ return tuple(items), redacted
298
+ if isinstance(value, frozenset):
299
+ frozen_items: list[object] = []
300
+ redacted = False
301
+ for item in value:
302
+ sanitized_item, item_redacted = redact_value(
303
+ item,
304
+ path=path,
305
+ secret_paths=secret_paths,
306
+ )
307
+ frozen_items.append(sanitized_item)
308
+ redacted = redacted or item_redacted
309
+ return frozenset(frozen_items), redacted
310
+ return value, False
311
+
312
+
313
+ def _looks_secret(path: tuple[str, ...]) -> bool:
314
+ return any(_looks_secret_name(segment) for segment in path)
315
+
316
+
317
+ def _looks_secret_name(name: str) -> bool:
318
+ """Configuration/Settings 共用的 secret-shaped name 单一判定权威。"""
319
+
320
+ normalized = name.casefold().replace("-", "_")
321
+ return any(
322
+ segment.endswith(_SECRET_NAME_SUFFIXES) for segment in normalized.split(".") if segment
323
+ )
324
+
325
+
326
+ def is_supported_scalar(value: object) -> bool:
327
+ if isinstance(value, (Secret, SecretBytes, SecretStr)):
328
+ return True
329
+ if isinstance(value, Enum):
330
+ return _enum_member_is_deeply_immutable(value, seen=set())
331
+ return _is_intrinsically_immutable_scalar(value)
332
+
333
+
334
+ def _enum_member_is_deeply_immutable(value: Enum, *, seen: set[int]) -> bool:
335
+ identity = id(value)
336
+ if identity in seen:
337
+ return True
338
+ member_state = vars(value)
339
+ if any(
340
+ name not in {"_name_", "_sort_order_", "_value_", "__objclass__"} for name in member_state
341
+ ):
342
+ # Enum member 是跨 Application singleton;任何额外 instance state 都不可发布。
343
+ return False
344
+ return _enum_payload_is_deeply_immutable(value.value, seen={*seen, identity})
345
+
346
+
347
+ def _enum_payload_is_deeply_immutable(value: object, *, seen: set[int]) -> bool:
348
+ if isinstance(value, Enum):
349
+ return _enum_member_is_deeply_immutable(value, seen=seen)
350
+ if _is_intrinsically_immutable_scalar(value):
351
+ return True
352
+ identity = id(value)
353
+ if identity in seen:
354
+ return True
355
+ branch_seen = {*seen, identity}
356
+ if type(value) is tuple or _is_named_tuple_instance(value):
357
+ tuple_value = cast(tuple[object, ...], value)
358
+ return all(
359
+ _enum_payload_is_deeply_immutable(item, seen=branch_seen) for item in tuple_value
360
+ )
361
+ if isinstance(value, frozenset):
362
+ return all(_enum_payload_is_deeply_immutable(item, seen=branch_seen) for item in value)
363
+ # MappingProxyType 仍可能被持有原 dict 的代码改写。
364
+ return False
365
+
366
+
367
+ def _is_named_tuple_instance(value: object) -> bool:
368
+ fields = getattr(type(value), "_fields", None)
369
+ return (
370
+ isinstance(value, tuple)
371
+ and isinstance(fields, tuple)
372
+ and all(isinstance(field, str) for field in fields)
373
+ and not hasattr(value, "__dict__")
374
+ )
375
+
376
+
377
+ def _is_intrinsically_immutable_scalar(value: object) -> bool:
378
+ if type(value) in {
379
+ UUID,
380
+ date,
381
+ datetime,
382
+ Decimal,
383
+ time,
384
+ timedelta,
385
+ bool,
386
+ bytes,
387
+ float,
388
+ int,
389
+ str,
390
+ type(None),
391
+ }:
392
+ return True
393
+ return isinstance(value, Path) and type(value).__module__ == Path.__module__
394
+
395
+
396
+ def lookup_path(tree: Mapping[str, object], parts: tuple[str, ...]) -> object:
397
+ current: object = tree
398
+ for part in parts:
399
+ if not isinstance(current, Mapping):
400
+ raise TypeError("configuration path traversed a scalar value")
401
+ current = current[part]
402
+ return current
403
+
404
+
405
+ def canonical_path(path: str) -> tuple[str, ...]:
406
+ if not isinstance(path, str):
407
+ raise TypeError("configuration path must be a string")
408
+ return tuple(canonical_segment(part) for part in path.split("."))
409
+
410
+
411
+ def canonical_segment(segment: object) -> str:
412
+ if not isinstance(segment, str):
413
+ raise TypeError("configuration key must be a string")
414
+ if not segment or segment != segment.strip() or "." in segment:
415
+ raise ValueError("configuration key segments must be non-empty and must not contain dots")
416
+ canonical = segment.casefold()
417
+ if not canonical.isidentifier() or keyword.iskeyword(canonical):
418
+ raise ValueError(f"configuration key {segment!r} is not a valid identifier segment")
419
+ return canonical
420
+
421
+
422
+ def raw_kind(value: object) -> str:
423
+ if isinstance(value, Mapping):
424
+ return "mapping"
425
+ if isinstance(value, (tuple, frozenset)):
426
+ return "sequence"
427
+ return type(value).__name__
@@ -0,0 +1,51 @@
1
+ """原生 YAML source 的安全结构边界;语法与标签仍由 PyYAML SafeLoader 解释。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import import_module
6
+ from pathlib import Path
7
+ from typing import Any, cast
8
+
9
+ from pydantic_settings import YamlConfigSettingsSource
10
+ from pydantic_settings.sources.types import Traversable
11
+
12
+ # PyYAML 未发布 typing;动态类型只留在原生 loader 边界,不向 Kernel 扩散。
13
+ yaml = import_module("yaml")
14
+
15
+
16
+ class _UniqueKeySafeLoader(yaml.SafeLoader): # type: ignore[misc, name-defined]
17
+ def construct_mapping(self, node: Any, deep: bool = False) -> dict[Any, Any]:
18
+ # 在原生 merge-key 展开前检查显式重复键;合法 anchor merge 仍由原生规则处理。
19
+ keys: set[object] = set()
20
+ for key_node, _ in node.value:
21
+ if key_node.tag == "tag:yaml.org,2002:merge":
22
+ continue
23
+ key = self.construct_object(key_node, deep=deep)
24
+ if key in keys:
25
+ raise ValueError("duplicate YAML mapping key")
26
+ keys.add(key)
27
+ return cast(dict[Any, Any], super().construct_mapping(node, deep=deep))
28
+
29
+
30
+ class _YamlConfigurationSource(YamlConfigSettingsSource):
31
+ def _read_file(self, file_path: Path | Traversable) -> dict[str, Any]:
32
+ with file_path.open(encoding="utf-8") as stream:
33
+ value = yaml.load(stream, Loader=_UniqueKeySafeLoader)
34
+ if value is None:
35
+ return {}
36
+ if not isinstance(value, dict):
37
+ raise ValueError("YAML configuration must be a mapping")
38
+ _reject_cycles(value, set())
39
+ return value
40
+
41
+
42
+ def _reject_cycles(value: object, active: set[int]) -> None:
43
+ if not isinstance(value, (dict, list)):
44
+ return
45
+ identity = id(value)
46
+ if identity in active:
47
+ raise ValueError("cyclic YAML configuration")
48
+ active.add(identity)
49
+ for item in value.values() if isinstance(value, dict) else value:
50
+ _reject_cycles(item, active)
51
+ active.remove(identity)
@@ -0,0 +1 @@
1
+ """随框架版本发布的开发工具;运行命令不依赖本包。"""
@@ -0,0 +1,182 @@
1
+ """包内模板 staging 校验和发布;不覆盖文件,不移动已安装环境。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import importlib.metadata as metadata
7
+ import importlib.resources as resources
8
+ import inspect
9
+ import keyword
10
+ import os
11
+ import platform
12
+ import re
13
+ import secrets
14
+ import shutil
15
+ import subprocess
16
+ import tempfile
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ # 官方库未发布 typing marker;仅在第三方导入边界忽略缺失的类型信息。
21
+ from cookiecutter.main import cookiecutter # type: ignore[import-untyped]
22
+
23
+ from ..application import Application
24
+ from ..cli import ENVIRONMENT_VARIABLE
25
+ from ..cli.errors import CommandError
26
+ from ..cli.project import Project, read_project
27
+ from .project_metadata import add_module_metadata
28
+ from .source import framework_source
29
+ from .wiring import add_dependency
30
+
31
+
32
+ def _name(value: str) -> tuple[str, str]:
33
+ if not re.fullmatch(r"[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*", value) or re.fullmatch(
34
+ r"con|prn|aux|nul|com[1-9]|lpt[1-9]", value
35
+ ):
36
+ raise CommandError("Use a lowercase project/module name valid on Windows and Linux")
37
+ package = value.replace("-", "_")
38
+ if keyword.iskeyword(package):
39
+ raise CommandError("Project/module package name must not be a Python keyword")
40
+ return package, "".join(part.capitalize() for part in package.split("_"))
41
+
42
+
43
+ def _render(template: str, context: dict[str, Any]) -> Path:
44
+ staging = Path(tempfile.mkdtemp(prefix="python-platform-generation-"))
45
+ root = resources.files(__package__).joinpath("templates").joinpath(template)
46
+ with resources.as_file(root) as template_path:
47
+ # Cookiecutter 用相对路径加载;安装目录较深时先复制到短 staging,避免 Windows MAX_PATH。
48
+ source = str(template_path.resolve())
49
+ if os.name == "nt" and not source.startswith("\\\\?\\"):
50
+ source = (
51
+ "\\\\?\\UNC\\" + source[2:] if source.startswith("\\\\") else "\\\\?\\" + source
52
+ )
53
+ local_template = staging / "template"
54
+ shutil.copytree(source, local_template)
55
+ rendered = Path(
56
+ cookiecutter(
57
+ str(local_template),
58
+ no_input=True,
59
+ extra_context=context,
60
+ output_dir=str(staging / "output"),
61
+ default_config=True,
62
+ accept_hooks=False,
63
+ keep_project_on_failure=True,
64
+ )
65
+ )
66
+ for path in tuple(rendered.rglob("*.jinja")):
67
+ path.rename(path.with_suffix(""))
68
+ _validate(rendered)
69
+ return rendered
70
+
71
+
72
+ def _validate(root: Path) -> None:
73
+ for path in root.rglob("*"):
74
+ if path.is_symlink():
75
+ raise CommandError("Generated output must not contain symbolic links")
76
+ if path.suffix == ".py":
77
+ ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
78
+ elif path.suffix == ".toml":
79
+ read_project(path)
80
+
81
+
82
+ def new_project(name: str, wheel: Path | None, dry_run: bool) -> None:
83
+ package, class_prefix = _name(name)
84
+ destination = Path.cwd() / name
85
+ if destination.exists():
86
+ raise CommandError(f"Destination already exists: {destination}")
87
+ source = framework_source(wheel)
88
+ if dry_run:
89
+ print(f"Would generate {destination} with bundled Host template")
90
+ return
91
+ distribution = metadata.distribution("python-platform")
92
+ generator = (distribution.read_text("WHEEL") or "").split("Generator: uv ")[-1].splitlines()[0]
93
+ if not re.fullmatch(r"\d+\.\d+\.\d+", generator):
94
+ raise CommandError("Cannot determine the framework wheel's uv build tool version")
95
+ staged = _render(
96
+ "project",
97
+ {
98
+ "project_name": name,
99
+ "package_name": package,
100
+ "class_prefix": class_prefix,
101
+ "framework_version": source.version,
102
+ "framework_source": source.uv_source,
103
+ "python_version": platform.python_version(),
104
+ "build_version": generator,
105
+ "host_environment_variable": ENVIRONMENT_VARIABLE,
106
+ "database_password": secrets.token_urlsafe(24),
107
+ "admin_password": secrets.token_urlsafe(24),
108
+ "jwt_key": secrets.token_urlsafe(48),
109
+ },
110
+ )
111
+ if source.wheel is not None:
112
+ vendor = staged / "vendor"
113
+ vendor.mkdir()
114
+ shutil.copy2(source.wheel, vendor / source.wheel.name)
115
+ # copytree 原生拒绝已存在目录;staging 跨 Windows 盘符,不能依赖目录 rename。
116
+ shutil.copytree(staged, destination)
117
+ print(f"Generated {destination}; staging retained at {staged}")
118
+ subprocess.run(["uv", "sync"], cwd=destination, check=True)
119
+
120
+
121
+ def add_module(name: str, dry_run: bool) -> None:
122
+ package, class_prefix = _name(name)
123
+ project = Project.current()
124
+ application = project.host("application").load()(environment="development")
125
+ if not isinstance(application, Application):
126
+ raise CommandError("The application entry point must return Application")
127
+ startup = application.modules.startup.module_type
128
+ if startup.__qualname__ != startup.__name__:
129
+ raise CommandError("Startup Module must be a top-level class for module generation")
130
+ source_path = Path(inspect.getfile(startup)).resolve()
131
+ if not source_path.is_relative_to(project.root / "src"):
132
+ raise CommandError(
133
+ "Startup Module source must belong to the current project's src directory"
134
+ )
135
+ modules = project.root / "src" / "modules"
136
+ destination = modules / package
137
+ if destination.exists():
138
+ raise CommandError(f"Module destination already exists: {destination}")
139
+ source = source_path.read_text(encoding="utf-8")
140
+ updated = source
141
+ for layer, suffix in (
142
+ ("application", "ApplicationModule"),
143
+ ("sqlalchemy", "SqlAlchemyModule"),
144
+ ("http_api", "HttpApiModule"),
145
+ ):
146
+ updated = add_dependency(
147
+ updated, startup.__name__, f"modules.{package}.{layer}.module", class_prefix + suffix
148
+ )
149
+ metadata_path = project.root / "pyproject.toml"
150
+ metadata_source = metadata_path.read_text(encoding="utf-8")
151
+ updated_metadata = add_module_metadata(metadata_source, package, class_prefix)
152
+ ast.parse(updated)
153
+ if dry_run:
154
+ print(
155
+ f"Would add {destination}, wire modules into {source_path} and update {metadata_path}"
156
+ )
157
+ return
158
+ staged = _render(
159
+ "module",
160
+ {
161
+ "module_name": package,
162
+ "class_prefix": class_prefix,
163
+ },
164
+ )
165
+ # 先验证全部新文件和接线;已有 startup 文件仅在内容仍等于读取快照时替换。
166
+ if (
167
+ source_path.read_text(encoding="utf-8") != source
168
+ or metadata_path.read_text(encoding="utf-8") != metadata_source
169
+ ):
170
+ raise CommandError("Startup source changed during generation; retry after reviewing it")
171
+ modules.mkdir(exist_ok=True)
172
+ if not modules.joinpath("__init__.py").exists():
173
+ modules.joinpath("__init__.py").touch()
174
+ shutil.copytree(staged, destination)
175
+ with tempfile.NamedTemporaryFile(
176
+ mode="w", encoding="utf-8", dir=source_path.parent, delete=False
177
+ ) as pending:
178
+ pending.write(updated)
179
+ Path(pending.name).replace(source_path)
180
+ metadata_path.write_text(updated_metadata, encoding="utf-8")
181
+ subprocess.run(["uv", "sync"], cwd=project.root, check=True)
182
+ print(f"Added {package}; staging retained at {staged}")