dinkster 0.0.2__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 (286) hide show
  1. dinkster/__init__.py +6 -0
  2. dinkster/__main__.py +3 -0
  3. dinkster/activation.py +380 -0
  4. dinkster/benchmark.py +531 -0
  5. dinkster/checkout-packs.lock +1 -0
  6. dinkster/cli.py +125 -0
  7. dinkster/comfy_compose.py +745 -0
  8. dinkster/compose.py +8007 -0
  9. dinkster/demo.py +284 -0
  10. dinkster/extension_assets.py +45 -0
  11. dinkster/frontend.py +159 -0
  12. dinkster/frontend_bundle/manifest.json +6 -0
  13. dinkster/frontend_bundle/static/assets/GLTFLoader-Bz7v_R8g.js +1 -0
  14. dinkster/frontend_bundle/static/assets/OrbitControls-Bm0pTgiB.js +1 -0
  15. dinkster/frontend_bundle/static/assets/index-DFieIngV.js +143 -0
  16. dinkster/frontend_bundle/static/assets/index-DoL_bCWG.css +1 -0
  17. dinkster/frontend_bundle/static/assets/model3d-viewer-CcNPJ4Fq.js +2 -0
  18. dinkster/frontend_bundle/static/assets/shared-worker-entry-CH6pyGn3.js +2 -0
  19. dinkster/frontend_bundle/static/assets/spark.module-DFJBdDqJ.js +12120 -0
  20. dinkster/frontend_bundle/static/assets/three.module-Y-ql4QRg.js +4116 -0
  21. dinkster/frontend_bundle/static/assets/workspace-worker-entry-BN5IbNTT.js +2 -0
  22. dinkster/frontend_bundle/static/branding/platform/dinkster.icns +0 -0
  23. dinkster/frontend_bundle/static/branding/platform/dinkster.ico +0 -0
  24. dinkster/frontend_bundle/static/branding/raster/color/1024.png +0 -0
  25. dinkster/frontend_bundle/static/branding/raster/color/128.png +0 -0
  26. dinkster/frontend_bundle/static/branding/raster/color/16.png +0 -0
  27. dinkster/frontend_bundle/static/branding/raster/color/24.png +0 -0
  28. dinkster/frontend_bundle/static/branding/raster/color/256.png +0 -0
  29. dinkster/frontend_bundle/static/branding/raster/color/32.png +0 -0
  30. dinkster/frontend_bundle/static/branding/raster/color/48.png +0 -0
  31. dinkster/frontend_bundle/static/branding/raster/color/512.png +0 -0
  32. dinkster/frontend_bundle/static/branding/raster/color/64.png +0 -0
  33. dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-color.svg +109 -0
  34. dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-cyan.svg +87 -0
  35. dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-dark.svg +87 -0
  36. dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-light.svg +87 -0
  37. dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-mono.svg +87 -0
  38. dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-white.svg +87 -0
  39. dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-color-dark.svg +157 -0
  40. dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-color-light.svg +157 -0
  41. dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-dark.svg +125 -0
  42. dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-heavier-dark.svg +147 -0
  43. dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-heavier-light.svg +147 -0
  44. dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-light.svg +125 -0
  45. dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-mono.svg +125 -0
  46. dinkster/frontend_bundle/static/fonts/LICENSE +155 -0
  47. dinkster/frontend_bundle/static/fonts/NotoSans-Bold.woff2 +0 -0
  48. dinkster/frontend_bundle/static/fonts/NotoSans-Italic.woff2 +0 -0
  49. dinkster/frontend_bundle/static/fonts/NotoSans-Regular.woff2 +0 -0
  50. dinkster/frontend_bundle/static/fonts/NotoSansMono-Regular.woff2 +0 -0
  51. dinkster/frontend_bundle/static/index.html +167 -0
  52. dinkster/frontend_bundle/static/manifest.webmanifest +1 -0
  53. dinkster/generation_api.py +1725 -0
  54. dinkster/generation_packs.py +180 -0
  55. dinkster/guess_api.py +324 -0
  56. dinkster/installer.py +1548 -0
  57. dinkster/isolated_demo.py +183 -0
  58. dinkster/launch.py +90 -0
  59. dinkster/lazy_worker.py +216 -0
  60. dinkster/manager.py +1427 -0
  61. dinkster/mounts_api.py +786 -0
  62. dinkster/native_policy.py +71 -0
  63. dinkster/official_packs/dinkster-nodes-comfy-0.0.2.zip +0 -0
  64. dinkster/official_packs/dinkster-nodes-foundation-0.0.2.zip +0 -0
  65. dinkster/official_packs/dinkster-nodes-image-0.0.2.zip +0 -0
  66. dinkster/official_packs/dinkster-nodes-media-io-0.0.2.zip +0 -0
  67. dinkster/official_packs/dinkster-nodes-remote-0.0.2.zip +0 -0
  68. dinkster/official_packs/dinkster-vision-birefnet-0.0.2.zip +0 -0
  69. dinkster/official_packs/dinkster-vision-depth-anything-v2-0.0.2.zip +0 -0
  70. dinkster/official_packs/dinkster-vision-depth-anything-v3-0.0.2.zip +0 -0
  71. dinkster/official_packs/dinkster-vision-detr-0.0.2.zip +0 -0
  72. dinkster/official_packs/dinkster-vision-efficient-sam-0.0.2.zip +0 -0
  73. dinkster/official_packs/dinkster-vision-hed-0.0.2.zip +0 -0
  74. dinkster/official_packs/dinkster-vision-rtdetr-0.0.2.zip +0 -0
  75. dinkster/official_packs/dinkster-vision-sam31-0.0.2.zip +0 -0
  76. dinkster/official_packs/dinkster-vision-upscale-0.0.2.zip +0 -0
  77. dinkster/official_packs/official-packs.lock +1 -0
  78. dinkster/pack_archive.py +135 -0
  79. dinkster/packs.py +199 -0
  80. dinkster/port.py +1105 -0
  81. dinkster/project_folders.py +180 -0
  82. dinkster/project_requirements.py +318 -0
  83. dinkster/registries.py +700 -0
  84. dinkster/reload_api.py +243 -0
  85. dinkster/remote_reconnect.py +218 -0
  86. dinkster/remotes.py +196 -0
  87. dinkster/resolver_api.py +135 -0
  88. dinkster/serve.py +2556 -0
  89. dinkster/setup.py +79 -0
  90. dinkster/storelock.py +89 -0
  91. dinkster/watch.py +159 -0
  92. dinkster/workflow_requirements.py +353 -0
  93. dinkster/workflow_requirements_api.py +149 -0
  94. dinkster-0.0.2.dist-info/METADATA +30 -0
  95. dinkster-0.0.2.dist-info/RECORD +286 -0
  96. dinkster-0.0.2.dist-info/WHEEL +4 -0
  97. dinkster-0.0.2.dist-info/entry_points.txt +6 -0
  98. dinkster-0.0.2.dist-info/licenses/LICENSE +674 -0
  99. dinkster_collab/__init__.py +49 -0
  100. dinkster_collab/routes.py +922 -0
  101. dinkster_collab/sessions.py +442 -0
  102. dinkster_collab/store.py +273 -0
  103. dinkster_compat_comfy/__init__.py +93 -0
  104. dinkster_compat_comfy/adapters.py +890 -0
  105. dinkster_compat_comfy/audio.py +12 -0
  106. dinkster_compat_comfy/bootstrap.py +389 -0
  107. dinkster_compat_comfy/comfy_execution.py +150 -0
  108. dinkster_compat_comfy/core_schemas.json +72248 -0
  109. dinkster_compat_comfy/devices.py +12 -0
  110. dinkster_compat_comfy/entry.py +85 -0
  111. dinkster_compat_comfy/image.py +12 -0
  112. dinkster_compat_comfy/latent.py +12 -0
  113. dinkster_compat_comfy/legacy.py +480 -0
  114. dinkster_compat_comfy/legacy_entry.py +67 -0
  115. dinkster_compat_comfy/legacy_sources.py +12 -0
  116. dinkster_compat_comfy/listmap.py +150 -0
  117. dinkster_compat_comfy/memory.py +12 -0
  118. dinkster_compat_comfy/native.py +12 -0
  119. dinkster_compat_comfy/pool.py +16 -0
  120. dinkster_compat_comfy/port_probe.py +247 -0
  121. dinkster_compat_comfy/prompt.py +526 -0
  122. dinkster_compat_comfy/py.typed +0 -0
  123. dinkster_compat_comfy/resident.py +12 -0
  124. dinkster_compat_comfy/saved_results.py +59 -0
  125. dinkster_compat_comfy/schema_snapshot.py +11 -0
  126. dinkster_compat_comfy/source_staging.py +565 -0
  127. dinkster_compat_comfy/translate.py +3392 -0
  128. dinkster_compat_comfy/translate_v3.py +751 -0
  129. dinkster_compat_comfy/video.py +12 -0
  130. dinkster_compat_comfy_pack/dinkster-pack.toml +165 -0
  131. dinkster_compat_comfy_pack/dinkster_compat_comfy/__init__.py +93 -0
  132. dinkster_compat_comfy_pack/dinkster_compat_comfy/adapters.py +890 -0
  133. dinkster_compat_comfy_pack/dinkster_compat_comfy/audio.py +12 -0
  134. dinkster_compat_comfy_pack/dinkster_compat_comfy/bootstrap.py +389 -0
  135. dinkster_compat_comfy_pack/dinkster_compat_comfy/comfy_execution.py +150 -0
  136. dinkster_compat_comfy_pack/dinkster_compat_comfy/core_schemas.json +72248 -0
  137. dinkster_compat_comfy_pack/dinkster_compat_comfy/devices.py +12 -0
  138. dinkster_compat_comfy_pack/dinkster_compat_comfy/entry.py +85 -0
  139. dinkster_compat_comfy_pack/dinkster_compat_comfy/image.py +12 -0
  140. dinkster_compat_comfy_pack/dinkster_compat_comfy/latent.py +12 -0
  141. dinkster_compat_comfy_pack/dinkster_compat_comfy/legacy.py +480 -0
  142. dinkster_compat_comfy_pack/dinkster_compat_comfy/legacy_entry.py +67 -0
  143. dinkster_compat_comfy_pack/dinkster_compat_comfy/legacy_sources.py +12 -0
  144. dinkster_compat_comfy_pack/dinkster_compat_comfy/listmap.py +150 -0
  145. dinkster_compat_comfy_pack/dinkster_compat_comfy/memory.py +12 -0
  146. dinkster_compat_comfy_pack/dinkster_compat_comfy/native.py +12 -0
  147. dinkster_compat_comfy_pack/dinkster_compat_comfy/pool.py +16 -0
  148. dinkster_compat_comfy_pack/dinkster_compat_comfy/port_probe.py +247 -0
  149. dinkster_compat_comfy_pack/dinkster_compat_comfy/prompt.py +526 -0
  150. dinkster_compat_comfy_pack/dinkster_compat_comfy/py.typed +0 -0
  151. dinkster_compat_comfy_pack/dinkster_compat_comfy/resident.py +12 -0
  152. dinkster_compat_comfy_pack/dinkster_compat_comfy/saved_results.py +59 -0
  153. dinkster_compat_comfy_pack/dinkster_compat_comfy/schema_snapshot.py +11 -0
  154. dinkster_compat_comfy_pack/dinkster_compat_comfy/source_staging.py +565 -0
  155. dinkster_compat_comfy_pack/dinkster_compat_comfy/translate.py +3392 -0
  156. dinkster_compat_comfy_pack/dinkster_compat_comfy/translate_v3.py +751 -0
  157. dinkster_compat_comfy_pack/dinkster_compat_comfy/video.py +12 -0
  158. dinkster_engine/__init__.py +75 -0
  159. dinkster_engine/compile.py +871 -0
  160. dinkster_engine/control.py +108 -0
  161. dinkster_engine/engine.py +3329 -0
  162. dinkster_engine/events.py +38 -0
  163. dinkster_engine/expansion.py +231 -0
  164. dinkster_engine/py.typed +0 -0
  165. dinkster_graph/__init__.py +61 -0
  166. dinkster_graph/lower.py +126 -0
  167. dinkster_graph/migrate.py +70 -0
  168. dinkster_graph/model.py +223 -0
  169. dinkster_graph/plan.py +113 -0
  170. dinkster_graph/py.typed +0 -0
  171. dinkster_graph/validate.py +1373 -0
  172. dinkster_graph/wire.py +356 -0
  173. dinkster_native/__init__.py +10 -0
  174. dinkster_native/attention.py +399 -0
  175. dinkster_native/audio.py +57 -0
  176. dinkster_native/devices.py +244 -0
  177. dinkster_native/entry.py +13 -0
  178. dinkster_native/fork_nodes.py +423 -0
  179. dinkster_native/image.py +90 -0
  180. dinkster_native/latent.py +52 -0
  181. dinkster_native/legacy_sources.py +328 -0
  182. dinkster_native/memory.py +134 -0
  183. dinkster_native/model3d/__init__.py +1 -0
  184. dinkster_native/model3d/image_crop.py +127 -0
  185. dinkster_native/model3d/image_preprocess.py +37 -0
  186. dinkster_native/model3d/mesh.py +333 -0
  187. dinkster_native/model3d/mesh_operations.py +2861 -0
  188. dinkster_native/model3d/mesh_ops/__init__.py +1 -0
  189. dinkster_native/model3d/mesh_ops/qem_decimate.py +1816 -0
  190. dinkster_native/model3d/mesh_ops/remesh.py +1373 -0
  191. dinkster_native/model3d/mesh_ops/uv_unwrap/__init__.py +1 -0
  192. dinkster_native/model3d/mesh_ops/uv_unwrap/mesh.py +166 -0
  193. dinkster_native/model3d/mesh_ops/uv_unwrap/pack.py +1006 -0
  194. dinkster_native/model3d/mesh_ops/uv_unwrap/parameterize.py +596 -0
  195. dinkster_native/model3d/mesh_ops/uv_unwrap/segment.py +472 -0
  196. dinkster_native/model3d/moge_geometry.py +170 -0
  197. dinkster_native/multigpu.py +139 -0
  198. dinkster_native/native.py +163 -0
  199. dinkster_native/nodes_model3d.py +382 -0
  200. dinkster_native/pool.py +130 -0
  201. dinkster_native/py.typed +1 -0
  202. dinkster_native/resident.py +50 -0
  203. dinkster_native/type_ids.py +5 -0
  204. dinkster_native/video.py +231 -0
  205. dinkster_native/workgroup.py +180 -0
  206. dinkster_native_pack/dinkster-pack.toml +92 -0
  207. dinkster_native_pack/dinkster_native/__init__.py +10 -0
  208. dinkster_native_pack/dinkster_native/attention.py +399 -0
  209. dinkster_native_pack/dinkster_native/audio.py +57 -0
  210. dinkster_native_pack/dinkster_native/devices.py +244 -0
  211. dinkster_native_pack/dinkster_native/entry.py +13 -0
  212. dinkster_native_pack/dinkster_native/fork_nodes.py +423 -0
  213. dinkster_native_pack/dinkster_native/image.py +90 -0
  214. dinkster_native_pack/dinkster_native/latent.py +52 -0
  215. dinkster_native_pack/dinkster_native/legacy_sources.py +328 -0
  216. dinkster_native_pack/dinkster_native/memory.py +134 -0
  217. dinkster_native_pack/dinkster_native/model3d/__init__.py +1 -0
  218. dinkster_native_pack/dinkster_native/model3d/image_crop.py +127 -0
  219. dinkster_native_pack/dinkster_native/model3d/image_preprocess.py +37 -0
  220. dinkster_native_pack/dinkster_native/model3d/mesh.py +333 -0
  221. dinkster_native_pack/dinkster_native/model3d/mesh_operations.py +2861 -0
  222. dinkster_native_pack/dinkster_native/model3d/mesh_ops/__init__.py +1 -0
  223. dinkster_native_pack/dinkster_native/model3d/mesh_ops/qem_decimate.py +1816 -0
  224. dinkster_native_pack/dinkster_native/model3d/mesh_ops/remesh.py +1373 -0
  225. dinkster_native_pack/dinkster_native/model3d/mesh_ops/uv_unwrap/__init__.py +1 -0
  226. dinkster_native_pack/dinkster_native/model3d/mesh_ops/uv_unwrap/mesh.py +166 -0
  227. dinkster_native_pack/dinkster_native/model3d/mesh_ops/uv_unwrap/pack.py +1006 -0
  228. dinkster_native_pack/dinkster_native/model3d/mesh_ops/uv_unwrap/parameterize.py +596 -0
  229. dinkster_native_pack/dinkster_native/model3d/mesh_ops/uv_unwrap/segment.py +472 -0
  230. dinkster_native_pack/dinkster_native/model3d/moge_geometry.py +170 -0
  231. dinkster_native_pack/dinkster_native/multigpu.py +139 -0
  232. dinkster_native_pack/dinkster_native/native.py +163 -0
  233. dinkster_native_pack/dinkster_native/nodes_model3d.py +382 -0
  234. dinkster_native_pack/dinkster_native/pool.py +130 -0
  235. dinkster_native_pack/dinkster_native/py.typed +1 -0
  236. dinkster_native_pack/dinkster_native/resident.py +50 -0
  237. dinkster_native_pack/dinkster_native/type_ids.py +5 -0
  238. dinkster_native_pack/dinkster_native/video.py +231 -0
  239. dinkster_native_pack/dinkster_native/workgroup.py +180 -0
  240. dinkster_nodes_generation/__init__.py +30 -0
  241. dinkster_nodes_generation/model3d.py +550 -0
  242. dinkster_nodes_generation/nodes.py +6120 -0
  243. dinkster_nodes_generation/prompt_enhance.py +105 -0
  244. dinkster_nodes_generation/py.typed +1 -0
  245. dinkster_nodes_generation/templates/minimax-h3.json +383 -0
  246. dinkster_nodes_generation/templates/minimax-h3.png +0 -0
  247. dinkster_nodes_generation/templates/sd15.json +231 -0
  248. dinkster_nodes_generation/templates/sd15.png +0 -0
  249. dinkster_nodes_generation/trellis2.py +248 -0
  250. dinkster_nodes_generation_pack/dinkster-pack.toml +40 -0
  251. dinkster_nodes_generation_pack/dinkster_nodes_generation/__init__.py +30 -0
  252. dinkster_nodes_generation_pack/dinkster_nodes_generation/model3d.py +550 -0
  253. dinkster_nodes_generation_pack/dinkster_nodes_generation/nodes.py +6120 -0
  254. dinkster_nodes_generation_pack/dinkster_nodes_generation/prompt_enhance.py +105 -0
  255. dinkster_nodes_generation_pack/dinkster_nodes_generation/py.typed +1 -0
  256. dinkster_nodes_generation_pack/dinkster_nodes_generation/templates/minimax-h3.json +383 -0
  257. dinkster_nodes_generation_pack/dinkster_nodes_generation/templates/minimax-h3.png +0 -0
  258. dinkster_nodes_generation_pack/dinkster_nodes_generation/templates/sd15.json +231 -0
  259. dinkster_nodes_generation_pack/dinkster_nodes_generation/templates/sd15.png +0 -0
  260. dinkster_nodes_generation_pack/dinkster_nodes_generation/trellis2.py +248 -0
  261. dinkster_server/__init__.py +186 -0
  262. dinkster_server/_federated_asset_catalog.py +797 -0
  263. dinkster_server/app.py +4794 -0
  264. dinkster_server/asset_stream.py +93 -0
  265. dinkster_server/auth.py +1073 -0
  266. dinkster_server/cache_client.py +146 -0
  267. dinkster_server/discovery.py +160 -0
  268. dinkster_server/events.py +281 -0
  269. dinkster_server/execution_journal.py +448 -0
  270. dinkster_server/federated_assets_v1.py +1407 -0
  271. dinkster_server/history.py +672 -0
  272. dinkster_server/image_document.py +472 -0
  273. dinkster_server/journal.py +355 -0
  274. dinkster_server/library.py +1673 -0
  275. dinkster_server/network_cost.py +193 -0
  276. dinkster_server/pack_settings.py +57 -0
  277. dinkster_server/pack_surfaces.py +128 -0
  278. dinkster_server/paging.py +52 -0
  279. dinkster_server/peers.py +308 -0
  280. dinkster_server/preflight.py +353 -0
  281. dinkster_server/py.typed +0 -0
  282. dinkster_server/queue.py +635 -0
  283. dinkster_server/redaction.py +132 -0
  284. dinkster_server/serve.py +127 -0
  285. dinkster_server/settings.py +512 -0
  286. dinkster_server/training_sessions.py +1188 -0
dinkster/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Dinkster umbrella development package.
2
+
3
+ Pack authors should use the stable, frozen extension surface in ``dinkster_api.v1``.
4
+ Other umbrella-package modules remain pre-release."""
5
+
6
+ __version__ = "0.0.2"
dinkster/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
dinkster/activation.py ADDED
@@ -0,0 +1,380 @@
1
+ """Live pack activation: reconcile the served surface with the install
2
+ root's current generation (DESIGN 3.9's production follow-up).
3
+
4
+ ``dinkster-pack`` mutates the installation with plan/apply discipline, but a
5
+ running ``dinkster-serve --install-root`` keeps serving the generation it
6
+ composed at startup. This module closes that gap with whole-generation
7
+ staging and one publication:
8
+
9
+ - ``GET /api/install/activation`` computes the reconciliation PLAN
10
+ without touching anything - which managed packs a live activation
11
+ would add, remove, reload, or leave alone. Plan/apply discipline on
12
+ the wire: the read side shows exactly what the write side would do.
13
+ - ``POST /api/install/activation`` starts and validates every worker in a
14
+ private composer. Only a complete valid generation is published. Any
15
+ missing, incompatible, or failed pack closes the staged workers and leaves
16
+ the previous surface, worker topology, snapshot, and epoch intact.
17
+
18
+ The POST body may pin ``{"generation": N}`` - the number the GET plan
19
+ reported. If ``dinkster-pack apply`` lands a NEWER generation between plan
20
+ and apply, the pinned request gets 409 ``generation-changed`` instead of
21
+ silently reconciling to a surface the caller never previewed. Omitting
22
+ the pin means "converge to whatever is current" - the delivery-hook
23
+ flow, where the confirmation already happened at ``dinkster-pack apply``
24
+ time and re-planning is exactly the point.
25
+
26
+ Registered whenever an install root serves - NOT dev-gated like the
27
+ reload/removal endpoints, because activation takes no input and can only
28
+ move the surface toward what ``dinkster-pack``'s own confirmed plan/apply
29
+ already put on disk. The mutation was authorized at apply time;
30
+ activation is its delivery.
31
+
32
+ Managed versus dev: a composed pack is MANAGED exactly when its spec's
33
+ manifest lives under the install root's content-addressed store.
34
+ Explicit ``--pack`` dev additions and the ComfyUI compat workers are
35
+ invisible to activation - it never touches them. Store directories are
36
+ digest-addressed, so spec equality is content identity: same path means
37
+ same bytes, and a version bump means a new store directory, classified
38
+ as a reload with the new spec.
39
+
40
+ Diagnostics are deterministic and ordered by pack name. Activations serialize;
41
+ a concurrent request gets 409 rather than interleaving two reconciliations.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import asyncio
47
+ import json
48
+ from dataclasses import dataclass
49
+ from pathlib import Path
50
+
51
+ from aiohttp import web
52
+ from dinkster_schema import core_logger
53
+ from dinkster_server import STATE_KEY, ServerState
54
+
55
+ from .compose import CompositionError, PackSpec, ServingComposer, resolve_manifest_path
56
+ from .installer import Installer
57
+
58
+ __all__ = ["ActivationPlan", "add_activation_routes", "compute_plan"]
59
+
60
+ _log = core_logger("activation")
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class ActivationPlan:
65
+ """What one activation would do, per managed pack name."""
66
+
67
+ generation: int | None
68
+ add: tuple[str, ...]
69
+ remove: tuple[str, ...]
70
+ reload: tuple[str, ...]
71
+ unchanged: tuple[str, ...]
72
+
73
+ def to_wire(self) -> dict[str, object]:
74
+ return {
75
+ "generation": self.generation,
76
+ "add": list(self.add),
77
+ "remove": list(self.remove),
78
+ "reload": list(self.reload),
79
+ "unchanged": list(self.unchanged),
80
+ }
81
+
82
+
83
+ def _spec_name(spec: PackSpec) -> str:
84
+ """A packs_for_serving spec carries exactly its manifest's pack-table
85
+ entry; the key is the pack name without re-parsing the manifest."""
86
+ assert spec.packs is not None and len(spec.packs) == 1
87
+ return next(iter(spec.packs))
88
+
89
+
90
+ def compute_plan(
91
+ composer: ServingComposer, installer: Installer
92
+ ) -> tuple[ActivationPlan, dict[str, PackSpec]]:
93
+ """Diff the live managed packs against the current generation.
94
+
95
+ Returns the plan plus the generation's specs by name (what apply
96
+ needs for the add/reload halves). Dev packs - composed specs whose
97
+ manifest is outside the store - are excluded from the diff entirely.
98
+ """
99
+ new_specs = {_spec_name(spec): spec for spec in installer.packs_for_serving()}
100
+ store_root = installer.store_root.resolve()
101
+ managed = {
102
+ name: spec
103
+ for name, spec in composer.pack_specs().items()
104
+ if Path(spec.manifest).resolve().is_relative_to(store_root)
105
+ }
106
+ add = tuple(sorted(name for name in new_specs if name not in managed))
107
+ remove = tuple(sorted(name for name in managed if name not in new_specs))
108
+ reload_ = tuple(
109
+ sorted(
110
+ name for name, spec in managed.items() if name in new_specs and new_specs[name] != spec
111
+ )
112
+ )
113
+ unchanged = tuple(
114
+ sorted(
115
+ name for name, spec in managed.items() if name in new_specs and new_specs[name] == spec
116
+ )
117
+ )
118
+ return (
119
+ ActivationPlan(
120
+ generation=installer.current_number(),
121
+ add=add,
122
+ remove=remove,
123
+ reload=reload_,
124
+ unchanged=unchanged,
125
+ ),
126
+ new_specs,
127
+ )
128
+
129
+
130
+ async def apply_activation(
131
+ state: ServerState, composer: ServingComposer, installer: Installer
132
+ ) -> dict[str, object]:
133
+ """Stage, validate, and atomically publish one complete generation."""
134
+ plan, new_specs = compute_plan(composer, installer)
135
+ if not (plan.add or plan.remove or plan.reload):
136
+ return {
137
+ "generation": plan.generation,
138
+ "epoch": state.schema_epoch,
139
+ "added": [],
140
+ "removed": [],
141
+ "reloaded": [],
142
+ "unchanged": list(plan.unchanged),
143
+ "failed": {},
144
+ "diagnostics": [],
145
+ "rolledBack": False,
146
+ }
147
+
148
+ current_specs = composer.pack_specs()
149
+ store_root = installer.store_root.resolve()
150
+ desired = {
151
+ name: spec
152
+ for name, spec in current_specs.items()
153
+ if not Path(spec.manifest).resolve().is_relative_to(store_root)
154
+ }
155
+ desired.update(new_specs)
156
+
157
+ async with composer.generation_transaction():
158
+ staged = composer.spawn_empty(composition_mode="production")
159
+ diagnostics_by_name: dict[str, dict[str, str]] = {}
160
+ adopted = False
161
+
162
+ def record_failure(name: str, exc: Exception) -> None:
163
+ message = str(exc)
164
+ if isinstance(exc, (FileNotFoundError, ModuleNotFoundError)) or (
165
+ message.startswith("pack manifest not found:")
166
+ or "ModuleNotFoundError" in message
167
+ or "No module named" in message
168
+ ):
169
+ kind = "missing-pack"
170
+ elif isinstance(exc, (CompositionError, ValueError)):
171
+ kind = "incompatible-pack"
172
+ else:
173
+ kind = "failed-pack"
174
+ diagnostics_by_name[name] = {"pack": name, "kind": kind, "error": message}
175
+ _log.error("activation: staging pack %s failed: %s", name, exc)
176
+
177
+ try:
178
+ valid: list[PackSpec] = []
179
+ names_by_manifest: dict[Path, str] = {}
180
+ for name in sorted(desired):
181
+ spec = desired[name]
182
+ try:
183
+ path = resolve_manifest_path(spec.manifest)
184
+ except Exception as exc:
185
+ record_failure(name, exc)
186
+ continue
187
+ valid.append(spec)
188
+ names_by_manifest[path.resolve()] = name
189
+ try:
190
+ ordered = staged.order_pack_entries(valid)
191
+ except Exception as exc:
192
+ record_failure("generation", exc)
193
+ ordered = ()
194
+ for spec in ordered:
195
+ path = resolve_manifest_path(spec.manifest)
196
+ name = names_by_manifest[path.resolve()]
197
+ try:
198
+ delta = await staged.add_pack(spec)
199
+ if delta.pack != name:
200
+ raise CompositionError(
201
+ f"generation expected pack {name!r}, manifest declared {delta.pack!r}"
202
+ )
203
+ except Exception as exc:
204
+ record_failure(name, exc)
205
+ try:
206
+ staged.validate_complete_generation()
207
+ except Exception as exc:
208
+ record_failure("generation", exc)
209
+ diagnostics = [diagnostics_by_name[name] for name in sorted(diagnostics_by_name)]
210
+ if diagnostics:
211
+ await staged.close()
212
+ failed = {item["pack"]: item["error"] for item in diagnostics}
213
+ return {
214
+ "generation": plan.generation,
215
+ "epoch": state.schema_epoch,
216
+ "added": [],
217
+ "removed": [],
218
+ "reloaded": [],
219
+ "unchanged": list(plan.unchanged),
220
+ "failed": failed,
221
+ "diagnostics": diagnostics,
222
+ "rolledBack": True,
223
+ "rollbackEvidence": {
224
+ "epoch": state.schema_epoch,
225
+ "extensionSnapshotDigest": state.engine.extension_snapshot_digest,
226
+ "packs": sorted(current_specs),
227
+ },
228
+ }
229
+
230
+ composition = staged.composition
231
+
232
+ async def publish() -> int:
233
+ nonlocal adopted
234
+ validation = await state.prepare_generation(
235
+ composition.schemas,
236
+ composition.packs,
237
+ composition.node_packs,
238
+ )
239
+ epoch = state.publish_generation(
240
+ composition.schemas,
241
+ composition.packs,
242
+ composition.node_packs,
243
+ execution_arms=composition.execution_arms,
244
+ choices=composition.choices,
245
+ lazy_choices=composition.lazy_choices,
246
+ schema_owners=composition.schema_owners,
247
+ choice_owners=composition.choice_owners,
248
+ compat_skips=composition.compat_skips,
249
+ _validation=validation,
250
+ )
251
+ old = composer.adopt(staged)
252
+ adopted = True
253
+ for name in plan.remove:
254
+ state.mark_pack_removed(name, epoch)
255
+ for name in (*plan.reload, *plan.add):
256
+ state.mark_pack_announced(name, epoch)
257
+ clear = getattr(state.engine.cache, "clear", None)
258
+ if callable(clear):
259
+ clear()
260
+ # Fail-loud retirement policy: the old generation is no longer
261
+ # admitted, and its workers are retired immediately. An in-flight
262
+ # run pinned to one may fail rather than drift generations.
263
+ await old.close()
264
+ # Retirement awaits, so an old invocation that was already
265
+ # returning could repopulate a legacy key after the first clear.
266
+ if callable(clear):
267
+ clear()
268
+ return epoch
269
+
270
+ await composer.finish_publication(publish())
271
+ except BaseException:
272
+ if not adopted:
273
+ await staged.close()
274
+ raise
275
+
276
+ _log.info(
277
+ "activation: generation %s live - %d added, %d removed, "
278
+ "%d reloaded, %d unchanged (epoch %d)",
279
+ plan.generation,
280
+ len(plan.add),
281
+ len(plan.remove),
282
+ len(plan.reload),
283
+ len(plan.unchanged),
284
+ state.schema_epoch,
285
+ )
286
+ return {
287
+ "generation": plan.generation,
288
+ "epoch": state.schema_epoch,
289
+ "added": list(plan.add),
290
+ "removed": list(plan.remove),
291
+ "reloaded": list(plan.reload),
292
+ "unchanged": list(plan.unchanged),
293
+ "failed": {},
294
+ "diagnostics": [],
295
+ "rolledBack": False,
296
+ }
297
+
298
+
299
+ def _conflict(error: str, detail: str) -> web.HTTPConflict:
300
+ return web.HTTPConflict(
301
+ text=json.dumps({"error": error, "detail": detail}),
302
+ content_type="application/json",
303
+ )
304
+
305
+
306
+ async def _read_pinned_generation(request: web.Request) -> int | None:
307
+ """The optional {"generation": N} pin from the POST body. An empty
308
+ body is the unpinned delivery-hook flow; anything else must be a JSON
309
+ object whose generation, if present, is an integer."""
310
+ raw = await request.read()
311
+ if not raw:
312
+ return None
313
+ try:
314
+ body = json.loads(raw)
315
+ except ValueError:
316
+ raise web.HTTPBadRequest(
317
+ text=json.dumps({"error": "bad-request", "detail": "body must be JSON"}),
318
+ content_type="application/json",
319
+ ) from None
320
+ if not isinstance(body, dict):
321
+ raise web.HTTPBadRequest(
322
+ text=json.dumps({"error": "bad-request", "detail": "body must be a JSON object"}),
323
+ content_type="application/json",
324
+ )
325
+ pinned = body.get("generation")
326
+ if pinned is None:
327
+ return None
328
+ if not isinstance(pinned, int) or isinstance(pinned, bool):
329
+ raise web.HTTPBadRequest(
330
+ text=json.dumps(
331
+ {
332
+ "error": "bad-request",
333
+ "detail": "generation must be an integer",
334
+ }
335
+ ),
336
+ content_type="application/json",
337
+ )
338
+ return pinned
339
+
340
+
341
+ def add_activation_routes(
342
+ app: web.Application, composer: ServingComposer, installer: Installer
343
+ ) -> None:
344
+ busy = asyncio.Lock()
345
+
346
+ async def handle_plan(request: web.Request) -> web.Response:
347
+ plan, _ = compute_plan(composer, installer)
348
+ return web.json_response(plan.to_wire())
349
+
350
+ async def handle_activate(request: web.Request) -> web.Response:
351
+ pinned = await _read_pinned_generation(request)
352
+ if busy.locked():
353
+ raise _conflict(
354
+ "activation-in-progress",
355
+ "another activation is being applied; retry when it finishes",
356
+ )
357
+ async with busy:
358
+ state = request.app[STATE_KEY]
359
+ if state.composition_progress is not None:
360
+ # Startup composition still driving: the plan would list
361
+ # pending packs as adds and race the drive tasks into
362
+ # duplicate-compose failures. The surface converges to
363
+ # the current generation on its own; activation only
364
+ # makes sense once it has settled.
365
+ raise _conflict(
366
+ "composition-in-progress",
367
+ "startup composition has not finished; retry after composition_complete",
368
+ )
369
+ current = installer.current_number()
370
+ if pinned is not None and pinned != current:
371
+ raise _conflict(
372
+ "generation-changed",
373
+ f"planned against generation {pinned} but "
374
+ f"{current} is current; re-fetch the plan",
375
+ )
376
+ payload = await apply_activation(state, composer, installer)
377
+ return web.json_response(payload)
378
+
379
+ app.router.add_get("/api/install/activation", handle_plan)
380
+ app.router.add_post("/api/install/activation", handle_activate)