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.
- dinkster/__init__.py +6 -0
- dinkster/__main__.py +3 -0
- dinkster/activation.py +380 -0
- dinkster/benchmark.py +531 -0
- dinkster/checkout-packs.lock +1 -0
- dinkster/cli.py +125 -0
- dinkster/comfy_compose.py +745 -0
- dinkster/compose.py +8007 -0
- dinkster/demo.py +284 -0
- dinkster/extension_assets.py +45 -0
- dinkster/frontend.py +159 -0
- dinkster/frontend_bundle/manifest.json +6 -0
- dinkster/frontend_bundle/static/assets/GLTFLoader-Bz7v_R8g.js +1 -0
- dinkster/frontend_bundle/static/assets/OrbitControls-Bm0pTgiB.js +1 -0
- dinkster/frontend_bundle/static/assets/index-DFieIngV.js +143 -0
- dinkster/frontend_bundle/static/assets/index-DoL_bCWG.css +1 -0
- dinkster/frontend_bundle/static/assets/model3d-viewer-CcNPJ4Fq.js +2 -0
- dinkster/frontend_bundle/static/assets/shared-worker-entry-CH6pyGn3.js +2 -0
- dinkster/frontend_bundle/static/assets/spark.module-DFJBdDqJ.js +12120 -0
- dinkster/frontend_bundle/static/assets/three.module-Y-ql4QRg.js +4116 -0
- dinkster/frontend_bundle/static/assets/workspace-worker-entry-BN5IbNTT.js +2 -0
- dinkster/frontend_bundle/static/branding/platform/dinkster.icns +0 -0
- dinkster/frontend_bundle/static/branding/platform/dinkster.ico +0 -0
- dinkster/frontend_bundle/static/branding/raster/color/1024.png +0 -0
- dinkster/frontend_bundle/static/branding/raster/color/128.png +0 -0
- dinkster/frontend_bundle/static/branding/raster/color/16.png +0 -0
- dinkster/frontend_bundle/static/branding/raster/color/24.png +0 -0
- dinkster/frontend_bundle/static/branding/raster/color/256.png +0 -0
- dinkster/frontend_bundle/static/branding/raster/color/32.png +0 -0
- dinkster/frontend_bundle/static/branding/raster/color/48.png +0 -0
- dinkster/frontend_bundle/static/branding/raster/color/512.png +0 -0
- dinkster/frontend_bundle/static/branding/raster/color/64.png +0 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-color.svg +109 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-cyan.svg +87 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-dark.svg +87 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-light.svg +87 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-mono.svg +87 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-symbol-white.svg +87 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-color-dark.svg +157 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-color-light.svg +157 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-dark.svg +125 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-heavier-dark.svg +147 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-heavier-light.svg +147 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-light.svg +125 -0
- dinkster/frontend_bundle/static/branding/svg/dinkster-wordmark-mono.svg +125 -0
- dinkster/frontend_bundle/static/fonts/LICENSE +155 -0
- dinkster/frontend_bundle/static/fonts/NotoSans-Bold.woff2 +0 -0
- dinkster/frontend_bundle/static/fonts/NotoSans-Italic.woff2 +0 -0
- dinkster/frontend_bundle/static/fonts/NotoSans-Regular.woff2 +0 -0
- dinkster/frontend_bundle/static/fonts/NotoSansMono-Regular.woff2 +0 -0
- dinkster/frontend_bundle/static/index.html +167 -0
- dinkster/frontend_bundle/static/manifest.webmanifest +1 -0
- dinkster/generation_api.py +1725 -0
- dinkster/generation_packs.py +180 -0
- dinkster/guess_api.py +324 -0
- dinkster/installer.py +1548 -0
- dinkster/isolated_demo.py +183 -0
- dinkster/launch.py +90 -0
- dinkster/lazy_worker.py +216 -0
- dinkster/manager.py +1427 -0
- dinkster/mounts_api.py +786 -0
- dinkster/native_policy.py +71 -0
- dinkster/official_packs/dinkster-nodes-comfy-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-nodes-foundation-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-nodes-image-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-nodes-media-io-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-nodes-remote-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-vision-birefnet-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-vision-depth-anything-v2-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-vision-depth-anything-v3-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-vision-detr-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-vision-efficient-sam-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-vision-hed-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-vision-rtdetr-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-vision-sam31-0.0.2.zip +0 -0
- dinkster/official_packs/dinkster-vision-upscale-0.0.2.zip +0 -0
- dinkster/official_packs/official-packs.lock +1 -0
- dinkster/pack_archive.py +135 -0
- dinkster/packs.py +199 -0
- dinkster/port.py +1105 -0
- dinkster/project_folders.py +180 -0
- dinkster/project_requirements.py +318 -0
- dinkster/registries.py +700 -0
- dinkster/reload_api.py +243 -0
- dinkster/remote_reconnect.py +218 -0
- dinkster/remotes.py +196 -0
- dinkster/resolver_api.py +135 -0
- dinkster/serve.py +2556 -0
- dinkster/setup.py +79 -0
- dinkster/storelock.py +89 -0
- dinkster/watch.py +159 -0
- dinkster/workflow_requirements.py +353 -0
- dinkster/workflow_requirements_api.py +149 -0
- dinkster-0.0.2.dist-info/METADATA +30 -0
- dinkster-0.0.2.dist-info/RECORD +286 -0
- dinkster-0.0.2.dist-info/WHEEL +4 -0
- dinkster-0.0.2.dist-info/entry_points.txt +6 -0
- dinkster-0.0.2.dist-info/licenses/LICENSE +674 -0
- dinkster_collab/__init__.py +49 -0
- dinkster_collab/routes.py +922 -0
- dinkster_collab/sessions.py +442 -0
- dinkster_collab/store.py +273 -0
- dinkster_compat_comfy/__init__.py +93 -0
- dinkster_compat_comfy/adapters.py +890 -0
- dinkster_compat_comfy/audio.py +12 -0
- dinkster_compat_comfy/bootstrap.py +389 -0
- dinkster_compat_comfy/comfy_execution.py +150 -0
- dinkster_compat_comfy/core_schemas.json +72248 -0
- dinkster_compat_comfy/devices.py +12 -0
- dinkster_compat_comfy/entry.py +85 -0
- dinkster_compat_comfy/image.py +12 -0
- dinkster_compat_comfy/latent.py +12 -0
- dinkster_compat_comfy/legacy.py +480 -0
- dinkster_compat_comfy/legacy_entry.py +67 -0
- dinkster_compat_comfy/legacy_sources.py +12 -0
- dinkster_compat_comfy/listmap.py +150 -0
- dinkster_compat_comfy/memory.py +12 -0
- dinkster_compat_comfy/native.py +12 -0
- dinkster_compat_comfy/pool.py +16 -0
- dinkster_compat_comfy/port_probe.py +247 -0
- dinkster_compat_comfy/prompt.py +526 -0
- dinkster_compat_comfy/py.typed +0 -0
- dinkster_compat_comfy/resident.py +12 -0
- dinkster_compat_comfy/saved_results.py +59 -0
- dinkster_compat_comfy/schema_snapshot.py +11 -0
- dinkster_compat_comfy/source_staging.py +565 -0
- dinkster_compat_comfy/translate.py +3392 -0
- dinkster_compat_comfy/translate_v3.py +751 -0
- dinkster_compat_comfy/video.py +12 -0
- dinkster_compat_comfy_pack/dinkster-pack.toml +165 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/__init__.py +93 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/adapters.py +890 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/audio.py +12 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/bootstrap.py +389 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/comfy_execution.py +150 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/core_schemas.json +72248 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/devices.py +12 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/entry.py +85 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/image.py +12 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/latent.py +12 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/legacy.py +480 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/legacy_entry.py +67 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/legacy_sources.py +12 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/listmap.py +150 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/memory.py +12 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/native.py +12 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/pool.py +16 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/port_probe.py +247 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/prompt.py +526 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/py.typed +0 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/resident.py +12 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/saved_results.py +59 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/schema_snapshot.py +11 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/source_staging.py +565 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/translate.py +3392 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/translate_v3.py +751 -0
- dinkster_compat_comfy_pack/dinkster_compat_comfy/video.py +12 -0
- dinkster_engine/__init__.py +75 -0
- dinkster_engine/compile.py +871 -0
- dinkster_engine/control.py +108 -0
- dinkster_engine/engine.py +3329 -0
- dinkster_engine/events.py +38 -0
- dinkster_engine/expansion.py +231 -0
- dinkster_engine/py.typed +0 -0
- dinkster_graph/__init__.py +61 -0
- dinkster_graph/lower.py +126 -0
- dinkster_graph/migrate.py +70 -0
- dinkster_graph/model.py +223 -0
- dinkster_graph/plan.py +113 -0
- dinkster_graph/py.typed +0 -0
- dinkster_graph/validate.py +1373 -0
- dinkster_graph/wire.py +356 -0
- dinkster_native/__init__.py +10 -0
- dinkster_native/attention.py +399 -0
- dinkster_native/audio.py +57 -0
- dinkster_native/devices.py +244 -0
- dinkster_native/entry.py +13 -0
- dinkster_native/fork_nodes.py +423 -0
- dinkster_native/image.py +90 -0
- dinkster_native/latent.py +52 -0
- dinkster_native/legacy_sources.py +328 -0
- dinkster_native/memory.py +134 -0
- dinkster_native/model3d/__init__.py +1 -0
- dinkster_native/model3d/image_crop.py +127 -0
- dinkster_native/model3d/image_preprocess.py +37 -0
- dinkster_native/model3d/mesh.py +333 -0
- dinkster_native/model3d/mesh_operations.py +2861 -0
- dinkster_native/model3d/mesh_ops/__init__.py +1 -0
- dinkster_native/model3d/mesh_ops/qem_decimate.py +1816 -0
- dinkster_native/model3d/mesh_ops/remesh.py +1373 -0
- dinkster_native/model3d/mesh_ops/uv_unwrap/__init__.py +1 -0
- dinkster_native/model3d/mesh_ops/uv_unwrap/mesh.py +166 -0
- dinkster_native/model3d/mesh_ops/uv_unwrap/pack.py +1006 -0
- dinkster_native/model3d/mesh_ops/uv_unwrap/parameterize.py +596 -0
- dinkster_native/model3d/mesh_ops/uv_unwrap/segment.py +472 -0
- dinkster_native/model3d/moge_geometry.py +170 -0
- dinkster_native/multigpu.py +139 -0
- dinkster_native/native.py +163 -0
- dinkster_native/nodes_model3d.py +382 -0
- dinkster_native/pool.py +130 -0
- dinkster_native/py.typed +1 -0
- dinkster_native/resident.py +50 -0
- dinkster_native/type_ids.py +5 -0
- dinkster_native/video.py +231 -0
- dinkster_native/workgroup.py +180 -0
- dinkster_native_pack/dinkster-pack.toml +92 -0
- dinkster_native_pack/dinkster_native/__init__.py +10 -0
- dinkster_native_pack/dinkster_native/attention.py +399 -0
- dinkster_native_pack/dinkster_native/audio.py +57 -0
- dinkster_native_pack/dinkster_native/devices.py +244 -0
- dinkster_native_pack/dinkster_native/entry.py +13 -0
- dinkster_native_pack/dinkster_native/fork_nodes.py +423 -0
- dinkster_native_pack/dinkster_native/image.py +90 -0
- dinkster_native_pack/dinkster_native/latent.py +52 -0
- dinkster_native_pack/dinkster_native/legacy_sources.py +328 -0
- dinkster_native_pack/dinkster_native/memory.py +134 -0
- dinkster_native_pack/dinkster_native/model3d/__init__.py +1 -0
- dinkster_native_pack/dinkster_native/model3d/image_crop.py +127 -0
- dinkster_native_pack/dinkster_native/model3d/image_preprocess.py +37 -0
- dinkster_native_pack/dinkster_native/model3d/mesh.py +333 -0
- dinkster_native_pack/dinkster_native/model3d/mesh_operations.py +2861 -0
- dinkster_native_pack/dinkster_native/model3d/mesh_ops/__init__.py +1 -0
- dinkster_native_pack/dinkster_native/model3d/mesh_ops/qem_decimate.py +1816 -0
- dinkster_native_pack/dinkster_native/model3d/mesh_ops/remesh.py +1373 -0
- dinkster_native_pack/dinkster_native/model3d/mesh_ops/uv_unwrap/__init__.py +1 -0
- dinkster_native_pack/dinkster_native/model3d/mesh_ops/uv_unwrap/mesh.py +166 -0
- dinkster_native_pack/dinkster_native/model3d/mesh_ops/uv_unwrap/pack.py +1006 -0
- dinkster_native_pack/dinkster_native/model3d/mesh_ops/uv_unwrap/parameterize.py +596 -0
- dinkster_native_pack/dinkster_native/model3d/mesh_ops/uv_unwrap/segment.py +472 -0
- dinkster_native_pack/dinkster_native/model3d/moge_geometry.py +170 -0
- dinkster_native_pack/dinkster_native/multigpu.py +139 -0
- dinkster_native_pack/dinkster_native/native.py +163 -0
- dinkster_native_pack/dinkster_native/nodes_model3d.py +382 -0
- dinkster_native_pack/dinkster_native/pool.py +130 -0
- dinkster_native_pack/dinkster_native/py.typed +1 -0
- dinkster_native_pack/dinkster_native/resident.py +50 -0
- dinkster_native_pack/dinkster_native/type_ids.py +5 -0
- dinkster_native_pack/dinkster_native/video.py +231 -0
- dinkster_native_pack/dinkster_native/workgroup.py +180 -0
- dinkster_nodes_generation/__init__.py +30 -0
- dinkster_nodes_generation/model3d.py +550 -0
- dinkster_nodes_generation/nodes.py +6120 -0
- dinkster_nodes_generation/prompt_enhance.py +105 -0
- dinkster_nodes_generation/py.typed +1 -0
- dinkster_nodes_generation/templates/minimax-h3.json +383 -0
- dinkster_nodes_generation/templates/minimax-h3.png +0 -0
- dinkster_nodes_generation/templates/sd15.json +231 -0
- dinkster_nodes_generation/templates/sd15.png +0 -0
- dinkster_nodes_generation/trellis2.py +248 -0
- dinkster_nodes_generation_pack/dinkster-pack.toml +40 -0
- dinkster_nodes_generation_pack/dinkster_nodes_generation/__init__.py +30 -0
- dinkster_nodes_generation_pack/dinkster_nodes_generation/model3d.py +550 -0
- dinkster_nodes_generation_pack/dinkster_nodes_generation/nodes.py +6120 -0
- dinkster_nodes_generation_pack/dinkster_nodes_generation/prompt_enhance.py +105 -0
- dinkster_nodes_generation_pack/dinkster_nodes_generation/py.typed +1 -0
- dinkster_nodes_generation_pack/dinkster_nodes_generation/templates/minimax-h3.json +383 -0
- dinkster_nodes_generation_pack/dinkster_nodes_generation/templates/minimax-h3.png +0 -0
- dinkster_nodes_generation_pack/dinkster_nodes_generation/templates/sd15.json +231 -0
- dinkster_nodes_generation_pack/dinkster_nodes_generation/templates/sd15.png +0 -0
- dinkster_nodes_generation_pack/dinkster_nodes_generation/trellis2.py +248 -0
- dinkster_server/__init__.py +186 -0
- dinkster_server/_federated_asset_catalog.py +797 -0
- dinkster_server/app.py +4794 -0
- dinkster_server/asset_stream.py +93 -0
- dinkster_server/auth.py +1073 -0
- dinkster_server/cache_client.py +146 -0
- dinkster_server/discovery.py +160 -0
- dinkster_server/events.py +281 -0
- dinkster_server/execution_journal.py +448 -0
- dinkster_server/federated_assets_v1.py +1407 -0
- dinkster_server/history.py +672 -0
- dinkster_server/image_document.py +472 -0
- dinkster_server/journal.py +355 -0
- dinkster_server/library.py +1673 -0
- dinkster_server/network_cost.py +193 -0
- dinkster_server/pack_settings.py +57 -0
- dinkster_server/pack_surfaces.py +128 -0
- dinkster_server/paging.py +52 -0
- dinkster_server/peers.py +308 -0
- dinkster_server/preflight.py +353 -0
- dinkster_server/py.typed +0 -0
- dinkster_server/queue.py +635 -0
- dinkster_server/redaction.py +132 -0
- dinkster_server/serve.py +127 -0
- dinkster_server/settings.py +512 -0
- dinkster_server/training_sessions.py +1188 -0
dinkster/__init__.py
ADDED
dinkster/__main__.py
ADDED
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)
|