feat: implement transport abstractions for virtual environments and add E2B environment provider

This commit is contained in:
Yeuoly 2025-12-31 17:51:38 +08:00
parent 29dc083d8d
commit c9610e9949
10 changed files with 518 additions and 41 deletions

View File

@ -4,7 +4,7 @@ from io import BytesIO
from typing import Any
from core.virtual_environment.__base.entities import CommandStatus, ConnectionHandle, FileState, Metadata
from core.virtual_environment.channel.transport import Transport
from core.virtual_environment.channel.transport import TransportReadCloser, TransportWriteCloser
class VirtualEnvironment(ABC):
@ -12,7 +12,7 @@ class VirtualEnvironment(ABC):
Base class for virtual environment implementations.
"""
def __init__(self, options: Mapping[str, Any], environments: Mapping[str, Any] | None = None) -> None:
def __init__(self, options: Mapping[str, Any], environments: Mapping[str, str] | None = None) -> None:
"""
Initialize the virtual environment with metadata.
"""
@ -21,7 +21,7 @@ class VirtualEnvironment(ABC):
self.metadata = self.construct_environment(options, environments or {})
@abstractmethod
def construct_environment(self, options: Mapping[str, Any], environments: Mapping[str, Any]) -> Metadata:
def construct_environment(self, options: Mapping[str, Any], environments: Mapping[str, str]) -> Metadata:
"""
Construct the unique identifier for the virtual environment.
@ -118,8 +118,8 @@ class VirtualEnvironment(ABC):
@abstractmethod
def execute_command(
self, connection_handle: ConnectionHandle, command: list[str]
) -> tuple[str, Transport, Transport, Transport]:
self, connection_handle: ConnectionHandle, command: list[str], environments: Mapping[str, str] | None = None
) -> tuple[str, TransportWriteCloser, TransportReadCloser, TransportReadCloser]:
"""
Execute a command in the virtual environment.
@ -128,8 +128,8 @@ class VirtualEnvironment(ABC):
command (list[str]): The command to execute as a list of strings.
Returns:
tuple[int, Transport, Transport, Transport]: A tuple containing pid and 3 handle
to os.pipe(): (stdin, stdout, stderr).
tuple[int, TransportWriteCloser, TransportReadCloser, TransportReadCloser]
a tuple containing pid and 3 handle to os.pipe(): (stdin, stdout, stderr).
After exuection, the 3 handles will be closed by caller.
"""

View File

@ -1,6 +1,6 @@
import os
from core.virtual_environment.channel.transport import Transport
from core.virtual_environment.channel.transport import Transport, TransportReadCloser, TransportWriteCloser
class PipeTransport(Transport):
@ -26,3 +26,33 @@ class PipeTransport(Transport):
def close(self) -> None:
os.close(self.r_fd)
os.close(self.w_fd)
class PipeReadCloser(TransportReadCloser):
"""
A Transport implementation using OS pipe for reading.
"""
def __init__(self, r_fd: int):
self.r_fd = r_fd
def read(self, n: int) -> bytes:
return os.read(self.r_fd, n)
def close(self) -> None:
os.close(self.r_fd)
class PipeWriteCloser(TransportWriteCloser):
"""
A Transport implementation using OS pipe for writing.
"""
def __init__(self, w_fd: int):
self.w_fd = w_fd
def write(self, data: bytes) -> None:
os.write(self.w_fd, data)
def close(self) -> None:
os.close(self.w_fd)

View File

@ -0,0 +1,66 @@
from queue import Queue
from core.virtual_environment.channel.transport import TransportReadCloser
class QueueTransportReadCloser(TransportReadCloser):
"""
Transport implementation using queues for inter-thread communication.
Usage:
q_transport = QueueTransportReadCloser()
write_handler = q_transport.get_write_handler()
# In writer thread
write_handler.write(b"data")
# In reader thread
data = q_transport.read(1024)
# Close transport when done
q_transport.close()
"""
class WriteHandler:
"""
A write handler that writes data to a queue.
"""
def __init__(self, queue: Queue[bytes | None]) -> None:
self.queue = queue
def write(self, data: bytes) -> None:
self.queue.put(data)
def __init__(
self,
) -> None:
"""
Initialize the QueueTransportReadCloser with write function.
"""
self.q = Queue[bytes | None]()
def get_write_handler(self) -> WriteHandler:
"""
Get a write handler that writes to the internal queue.
"""
return QueueTransportReadCloser.WriteHandler(self.q)
def close(self) -> None:
"""
Close the transport by putting a sentinel value in the queue.
"""
self.q.put(None)
def read(self, n: int) -> bytes:
"""
Read up to n bytes from the queue.
"""
data = bytearray()
while len(data) < n:
chunk = self.q.get()
if chunk is None:
break
data.extend(chunk)
return bytes(data)

View File

@ -1,6 +1,6 @@
import socket
from core.virtual_environment.channel.transport import Transport
from core.virtual_environment.channel.transport import Transport, TransportReadCloser, TransportWriteCloser
class SocketTransport(Transport):
@ -19,3 +19,33 @@ class SocketTransport(Transport):
def close(self) -> None:
self.sock.close()
class SocketReadCloser(TransportReadCloser):
"""
A Transport implementation using a socket for reading.
"""
def __init__(self, sock: socket.SocketIO):
self.sock = sock
def read(self, n: int) -> bytes:
return self.sock.read(n)
def close(self) -> None:
self.sock.close()
class SocketWriteCloser(TransportWriteCloser):
"""
A Transport implementation using a socket for writing.
"""
def __init__(self, sock: socket.SocketIO):
self.sock = sock
def write(self, data: bytes) -> None:
self.sock.write(data)
def close(self) -> None:
self.sock.close()

View File

@ -2,24 +2,75 @@ from abc import abstractmethod
from typing import Protocol
class Transport(Protocol):
@abstractmethod
def write(self, data: bytes) -> None:
"""
Write data to the transport.
"""
pass
@abstractmethod
def read(self, n: int) -> bytes:
"""
Read up to n bytes from the transport.
"""
pass
class TransportCloser(Protocol):
"""
Transport that can be closed.
"""
@abstractmethod
def close(self) -> None:
"""
Close the transport.
"""
class TransportWriter(Protocol):
"""
Transport that can be written to.
"""
@abstractmethod
def write(self, data: bytes) -> None:
"""
Write data to the transport.
"""
class TransportReader(Protocol):
"""
Transport that can be read from.
"""
@abstractmethod
def read(self, n: int) -> bytes:
"""
Read up to n bytes from the transport.
"""
class TransportReadCloser(TransportReader, TransportCloser):
"""
Transport that can be read from and closed.
"""
class TransportWriteCloser(TransportWriter, TransportCloser):
"""
Transport that can be written to and closed.
"""
class Transport(TransportReader, TransportWriter, TransportCloser):
"""
Transport that can be read from, written to, and closed.
"""
class NopTransportWriteCloser(TransportWriteCloser):
"""
A no-operation TransportWriteCloser implementation.
This transport does nothing on write and close operations.
"""
def write(self, data: bytes) -> None:
"""
No-operation write method.
"""
pass
def close(self) -> None:
"""
No-operation close method.
"""
pass

View File

@ -15,8 +15,8 @@ import docker
from core.virtual_environment.__base.entities import Arch, CommandStatus, ConnectionHandle, FileState, Metadata
from core.virtual_environment.__base.exec import VirtualEnvironmentLaunchFailedError
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
from core.virtual_environment.channel.socket_transport import SocketTransport
from core.virtual_environment.channel.transport import Transport
from core.virtual_environment.channel.socket_transport import SocketReadCloser, SocketWriteCloser
from core.virtual_environment.channel.transport import TransportReadCloser, TransportWriteCloser
"""
EXAMPLE:
@ -69,7 +69,7 @@ class DockerDaemonEnvironment(VirtualEnvironment):
DOCKER_IMAGE = "docker_image"
DOCKER_COMMAND = "docker_command"
def construct_environment(self, options: Mapping[str, Any], environments: Mapping[str, Any]) -> Metadata:
def construct_environment(self, options: Mapping[str, Any], environments: Mapping[str, str]) -> Metadata:
"""
Construct the Docker daemon virtual environment.
"""
@ -253,7 +253,7 @@ class DockerDaemonEnvironment(VirtualEnvironment):
def execute_command(
self, connection_handle: ConnectionHandle, command: list[str], environments: Mapping[str, str] | None = None
) -> tuple[str, Transport, Transport, Transport]:
) -> tuple[str, TransportWriteCloser, TransportReadCloser, TransportReadCloser]:
container = self._get_container()
container_id = container.id
if not isinstance(container_id, str) or not container_id:
@ -279,8 +279,10 @@ class DockerDaemonEnvironment(VirtualEnvironment):
exec_id: str = str(exec_info.get("Id"))
raw_sock: socket.SocketIO = cast(socket.SocketIO, api_client.exec_start(exec_id, socket=True, tty=False)) # pyright: ignore[reportUnknownMemberType] #
transport = SocketTransport(raw_sock)
return exec_id, transport, transport, transport
stdin_transport = SocketWriteCloser(raw_sock)
stdout_transport = SocketReadCloser(raw_sock)
return exec_id, stdin_transport, stdout_transport, stdout_transport
def get_command_status(self, connection_handle: ConnectionHandle, pid: str) -> CommandStatus:
api_client = self.get_docker_api_client(self.get_docker_sock())

View File

@ -0,0 +1,230 @@
import os
import threading
from collections.abc import Mapping, Sequence
from enum import StrEnum
from functools import cached_property
from io import BytesIO
from typing import Any
from uuid import uuid4
from e2b_code_interpreter import Sandbox
from core.virtual_environment.__base.entities import Arch, CommandStatus, ConnectionHandle, FileState, Metadata
from core.virtual_environment.__base.exec import ArchNotSupportedError
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
from core.virtual_environment.channel.queue_transport import QueueTransportReadCloser
from core.virtual_environment.channel.transport import (
NopTransportWriteCloser,
TransportReadCloser,
TransportWriteCloser,
)
"""
import logging
from collections.abc import Mapping
from typing import Any
from core.virtual_environment.providers.e2b_sandbox import E2BEnvironment
options: Mapping[str, Any] = {E2BEnvironment.OptionsKey.API_KEY: "?????????"}
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
# environment = DockerDaemonEnvironment(options=options)
# environment = LocalVirtualEnvironment(options=options)
environment = E2BEnvironment(options=options)
connection_handle = environment.establish_connection()
pid, transport_stdin, transport_stdout, transport_stderr = environment.execute_command(
connection_handle, ["uname", "-a"]
)
logger.info("Executed command with PID: %s", pid)
# consume stdout
output = transport_stdout.read(1024)
logger.info("Command output: %s", output.decode().strip())
environment.release_connection(connection_handle)
environment.release_environment()
"""
class E2BEnvironment(VirtualEnvironment):
"""
E2B virtual environment provider.
"""
_WORKDIR = "/home/user"
class OptionsKey(StrEnum):
API_KEY = "api_key"
E2B_LIST_FILE_DEPTH = "e2b_list_file_depth"
E2B_DEFAULT_TEMPLATE = "code-interpreter-v1"
class StoreKey(StrEnum):
SANDBOX = "sandbox"
def construct_environment(self, options: Mapping[str, Any], environments: Mapping[str, str]) -> Metadata:
"""
Construct a new E2B virtual environment.
"""
# TODO: add Dify as the user agent
sandbox = Sandbox.create(
template=options.get(self.OptionsKey.E2B_DEFAULT_TEMPLATE, "code-interpreter-v1"),
api_key=options.get(self.OptionsKey.API_KEY, ""),
)
info = sandbox.get_info(api_key=options.get(self.OptionsKey.API_KEY, ""))
output = sandbox.commands.run("uname -m").stdout.strip()
return Metadata(
id=info.sandbox_id,
arch=self._convert_architecture(output),
store={
self.StoreKey.SANDBOX: sandbox,
},
)
def release_environment(self) -> None:
"""
Release the E2B virtual environment.
"""
if not Sandbox.kill(api_key=self.api_key, sandbox_id=self.metadata.id):
raise Exception(f"Failed to release E2B sandbox with ID: {self.metadata.id}")
def establish_connection(self) -> ConnectionHandle:
"""
Establish a connection to the E2B virtual environment.
"""
return ConnectionHandle(id=uuid4().hex)
def release_connection(self, connection_handle: ConnectionHandle) -> None:
"""
Release the connection to the E2B virtual environment.
"""
pass
def upload_file(self, path: str, content: BytesIO) -> None:
"""
Upload a file to the E2B virtual environment.
Args:
path (str): The path to upload the file to.
content (BytesIO): The content of the file.
"""
path = os.path.join(self._WORKDIR, path.lstrip("/"))
sandbox: Sandbox = self.metadata.store[self.StoreKey.SANDBOX]
sandbox.files.write(path, content) # pyright: ignore[reportUnknownMemberType] #
def download_file(self, path: str) -> BytesIO:
"""
Download a file from the E2B virtual environment.
Args:
path (str): The path to download the file from.
Returns:
BytesIO: The content of the file.
"""
path = os.path.join(self._WORKDIR, path.lstrip("/"))
sandbox: Sandbox = self.metadata.store[self.StoreKey.SANDBOX]
content = sandbox.files.read(path)
return BytesIO(content.encode())
def list_files(self, directory_path: str, limit: int) -> Sequence[FileState]:
"""
List files in a directory of the E2B virtual environment.
"""
sandbox: Sandbox = self.metadata.store[self.StoreKey.SANDBOX]
directory_path = os.path.join(self._WORKDIR, directory_path.lstrip("/"))
files_info = sandbox.files.list(directory_path, depth=self.options.get(self.OptionsKey.E2B_LIST_FILE_DEPTH, 3))
return [
FileState(
path=os.path.relpath(file_info.path, self._WORKDIR),
size=file_info.size,
created_at=int(file_info.modified_time.timestamp()),
updated_at=int(file_info.modified_time.timestamp()),
)
for file_info in files_info
]
def execute_command(
self, connection_handle: ConnectionHandle, command: list[str], environments: Mapping[str, str] | None = None
) -> tuple[str, TransportWriteCloser, TransportReadCloser, TransportReadCloser]:
"""
Execute a command in the E2B virtual environment.
STDIN is not yet supported. E2B's API is such a terrible mess... to support it may lead a bad design.
as a result we leave it for future improvement.
"""
sandbox: Sandbox = self.metadata.store[self.StoreKey.SANDBOX]
stdout_stream = QueueTransportReadCloser()
stderr_stream = QueueTransportReadCloser()
threading.Thread(
target=self._cmd_thread,
args=(sandbox, command, environments, stdout_stream, stderr_stream),
).start()
return (
"N/A",
NopTransportWriteCloser(), # stdin not supported yet
stdout_stream,
stderr_stream,
)
def get_command_status(self, connection_handle: ConnectionHandle, pid: str) -> CommandStatus:
return super().get_command_status(connection_handle, pid)
def _cmd_thread(
self,
sandbox: Sandbox,
command: list[str],
environments: Mapping[str, str] | None,
stdout_stream: QueueTransportReadCloser,
stderr_stream: QueueTransportReadCloser,
) -> None:
""" """
stdout_stream_write_handler = stdout_stream.get_write_handler()
stderr_stream_write_handler = stderr_stream.get_write_handler()
sandbox.commands.run(
cmd=" ".join(command),
envs=dict(environments or {}),
# stdin=True,
on_stdout=lambda data: stdout_stream_write_handler.write(data.encode()),
on_stderr=lambda data: stderr_stream_write_handler.write(data.encode()),
)
# Close the write handlers to signal EOF
stdout_stream.close()
stderr_stream.close()
@cached_property
def api_key(self) -> str:
"""
Get the API key for the E2B environment.
"""
return self.options.get(self.OptionsKey.API_KEY, "")
def _convert_architecture(self, arch_str: str) -> Arch:
"""
Convert architecture string to standard format.
"""
arch_map = {
"x86_64": Arch.AMD64,
"aarch64": Arch.ARM64,
"armv7l": Arch.ARM64,
"arm64": Arch.ARM64,
"amd64": Arch.AMD64,
"arm64v8": Arch.ARM64,
"arm64v7": Arch.ARM64,
}
if arch_str in arch_map:
return arch_map[arch_str]
raise ArchNotSupportedError(f"Unsupported architecture: {arch_str}")

View File

@ -11,8 +11,8 @@ from uuid import uuid4
from core.virtual_environment.__base.entities import Arch, CommandStatus, ConnectionHandle, FileState, Metadata
from core.virtual_environment.__base.exec import ArchNotSupportedError
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
from core.virtual_environment.channel.pipe_transport import PipeTransport
from core.virtual_environment.channel.transport import Transport
from core.virtual_environment.channel.pipe_transport import PipeReadCloser, PipeWriteCloser
from core.virtual_environment.channel.transport import TransportReadCloser, TransportWriteCloser
class LocalVirtualEnvironment(VirtualEnvironment):
@ -23,7 +23,7 @@ class LocalVirtualEnvironment(VirtualEnvironment):
NEVER USE IT IN PRODUCTION ENVIRONMENTS.
"""
def construct_environment(self, options: Mapping[str, Any], environments: Mapping[str, Any]) -> Metadata:
def construct_environment(self, options: Mapping[str, Any], environments: Mapping[str, str]) -> Metadata:
"""
Construct the local virtual environment.
@ -118,7 +118,7 @@ class LocalVirtualEnvironment(VirtualEnvironment):
def execute_command(
self, connection_handle: ConnectionHandle, command: list[str], environments: Mapping[str, str] | None = None
) -> tuple[str, Transport, Transport, Transport]:
) -> tuple[str, TransportWriteCloser, TransportReadCloser, TransportReadCloser]:
"""
Execute a command in the local virtual environment.
@ -162,9 +162,9 @@ class LocalVirtualEnvironment(VirtualEnvironment):
os.close(stderr_write_fd)
# Create PipeTransport instances for stdin, stdout, and stderr
stdin_transport = PipeTransport(r_fd=stdin_read_fd, w_fd=stdin_write_fd)
stdout_transport = PipeTransport(r_fd=stdout_read_fd, w_fd=stdout_write_fd)
stderr_transport = PipeTransport(r_fd=stderr_read_fd, w_fd=stderr_write_fd)
stdin_transport = PipeWriteCloser(w_fd=stdin_write_fd)
stdout_transport = PipeReadCloser(r_fd=stdout_read_fd)
stderr_transport = PipeReadCloser(r_fd=stderr_read_fd)
# Return the process ID and file descriptors for stdin, stdout, and stderr
return str(process.pid), stdin_transport, stdout_transport, stderr_transport

View File

@ -88,12 +88,13 @@ dependencies = [
"httpx-sse~=0.4.0",
"sendgrid~=6.12.3",
"flask-restx~=1.3.0",
"packaging~=23.2",
"packaging==24.1",
"croniter>=6.0.0",
"weaviate-client==4.17.0",
"apscheduler>=3.11.0",
"weave>=0.52.16",
"docker>=7.1.0",
"e2b-code-interpreter>=2.4.1",
]
# Before adding new dependency, consider place it in
# alphabet order (a-z) and suitable group.

75
api/uv.lock generated
View File

@ -685,6 +685,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/ad/d71da675eef85ac153eef5111ca0caa924548c9591da00939bcabba8de8e/bottleneck-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:81e3822499f057a917b7d3972ebc631ac63c6bbcc79ad3542a66c4c40634e3a6", size = 113493 },
]
[[package]]
name = "bracex"
version = "2.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508 },
]
[[package]]
name = "brotli"
version = "1.2.0"
@ -1383,6 +1392,7 @@ dependencies = [
{ name = "charset-normalizer" },
{ name = "croniter" },
{ name = "docker" },
{ name = "e2b-code-interpreter" },
{ name = "flask" },
{ name = "flask-compress" },
{ name = "flask-cors" },
@ -1582,6 +1592,7 @@ requires-dist = [
{ name = "charset-normalizer", specifier = ">=3.4.4" },
{ name = "croniter", specifier = ">=6.0.0" },
{ name = "docker", specifier = ">=7.1.0" },
{ name = "e2b-code-interpreter", specifier = ">=2.4.1" },
{ name = "flask", specifier = "~=3.1.2" },
{ name = "flask-compress", specifier = ">=1.17,<1.18" },
{ name = "flask-cors", specifier = "~=6.0.0" },
@ -1629,7 +1640,7 @@ requires-dist = [
{ name = "opentelemetry-semantic-conventions", specifier = "==0.48b0" },
{ name = "opentelemetry-util-http", specifier = "==0.48b0" },
{ name = "opik", specifier = "~=1.8.72" },
{ name = "packaging", specifier = "~=23.2" },
{ name = "packaging", specifier = "==24.1" },
{ name = "pandas", extras = ["excel", "output-formatting", "performance"], specifier = "~=2.2.2" },
{ name = "psycogreen", specifier = "~=1.0.2" },
{ name = "psycopg2-binary", specifier = "~=2.9.6" },
@ -1799,6 +1810,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 },
]
[[package]]
name = "dockerfile-parse"
version = "2.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/92/df/929ee0b5d2c8bd8d713c45e71b94ab57c7e11e322130724d54f469b2cd48/dockerfile-parse-2.0.1.tar.gz", hash = "sha256:3184ccdc513221983e503ac00e1aa504a2aa8f84e5de673c46b0b6eee99ec7bc", size = 24556 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/6c/79cd5bc1b880d8c1a9a5550aa8dacd57353fa3bb2457227e1fb47383eb49/dockerfile_parse-2.0.1-py2.py3-none-any.whl", hash = "sha256:bdffd126d2eb26acf1066acb54cb2e336682e1d72b974a40894fac76a4df17f6", size = 14845 },
]
[[package]]
name = "docstring-parser"
version = "0.17.0"
@ -1833,6 +1853,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922 },
]
[[package]]
name = "e2b"
version = "2.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "dockerfile-parse" },
{ name = "httpcore" },
{ name = "httpx" },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "python-dateutil" },
{ name = "rich" },
{ name = "typing-extensions" },
{ name = "wcmatch" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/d6/bf09e2a2f81a0296dc20b59a53b3cb8e019aaf6a734a3708b711cfd0ba48/e2b-2.9.0.tar.gz", hash = "sha256:b6b0d7dc816e9e0f6ca82ddd4add8e86d72068fb79d9430a07167ab773a822fa", size = 111618 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/a3/cd58df6423356e967b1b5b26d2b3adb2d001b47d04c4e5499778ef88c23f/e2b-2.9.0-py3-none-any.whl", hash = "sha256:216752c7be17630721ad60a023f488ad3a2a1c995eaf4c5c7d0d1ec6f9129742", size = 204021 },
]
[[package]]
name = "e2b-code-interpreter"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "e2b" },
{ name = "httpx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/eb/db6e51edd9f3402fd68d026572579b9b1bd833b10d990376a1e4c05d5b8d/e2b_code_interpreter-2.4.1.tar.gz", hash = "sha256:4b15014ee0d0dfcdc3072e1f409cbb87ca48f48d53d75629b7257e5513b9e7dd", size = 10700 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/e7/09b9106ead227f7be14bd97c3181391ee498bb38933b1a9c566b72c8567a/e2b_code_interpreter-2.4.1-py3-none-any.whl", hash = "sha256:15d35f025b4a15033e119f2e12e7ac65657ad2b5a013fa9149e74581fbee778a", size = 13719 },
]
[[package]]
name = "elastic-transport"
version = "8.17.1"
@ -4319,11 +4374,11 @@ wheels = [
[[package]]
name = "packaging"
version = "23.2"
version = "24.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", size = 146714 }
sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7", size = 53011 },
{ url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
]
[[package]]
@ -7018,6 +7073,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546 },
]
[[package]]
name = "wcmatch"
version = "10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bracex" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854 },
]
[[package]]
name = "wcwidth"
version = "0.2.14"