mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-28 12:26:46 +08:00
feat(plugin): route certified installs to shared workers
This commit is contained in:
@@ -8,11 +8,12 @@ persistence, or a Plugin Runtime apply request. It calls the SDK public
|
||||
certificate envelope from the ZIP comment and verifies the signed normalized
|
||||
ZIP digest without extracting the payload.
|
||||
|
||||
Core retains the normalized digest (`normalized_zip_digest()`), verification
|
||||
state, declared shared-runtime profile, key ID, selected admission profile, and
|
||||
stable admission code in the durable plugin `install_info._certification`
|
||||
record. The record belongs to the installation row; no schema migration is
|
||||
needed for this additive JSON metadata.
|
||||
Core retains the artifact SHA-256, normalized digest
|
||||
(`normalized_zip_digest()`), verification state, declared shared-runtime
|
||||
profile, key ID, selected admission profile, and stable admission code in the
|
||||
durable plugin `install_info._certification` record. The record belongs to the
|
||||
installation row; no schema migration is needed for this additive JSON
|
||||
metadata.
|
||||
|
||||
## Trusted issuer configuration
|
||||
|
||||
@@ -50,13 +51,14 @@ protects those endpoints. A force never creates a Cloud dedicated fallback.
|
||||
|
||||
## Runtime and logs
|
||||
|
||||
The current Plugin Runtime control protocol has one process-wide runtime profile
|
||||
per Core instance. In Cloud that existing profile is `shared`; Cloud admission
|
||||
therefore prevents an archive that did not select `shared-runtime-v1` from
|
||||
reaching its apply API. In OSS the existing `oss_dev` runtime remains the
|
||||
dedicated compatibility profile. Core records the selected profile for every
|
||||
installation so a future multi-runtime control protocol can consume it without
|
||||
re-verifying an already persisted archive.
|
||||
SDK 0.6.2 carries an installation-level execution mode in both apply and
|
||||
authoritative reconcile payloads. Core selects `shared-runtime-v1` only when
|
||||
the persisted certification record says verification was valid, both the
|
||||
certificate and admission profiles are `shared-runtime-v1`, the admission code
|
||||
is shared-eligible, and the record's artifact SHA-256 exactly matches the
|
||||
installation row. Missing, malformed, stale, invalid, or dedicated admission
|
||||
facts select `dedicated`. Install, upgrade, configuration revision, restart,
|
||||
and reconnect all use this same persisted-fact derivation.
|
||||
|
||||
The existing public plugin-log boundary already applies the immutable
|
||||
installation binding (including workspace UUID) through
|
||||
@@ -68,7 +70,5 @@ same existing installation scope.
|
||||
|
||||
## SDK versioning
|
||||
|
||||
Core intentionally continues to declare `langbot-plugin==0.5.8` until the SDK
|
||||
beta containing this public certification API is released. Local development
|
||||
and the integration tests may install the SDK source checkout, but this Core
|
||||
change does not publish or pin a prerelease.
|
||||
Core pins `langbot-plugin==0.6.2`, the first published SDK release carrying the
|
||||
canonical installation execution-mode contract.
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ dependencies = [
|
||||
"langchain-text-splitters>=1.1.2",
|
||||
"chromadb>=1.0.0,<2.0.0",
|
||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||
"langbot-plugin==0.6.1",
|
||||
"langbot-plugin==0.6.2",
|
||||
"asyncpg>=0.30.0",
|
||||
"line-bot-sdk>=3.19.0",
|
||||
"matrix-nio>=0.25.2",
|
||||
|
||||
@@ -14,6 +14,7 @@ from enum import Enum
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
from langbot_plugin.certification import normalized_zip_digest, verify_archive
|
||||
from langbot_plugin.entities.io.context import PluginExecutionMode
|
||||
|
||||
|
||||
SHARED_RUNTIME_V1 = 'shared-runtime-v1'
|
||||
@@ -232,3 +233,27 @@ def decide_plugin_log_visibility(facts: PluginCertificationFacts) -> PluginLogVi
|
||||
if facts.certificate.is_valid_shared_runtime:
|
||||
return PluginLogVisibility.TENANT_SCOPED
|
||||
return PluginLogVisibility.DETAILED_PROCESS
|
||||
|
||||
|
||||
def execution_mode_for_persisted_installation(
|
||||
*,
|
||||
artifact_digest: str,
|
||||
install_info: object,
|
||||
) -> PluginExecutionMode:
|
||||
"""Derive placement only from persisted facts bound to the exact artifact."""
|
||||
|
||||
if not isinstance(install_info, Mapping):
|
||||
return PluginExecutionMode.DEDICATED
|
||||
certification = install_info.get('_certification')
|
||||
if not isinstance(certification, Mapping):
|
||||
return PluginExecutionMode.DEDICATED
|
||||
shared_facts = {
|
||||
'artifact_digest': artifact_digest,
|
||||
'verification': CertificateVerification.VALID.value,
|
||||
'certificate_runtime_profile': SHARED_RUNTIME_V1,
|
||||
'runtime_profile': SHARED_RUNTIME_V1,
|
||||
'admission_code': AdmissionCode.SHARED_ELIGIBLE.value,
|
||||
}
|
||||
if all(certification.get(key) == value for key, value in shared_facts.items()):
|
||||
return PluginExecutionMode.SHARED_CERTIFIED
|
||||
return PluginExecutionMode.DEDICATED
|
||||
|
||||
@@ -34,6 +34,7 @@ from .certification import (
|
||||
PluginCertificationFacts,
|
||||
VerifiedArchiveCertificate,
|
||||
decide_plugin_admission,
|
||||
execution_mode_for_persisted_installation,
|
||||
verify_plugin_archive_certificate,
|
||||
)
|
||||
from .github import (
|
||||
@@ -62,6 +63,7 @@ from langbot_plugin.runtime.security import (
|
||||
)
|
||||
from langbot_plugin.entities.io.context import (
|
||||
InstallationBinding,
|
||||
PluginExecutionMode,
|
||||
PluginInstallationDesiredState,
|
||||
PluginWorkerPolicy,
|
||||
RuntimeIdentity,
|
||||
@@ -343,6 +345,13 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
artifact_digest=setting.artifact_digest,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _execution_mode_from_setting(setting: persistence_plugin.PluginSetting) -> PluginExecutionMode:
|
||||
return execution_mode_for_persisted_installation(
|
||||
artifact_digest=setting.artifact_digest,
|
||||
install_info=setting.install_info,
|
||||
)
|
||||
|
||||
def _legacy_oss_bridge_binding(self, execution_context: ExecutionContext) -> InstallationBinding:
|
||||
seed = f'langbot:oss-plugin-bridge:{execution_context.instance_uuid}:{execution_context.workspace_uuid}'
|
||||
return InstallationBinding(
|
||||
@@ -511,6 +520,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
PluginInstallationDesiredState(
|
||||
binding=binding,
|
||||
enabled=setting.enabled,
|
||||
execution_mode=self._execution_mode_from_setting(setting),
|
||||
)
|
||||
)
|
||||
return tuple(desired_states)
|
||||
@@ -526,6 +536,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
desired.binding,
|
||||
artifact_package=artifact_package,
|
||||
enabled=desired.enabled,
|
||||
execution_mode=desired.execution_mode,
|
||||
)
|
||||
self._raise_apply_failure(desired, result)
|
||||
if result.get('state') != 'artifact_missing':
|
||||
@@ -551,6 +562,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
desired.binding,
|
||||
artifact_package=persisted_package,
|
||||
enabled=desired.enabled,
|
||||
execution_mode=desired.execution_mode,
|
||||
)
|
||||
self._raise_apply_failure(desired, repaired)
|
||||
if repaired.get('state') == 'artifact_missing':
|
||||
@@ -1793,6 +1805,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
}:
|
||||
raise ValueError(decision.code.value)
|
||||
certification_info = {
|
||||
'artifact_digest': hashlib.sha256(file_bytes).hexdigest(),
|
||||
'normalized_digest': facts.artifact_digest,
|
||||
'verification': facts.certificate.verification.value,
|
||||
'certificate_runtime_profile': facts.certificate.runtime_profile,
|
||||
@@ -1893,7 +1906,14 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
install_info=install_info,
|
||||
artifact_digest=artifact_digest,
|
||||
)
|
||||
desired = PluginInstallationDesiredState(binding=binding, enabled=True)
|
||||
desired = PluginInstallationDesiredState(
|
||||
binding=binding,
|
||||
enabled=True,
|
||||
execution_mode=execution_mode_for_persisted_installation(
|
||||
artifact_digest=artifact_digest,
|
||||
install_info=install_info,
|
||||
),
|
||||
)
|
||||
runtime_handler.register_installation_binding(
|
||||
binding,
|
||||
plugin_author=plugin_author,
|
||||
@@ -2143,6 +2163,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
desired = PluginInstallationDesiredState(
|
||||
binding=binding,
|
||||
enabled=setting.enabled,
|
||||
execution_mode=self._execution_mode_from_setting(setting),
|
||||
)
|
||||
is_legacy_oss = self.runtime_profile == 'oss_dev' and (
|
||||
not isinstance(setting.install_info, dict)
|
||||
|
||||
@@ -24,6 +24,7 @@ from langbot_plugin.entities.io.context import (
|
||||
ActionContext,
|
||||
ApplyPluginInstallationRequest,
|
||||
InstallationBinding,
|
||||
PluginExecutionMode,
|
||||
PluginInstallationDesiredState,
|
||||
PluginWorkerPolicy,
|
||||
ReconcilePluginInstallationsRequest,
|
||||
@@ -2793,6 +2794,7 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
*,
|
||||
artifact_package: bytes | None,
|
||||
enabled: bool,
|
||||
execution_mode: PluginExecutionMode = PluginExecutionMode.DEDICATED,
|
||||
) -> dict[str, Any]:
|
||||
with self.installation_scope(binding):
|
||||
artifact_file_key = None
|
||||
@@ -2801,6 +2803,7 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
request = ApplyPluginInstallationRequest(
|
||||
artifact_file_key=artifact_file_key,
|
||||
enabled=enabled,
|
||||
execution_mode=execution_mode,
|
||||
)
|
||||
return await self.call_action(
|
||||
LangBotToRuntimeAction.APPLY_PLUGIN_INSTALLATION,
|
||||
|
||||
@@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
from langbot.pkg.plugin.connector import PluginRuntimeConnector
|
||||
from langbot_plugin.entities.io.context import InstallationBinding
|
||||
from langbot_plugin.entities.io.context import PluginExecutionMode
|
||||
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
||||
|
||||
|
||||
@@ -57,11 +58,17 @@ async def test_install_plugin_admits_archive_before_persistence_and_applies_sele
|
||||
)
|
||||
persisted_info = connector._persist_installation_package.await_args.kwargs['install_info']
|
||||
assert persisted_info['_certification']['runtime_profile'] == expected_profile
|
||||
assert persisted_info['_certification']['artifact_digest'] == hashlib.sha256(package).hexdigest()
|
||||
assert persisted_info['_certification']['normalized_digest'] == _normalized_digest(package)
|
||||
connector.handler.apply_plugin_installation.assert_awaited_once_with(
|
||||
binding,
|
||||
artifact_package=package,
|
||||
enabled=True,
|
||||
execution_mode=(
|
||||
PluginExecutionMode.SHARED_CERTIFIED
|
||||
if expected_profile == 'shared-runtime-v1'
|
||||
else PluginExecutionMode.DEDICATED
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -135,7 +142,10 @@ async def test_marketplace_version_selection_keeps_certificate_gate_and_single_a
|
||||
assert persisted_info['plugin_version'] == '1.0.0'
|
||||
assert persisted_info['_certification']['runtime_profile'] == 'shared-runtime-v1'
|
||||
connector.handler.apply_plugin_installation.assert_awaited_once_with(
|
||||
binding, artifact_package=package, enabled=True
|
||||
binding,
|
||||
artifact_package=package,
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.SHARED_CERTIFIED,
|
||||
)
|
||||
connector._refresh_runner_registry.assert_awaited_once()
|
||||
assert task_context.metadata['progress_percent'] == 100
|
||||
|
||||
@@ -6,6 +6,8 @@ import zipfile
|
||||
|
||||
import pytest
|
||||
|
||||
from langbot_plugin.entities.io.context import PluginExecutionMode
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('deployment', 'certificate', 'force', 'expected_disposition', 'expected_code'),
|
||||
@@ -150,6 +152,80 @@ def test_log_visibility_policy_only_scopes_valid_shared_certifications(
|
||||
assert visibility.value == expected_visibility
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('certification', 'expected_mode'),
|
||||
[
|
||||
(
|
||||
{
|
||||
'artifact_digest': 'a' * 64,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
},
|
||||
PluginExecutionMode.SHARED_CERTIFIED,
|
||||
),
|
||||
(None, PluginExecutionMode.DEDICATED),
|
||||
({}, PluginExecutionMode.DEDICATED),
|
||||
(
|
||||
{
|
||||
'artifact_digest': 'a' * 64,
|
||||
'verification': 'invalid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
},
|
||||
PluginExecutionMode.DEDICATED,
|
||||
),
|
||||
(
|
||||
{
|
||||
'artifact_digest': 'b' * 64,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
},
|
||||
PluginExecutionMode.DEDICATED,
|
||||
),
|
||||
(
|
||||
{
|
||||
'artifact_digest': 'a' * 64,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'runtime_profile': 'dedicated',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
},
|
||||
PluginExecutionMode.DEDICATED,
|
||||
),
|
||||
(
|
||||
{
|
||||
'artifact_digest': 'a' * 64,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_OSS_FORCED_DEDICATED',
|
||||
},
|
||||
PluginExecutionMode.DEDICATED,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_persisted_certification_selects_shared_execution_only_for_exact_admitted_artifact(
|
||||
certification: dict[str, str] | None,
|
||||
expected_mode: PluginExecutionMode,
|
||||
) -> None:
|
||||
from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation
|
||||
|
||||
install_info = {} if certification is None else {'_certification': certification}
|
||||
|
||||
assert (
|
||||
execution_mode_for_persisted_installation(
|
||||
artifact_digest='a' * 64,
|
||||
install_info=install_info,
|
||||
)
|
||||
is expected_mode
|
||||
)
|
||||
|
||||
|
||||
def _archive_bytes(manifest: dict[str, object]) -> bytes:
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, 'w') as archive:
|
||||
|
||||
@@ -17,7 +17,7 @@ from unittest.mock import AsyncMock, Mock
|
||||
from importlib import import_module
|
||||
|
||||
from tests.factories import text_query
|
||||
from langbot_plugin.entities.io.context import InstallationBinding
|
||||
from langbot_plugin.entities.io.context import InstallationBinding, PluginExecutionMode
|
||||
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
from langbot.pkg.workspace.errors import WorkspaceNotFoundError
|
||||
@@ -758,7 +758,16 @@ class TestSetPluginConfig:
|
||||
runtime_revision=1,
|
||||
artifact_digest=TEST_INSTALLATION_BINDING.artifact_digest,
|
||||
enabled=True,
|
||||
install_info={'_artifact_storage': 'tenant_binary_storage_v1'},
|
||||
install_info={
|
||||
'_artifact_storage': 'tenant_binary_storage_v1',
|
||||
'_certification': {
|
||||
'artifact_digest': TEST_INSTALLATION_BINDING.artifact_digest,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
},
|
||||
},
|
||||
)
|
||||
connector._setting_for_plugin = AsyncMock(return_value=(TEST_EXECUTION_CONTEXT, setting))
|
||||
connector.ap.persistence_mgr.execute_async = AsyncMock(return_value=SimpleNamespace(rowcount=1))
|
||||
@@ -777,6 +786,7 @@ class TestSetPluginConfig:
|
||||
applied_binding,
|
||||
artifact_package=None,
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.SHARED_CERTIFIED,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
from langbot_plugin.entities.io.context import InstallationBinding
|
||||
from langbot_plugin.entities.io.context import InstallationBinding, PluginExecutionMode
|
||||
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
||||
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
@@ -45,7 +45,17 @@ def mock_archive_admission(connector: PluginRuntimeConnector, digest: str) -> No
|
||||
# is exercised by integration/plugin/test_certified_plugin_admission.py.
|
||||
connector._admit_plugin_archive = Mock(
|
||||
side_effect=lambda _package, info: (
|
||||
{**info, '_certification': {'normalized_digest': digest}},
|
||||
{
|
||||
**info,
|
||||
'_certification': {
|
||||
'artifact_digest': digest,
|
||||
'normalized_digest': digest,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
},
|
||||
},
|
||||
SimpleNamespace(for_installation=lambda _uuid: SimpleNamespace(artifact_digest=digest)),
|
||||
)
|
||||
)
|
||||
@@ -56,6 +66,7 @@ def plugin_setting(
|
||||
artifact_digest: str,
|
||||
*,
|
||||
durable: bool = True,
|
||||
certification: dict[str, str] | None = None,
|
||||
) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
plugin_author='author',
|
||||
@@ -67,7 +78,10 @@ def plugin_setting(
|
||||
priority=0,
|
||||
created_at=datetime.datetime(2026, 1, 1),
|
||||
install_source='local',
|
||||
install_info={'_artifact_storage': 'tenant_binary_storage_v1'} if durable else {},
|
||||
install_info={
|
||||
**({'_artifact_storage': 'tenant_binary_storage_v1'} if durable else {}),
|
||||
**({'_certification': certification} if certification is not None else {}),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -163,6 +177,43 @@ async def test_shared_reconnect_replays_two_workspaces_and_removes_missing_proje
|
||||
assert set(connector._known_desired_states) == {setting_a.installation_uuid}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconcile_reload_projects_certified_exact_artifact_to_shared_execution():
|
||||
binding = execution_binding('workspace-a')
|
||||
digest = 'a' * 64
|
||||
setting = plugin_setting(
|
||||
'01',
|
||||
digest,
|
||||
certification={
|
||||
'artifact_digest': digest,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
},
|
||||
)
|
||||
connector = shared_connector([[binding]], {'workspace-a': [setting]})
|
||||
connector.handler = runtime_handler()
|
||||
|
||||
await connector._prepare_connected_runtime()
|
||||
|
||||
desired = connector.handler.reconcile_plugin_installations.await_args.args[0][0]
|
||||
assert desired.execution_mode is PluginExecutionMode.SHARED_CERTIFIED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconcile_reload_defaults_legacy_installation_to_dedicated_execution():
|
||||
binding = execution_binding('workspace-a')
|
||||
setting = plugin_setting('01', 'a' * 64)
|
||||
connector = shared_connector([[binding]], {'workspace-a': [setting]})
|
||||
connector.handler = runtime_handler()
|
||||
|
||||
await connector._prepare_connected_runtime()
|
||||
|
||||
desired = connector.handler.reconcile_plugin_installations.await_args.args[0][0]
|
||||
assert desired.execution_mode is PluginExecutionMode.DEDICATED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_projected_workspaces_do_not_retain_installation_sets():
|
||||
binding_a = execution_binding('workspace-a')
|
||||
@@ -223,6 +274,7 @@ async def test_fresh_shared_runtime_cache_replays_persisted_local_package():
|
||||
desired.binding,
|
||||
artifact_package=package,
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.DEDICATED,
|
||||
)
|
||||
|
||||
|
||||
@@ -304,6 +356,7 @@ async def test_local_install_persists_verified_package_before_runtime_apply():
|
||||
binding,
|
||||
artifact_package=package,
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.DEDICATED,
|
||||
)
|
||||
|
||||
|
||||
@@ -396,6 +449,9 @@ async def test_marketplace_upgrade_reports_multistep_progress():
|
||||
'download_current': 0,
|
||||
'download_speed': 0,
|
||||
}
|
||||
assert connector.handler.apply_plugin_installation.await_args.kwargs['execution_mode'] is (
|
||||
PluginExecutionMode.SHARED_CERTIFIED
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -430,7 +486,13 @@ async def test_workspace_reads_do_not_wait_for_an_installation_apply():
|
||||
connector._persist_installation_package = AsyncMock(return_value=(binding, None, False))
|
||||
connector._wait_for_installed_plugin_ready = AsyncMock()
|
||||
connector._load_workspace_desired_states = AsyncMock(
|
||||
return_value=[PluginInstallationDesiredState(binding=binding, enabled=True)]
|
||||
return_value=[
|
||||
PluginInstallationDesiredState(
|
||||
binding=binding,
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.SHARED_CERTIFIED,
|
||||
)
|
||||
]
|
||||
)
|
||||
apply_started = asyncio.Event()
|
||||
release_apply = asyncio.Event()
|
||||
|
||||
@@ -10,7 +10,12 @@ from unittest.mock import AsyncMock, MagicMock, Mock
|
||||
import pytest
|
||||
|
||||
from langbot_plugin.entities.io.actions.enums import LangBotToRuntimeAction, PluginToRuntimeAction
|
||||
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding, PluginInstallationDesiredState
|
||||
from langbot_plugin.entities.io.context import (
|
||||
ActionContext,
|
||||
InstallationBinding,
|
||||
PluginExecutionMode,
|
||||
PluginInstallationDesiredState,
|
||||
)
|
||||
|
||||
|
||||
def make_handler(app):
|
||||
@@ -90,7 +95,25 @@ async def test_reconcile_plugin_installations_accepts_configured_cold_start_time
|
||||
|
||||
await runtime_handler.reconcile_plugin_installations((desired,), timeout=900)
|
||||
|
||||
assert runtime_handler.call_action.await_args.kwargs["timeout"] == 900
|
||||
assert runtime_handler.call_action.await_args.kwargs['timeout'] == 900
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_plugin_installation_serializes_certified_shared_execution_mode():
|
||||
runtime_handler = make_handler(SimpleNamespace())
|
||||
runtime_handler.send_file = AsyncMock(return_value='artifact-file')
|
||||
runtime_handler.call_action = AsyncMock(return_value={'state': 'starting'})
|
||||
binding = next(iter(runtime_handler._installation_bindings.values()))[0]
|
||||
|
||||
await runtime_handler.apply_plugin_installation(
|
||||
binding,
|
||||
artifact_package=b'package',
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.SHARED_CERTIFIED,
|
||||
)
|
||||
|
||||
assert runtime_handler.call_action.await_args.args[0] == LangBotToRuntimeAction.APPLY_PLUGIN_INSTALLATION
|
||||
assert runtime_handler.call_action.await_args.args[1]['execution_mode'] == 'shared-runtime-v1'
|
||||
|
||||
|
||||
class TestHandlerQueryVariables:
|
||||
|
||||
@@ -1066,7 +1066,7 @@ name = "cuda-bindings"
|
||||
version = "13.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-pathfinder" },
|
||||
{ name = "cuda-pathfinder", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" },
|
||||
@@ -1099,34 +1099,34 @@ wheels = [
|
||||
|
||||
[package.optional-dependencies]
|
||||
cudart = [
|
||||
{ name = "nvidia-cuda-runtime" },
|
||||
{ name = "nvidia-cuda-runtime", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cufft = [
|
||||
{ name = "nvidia-cufft" },
|
||||
{ name = "nvidia-cufft", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cufile = [
|
||||
{ name = "nvidia-cufile" },
|
||||
{ name = "nvidia-cufile", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cupti = [
|
||||
{ name = "nvidia-cuda-cupti" },
|
||||
{ name = "nvidia-cuda-cupti", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
curand = [
|
||||
{ name = "nvidia-curand" },
|
||||
{ name = "nvidia-curand", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cusolver = [
|
||||
{ name = "nvidia-cusolver" },
|
||||
{ name = "nvidia-cusolver", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cusparse = [
|
||||
{ name = "nvidia-cusparse" },
|
||||
{ name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
nvjitlink = [
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
nvrtc = [
|
||||
{ name = "nvidia-cuda-nvrtc" },
|
||||
{ name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
nvtx = [
|
||||
{ name = "nvidia-nvtx" },
|
||||
{ name = "nvidia-nvtx", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2185,7 +2185,7 @@ requires-dist = [
|
||||
{ name = "gewechat-client", specifier = ">=0.1.5" },
|
||||
{ name = "html2text", specifier = ">=2024.2.26" },
|
||||
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1" },
|
||||
{ name = "langbot-plugin", specifier = "==0.6.1" },
|
||||
{ name = "langbot-plugin", specifier = "==0.6.2" },
|
||||
{ name = "langchain", specifier = ">=1.3.9" },
|
||||
{ name = "langchain-core", specifier = ">=1.3.3" },
|
||||
{ name = "langchain-text-splitters", specifier = ">=1.1.2" },
|
||||
@@ -2255,7 +2255,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langbot-plugin"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
@@ -2276,9 +2276,9 @@ dependencies = [
|
||||
{ name = "watchdog" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/70/ade1e2e71e666a80acfffde0fc753173e74b7a528ef495a1705f53c40ea8/langbot_plugin-0.6.1.tar.gz", hash = "sha256:214e415c2cd3c286f4b07cfbbe11e6b865839596ad500c2100944b00b3ba5f2b", size = 625357, upload-time = "2026-09-24T14:55:08.202Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/df/376749b79b133a3f9e65975243c1b4a734f072cbdbb2a3f5d5add7dc800c/langbot_plugin-0.6.2.tar.gz", hash = "sha256:64dc0f47b625c3595028dadea3e500ee123cb0e5481c97fecd7f4308d703623c", size = 639489, upload-time = "2026-09-25T12:16:11.18Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/06/4d8a889b0c28dcbf7ccf5ca40bcc7125ac9639942f7d05340768b28d159c/langbot_plugin-0.6.1-py3-none-any.whl", hash = "sha256:5607b4c787a92259b4a1f7beb4bdca9f371e37b316eaf8f316d2448e888da77e", size = 413828, upload-time = "2026-09-24T14:55:06.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/6e/d7f725ff4c42e7092368d2651354a84d5f195baf4dfbd5c537979b6d50aa/langbot_plugin-0.6.2-py3-none-any.whl", hash = "sha256:65845f20e65e32b4daeffdf69119b35be8775ce902d9e050e38f2cf5d77e5203", size = 419610, upload-time = "2026-09-25T12:16:09.973Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3306,7 +3306,7 @@ name = "nvidia-cublas"
|
||||
version = "13.1.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cuda-nvrtc" },
|
||||
{ name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
|
||||
@@ -3345,7 +3345,7 @@ name = "nvidia-cudnn-cu13"
|
||||
version = "9.20.0.48"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas" },
|
||||
{ name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
|
||||
@@ -3357,7 +3357,7 @@ name = "nvidia-cufft"
|
||||
version = "12.0.0.61"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
|
||||
@@ -3387,9 +3387,9 @@ name = "nvidia-cusolver"
|
||||
version = "12.0.4.66"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas" },
|
||||
{ name = "nvidia-cusparse" },
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
|
||||
@@ -3401,7 +3401,7 @@ name = "nvidia-cusparse"
|
||||
version = "12.6.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
|
||||
@@ -4494,7 +4494,7 @@ name = "pylibseekdb"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pymysql" },
|
||||
{ name = "pymysql", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/a8/7413d33218aff55a14ec9d20532b49243ffd0579e7a92244922c1885444e/pylibseekdb-1.4.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5cb2efab9f1321cdb4b034d3a2bd92e41a402fc95e7dc9579c7473a426f96e24", size = 52173499, upload-time = "2026-08-27T13:05:09.347Z" },
|
||||
@@ -5262,10 +5262,10 @@ name = "scikit-learn"
|
||||
version = "1.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "joblib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scipy" },
|
||||
{ name = "threadpoolctl" },
|
||||
{ name = "joblib", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "scipy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "threadpoolctl", marker = "python_full_version >= '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" }
|
||||
wheels = [
|
||||
@@ -5312,7 +5312,7 @@ name = "scipy"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
|
||||
wheels = [
|
||||
@@ -5383,14 +5383,14 @@ name = "sentence-transformers"
|
||||
version = "5.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scikit-learn" },
|
||||
{ name = "scipy" },
|
||||
{ name = "torch" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "transformers" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "huggingface-hub", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "scikit-learn", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "scipy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "torch", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "tqdm", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "transformers", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/30/21664028fc0776eb1ca024879480bbbab36f02923a8ff9e4cae5a150fa35/sentence_transformers-5.2.3.tar.gz", hash = "sha256:3cd3044e1f3fe859b6a1b66336aac502eaae5d3dd7d5c8fc237f37fbf58137c7", size = 381623, upload-time = "2026-02-17T14:05:20.238Z" }
|
||||
wheels = [
|
||||
@@ -5772,21 +5772,21 @@ name = "torch"
|
||||
version = "2.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-bindings", marker = "sys_platform == 'linux'" },
|
||||
{ name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" },
|
||||
{ name = "filelock" },
|
||||
{ name = "fsspec" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "networkx" },
|
||||
{ name = "nvidia-cublas", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "sympy" },
|
||||
{ name = "triton", marker = "sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "cuda-bindings", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "filelock", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "fsspec", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "jinja2", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "networkx", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cudnn-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparselt-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nccl-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvshmem-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "sympy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "triton", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.14'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/38/7028d3be540f1dcdf41660a2b01d0c51d2cb73915fe370d84e4d277a6d47/torch-2.12.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ef81f503912effea2ce3d9b12a2e3a6ed488943e91271c90c7a829f60baf6aa2", size = 87975425, upload-time = "2026-06-17T21:08:34.094Z" },
|
||||
@@ -5828,15 +5828,15 @@ name = "transformers"
|
||||
version = "5.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "regex" },
|
||||
{ name = "safetensors" },
|
||||
{ name = "tokenizers" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typer" },
|
||||
{ name = "huggingface-hub", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "packaging", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "pyyaml", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "regex", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "safetensors", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "tokenizers", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "tqdm", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "typer", marker = "python_full_version >= '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/1a/70e830d53ecc96ce69cfa8de38f163712d2b43ac52fbd743f39f56025c31/transformers-5.3.0.tar.gz", hash = "sha256:009555b364029da9e2946d41f1c5de9f15e6b1df46b189b7293f33a161b9c557", size = 8830831, upload-time = "2026-03-04T17:41:46.119Z" }
|
||||
wheels = [
|
||||
@@ -6097,9 +6097,9 @@ name = "valkey-glide"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "anyio", marker = "sys_platform != 'win32'" },
|
||||
{ name = "protobuf", marker = "sys_platform != 'win32'" },
|
||||
{ name = "sniffio", marker = "sys_platform != 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/a2/582b34c6acc8dc857c537f6007459cba48dfa0dc404789a657e5c1a998c0/valkey_glide-2.4.1.tar.gz", hash = "sha256:f1155d84156d11b90488aa67e90102f0bf98a45314f5b99308ac9074c05f7241", size = 898030, upload-time = "2026-05-28T21:41:55.881Z" }
|
||||
wheels = [
|
||||
|
||||
Reference in New Issue
Block a user