Skip to content

Commit

Permalink
update the python-sdk sync and async write method with new spec
Browse files Browse the repository at this point in the history
  • Loading branch information
0div committed Dec 12, 2024
1 parent 38f7c8b commit 1f1e8aa
Show file tree
Hide file tree
Showing 17 changed files with 164 additions and 94 deletions.
13 changes: 12 additions & 1 deletion packages/python-sdk/e2b/sandbox/filesystem/filesystem.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from enum import Enum
from dataclasses import dataclass
from typing import Optional
from typing import Optional, Union, IO

from e2b.envd.filesystem import filesystem_pb2

Expand Down Expand Up @@ -45,3 +45,14 @@ class EntryInfo:
"""
Path to the filesystem object.
"""

WriteData = Union[str, bytes, IO]

@dataclass
class WriteEntry:
"""
Contains path and data of the file to be written to the filesystem.
"""

path: str
data: WriteData
53 changes: 29 additions & 24 deletions packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from io import TextIOBase
from typing import IO, AsyncIterator, List, Literal, Optional, Union, overload
from e2b.sandbox.filesystem.filesystem import WriteEntry, EntryInfo

import e2b_connect as connect
import httpcore
Expand Down Expand Up @@ -131,48 +132,52 @@ async def read(

async def write(
self,
path: str,
data: Union[str, bytes, IO],
user: Username = "user",
files: List[WriteEntry],
user: Optional[Username] = "user",
request_timeout: Optional[float] = None,
) -> EntryInfo:
) -> List[EntryInfo]:
"""
Write content to a file on the path.
Writing to a file that doesn't exist creates the file.
Writing to a file that already exists overwrites the file.
Write content to a file(s).
Writing to a file at path that doesn't exist creates the necessary directories.
:param path: Path to the file
:param data: Data to write to the file, can be a `str`, `bytes`, or `IO`.
:param files: list of files to write
:param user: Run the operation as this user
:param request_timeout: Timeout for the request in **seconds**
:return: Information about the written file
:param request_timeout: Timeout for the request
:return: Information about the written files
"""
if isinstance(data, TextIOBase):
data = data.read().encode()

# Prepare the files for the multipart/form-data request
httpx_files = []
for file in files:
file_path, file_data = file['path'], file['data']
if isinstance(file_data, str) or isinstance(file_data, bytes):
httpx_files.append(('file', (file_path, file_data)))
elif isinstance(file_data, TextIOBase):
httpx_files.append(('file', (file_path, file_data.read())))
else:
raise ValueError(f"Unsupported data type for file {file_path}")

# Allow passing empty list of files
if len(httpx_files) == 0: return []

params = {"username": user}

r = await self._envd_api.post(
ENVD_API_FILES_ROUTE,
files={"file": data},
params={"path": path, "username": user},
files=httpx_files,
params=params,
timeout=self._connection_config.get_request_timeout(request_timeout),
)

err = await ahandle_envd_api_exception(r)
if err:
raise err

files = r.json()
write_files = r.json()

if not isinstance(files, list) or len(files) == 0:
if not isinstance(write_files, list) or len(write_files) == 0:
raise Exception("Expected to receive information about written file")

file = files[0]
return EntryInfo(**file)
return [EntryInfo(**file) for file in write_files]

async def list(
self,
Expand Down
57 changes: 30 additions & 27 deletions packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from io import TextIOBase
from typing import IO, Iterator, List, Literal, Optional, Union, overload
from typing import Iterator, List, Literal, Optional, overload

import e2b_connect
import httpcore
Expand All @@ -13,10 +13,9 @@
from e2b.envd.api import ENVD_API_FILES_ROUTE, handle_envd_api_exception
from e2b.envd.filesystem import filesystem_connect, filesystem_pb2
from e2b.envd.rpc import authentication_header, handle_rpc_exception
from e2b.sandbox.filesystem.filesystem import EntryInfo, map_file_type
from e2b.sandbox.filesystem.filesystem import EntryInfo, map_file_type, WriteEntry
from e2b.sandbox_sync.filesystem.watch_handle import WatchHandle


class Filesystem:
"""
Module for interacting with the filesystem in the sandbox.
Expand Down Expand Up @@ -128,48 +127,52 @@ def read(

def write(
self,
path: str,
data: Union[str, bytes, IO],
user: Username = "user",
files: List[WriteEntry],
user: Optional[Username] = "user",
request_timeout: Optional[float] = None,
) -> EntryInfo:
) -> List[EntryInfo]:
"""
Write content to a file on the path.
Writing to a file that doesn't exist creates the file.
Writing to a file that already exists overwrites the file.
Write content to a file(s).
Writing to a file at path that doesn't exist creates the necessary directories.
:param path: Path to the file
:param data: Data to write to the file, can be a `str`, `bytes`, or `IO`.
:param files: list of files to write
:param user: Run the operation as this user
:param request_timeout: Timeout for the request in **seconds**
:return: Information about the written file
:param request_timeout: Timeout for the request
:return: Information about the written files
"""
if isinstance(data, TextIOBase):
data = data.read().encode()

# Prepare the files for the multipart/form-data request
httpx_files = []
for file in files:
file_path, file_data = file['path'], file['data']
if isinstance(file_data, str) or isinstance(file_data, bytes):
httpx_files.append(('file', (file_path, file_data)))
elif isinstance(file_data, TextIOBase):
httpx_files.append(('file', (file_path, file_data.read())))
else:
raise ValueError(f"Unsupported data type for file {file_path}")

# Allow passing empty list of files
if len(httpx_files) == 0: return []

params = {"username": user}

r = self._envd_api.post(
ENVD_API_FILES_ROUTE,
files={"file": data},
params={"path": path, "username": user},
files=httpx_files,
params=params,
timeout=self._connection_config.get_request_timeout(request_timeout),
)

err = handle_envd_api_exception(r)
if err:
raise err

files = r.json()
write_files = r.json()

if not isinstance(files, list) or len(files) == 0:
if not isinstance(write_files, list) or len(write_files) == 0:
raise Exception("Expected to receive information about written file")

file = files[0]
return EntryInfo(**file)
return [EntryInfo(**file) for file in write_files]

def list(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
async def test_exists(async_sandbox: AsyncSandbox):
filename = "test_exists.txt"

await async_sandbox.files.write(filename, "test")
await async_sandbox.files.write([{ "path": filename, "data": "test" }])
assert await async_sandbox.files.exists(filename)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async def test_list_directory(async_sandbox: AsyncSandbox):
files = await async_sandbox.files.list(dir_name)
assert len(files) == 0

await async_sandbox.files.write(f"{dir_name}/test_file", "test")
await async_sandbox.files.write([{ "path": f"{dir_name}/test_file", "data": "test" }])
files1 = await async_sandbox.files.list(dir_name)
assert len(files1) == 1
assert files1[0].name == "test_file"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ async def test_read_file(async_sandbox: AsyncSandbox):
filename = "test_read.txt"
content = "Hello, world!"

await async_sandbox.files.write(filename, content)
await async_sandbox.files.write([{ "path": filename, "data": content }])
read_content = await async_sandbox.files.read(filename)
assert read_content == content

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ async def test_remove_file(async_sandbox: AsyncSandbox):
filename = "test_remove.txt"
content = "This file will be removed."

await async_sandbox.files.write(filename, content)
await async_sandbox.files.write([{ "path": filename, "data": content }])

await async_sandbox.files.remove(filename)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ async def test_rename_file(async_sandbox: AsyncSandbox):
new_filename = "test_rename_new.txt"
content = "This file will be renamed."

await async_sandbox.files.write(old_filename, content)
await async_sandbox.files.write([{ "path": old_filename, "data": content }])
info = await async_sandbox.files.rename(old_filename, new_filename)
assert info.path == f"/home/user/{new_filename}"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async def test_watch_directory_changes(async_sandbox: AsyncSandbox):
new_content = "This file has been modified."

await async_sandbox.files.make_dir(dirname)
await async_sandbox.files.write(f"{dirname}/{filename}", content)
await async_sandbox.files.write([{ "path": f"{dirname}/{filename}", "data": content }])

event_triggered = Event()

Expand All @@ -28,7 +28,7 @@ def handle_event(e: FilesystemEvent):

handle = await async_sandbox.files.watch_dir(dirname, on_event=handle_event)

await async_sandbox.files.write(f"{dirname}/{filename}", new_content)
await async_sandbox.files.write([{ "path": f"{dirname}/{filename}", "data": new_content }])

await event_triggered.wait()

Expand All @@ -44,7 +44,7 @@ async def test_watch_non_existing_directory(async_sandbox: AsyncSandbox):

async def test_watch_file(async_sandbox: AsyncSandbox):
filename = "test_watch.txt"
await async_sandbox.files.write(filename, "This file will be watched.")
await async_sandbox.files.write([{ "path": filename, "data": "This file will be watched." }])

with pytest.raises(SandboxException):
await async_sandbox.files.watch_dir(filename, on_event=lambda e: None)
49 changes: 37 additions & 12 deletions packages/python-sdk/tests/async/sandbox_async/files/test_write.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,52 @@
from e2b import AsyncSandbox


from e2b.sandbox.filesystem.filesystem import EntryInfo
async def test_write_file(async_sandbox: AsyncSandbox):
filename = "test_write.txt"
content = "This is a test file."
# Attempt to write with empty files array
empty_info = await async_sandbox.files.write([])
assert isinstance(empty_info, list)
assert len(empty_info) == 0

# Attempt to write with one file in array
info = await async_sandbox.files.write([{ "path": "one_test_file.txt", "data": "This is a test file." }])
assert isinstance(info, list)
assert len(info) == 1
info = info[0]
assert isinstance(info, EntryInfo)
assert info.path == "/home/user/one_test_file.txt"
exists = await async_sandbox.files.exists(info.path)
assert exists

info = await async_sandbox.files.write(filename, content)
assert info.path == f"/home/user/{filename}"
read_content = await async_sandbox.files.read(info.path)
assert read_content == "This is a test file."

exists = await async_sandbox.files.exists(filename)
assert exists
# Attempt to write with multiple files in array
files = []
for i in range(10):
path = f"test_write_{i}.txt"
content = f"This is a test file {i}."
files.append({"path": path, "data": content})

read_content = await async_sandbox.files.read(filename)
assert read_content == content
infos = await async_sandbox.files.write(files)
assert isinstance(infos, list)
assert len(infos) == len(files)
for i, info in enumerate(infos):
assert isinstance(info, EntryInfo)
assert info.path == f"/home/user/test_write_{i}.txt"
exists = await async_sandbox.files.exists(path)
assert exists

read_content = await async_sandbox.files.read(info.path)
assert read_content == files[i]["data"]


async def test_overwrite_file(async_sandbox: AsyncSandbox):
filename = "test_overwrite.txt"
initial_content = "Initial content."
new_content = "New content."

await async_sandbox.files.write(filename, initial_content)
await async_sandbox.files.write(filename, new_content)
await async_sandbox.files.write([{ "path": filename, "data": initial_content }])
await async_sandbox.files.write([{ "path": filename, "data": new_content }])
read_content = await async_sandbox.files.read(filename)
assert read_content == new_content

Expand All @@ -30,7 +55,7 @@ async def test_write_to_non_existing_directory(async_sandbox: AsyncSandbox):
filename = "non_existing_dir/test_write.txt"
content = "This should succeed too."

await async_sandbox.files.write(filename, content)
await async_sandbox.files.write([{ "path": filename, "data": content }])
exists = await async_sandbox.files.exists(filename)
assert exists

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
def test_exists(sandbox: Sandbox):
filename = "test_exists.txt"

sandbox.files.write(filename, "test")
sandbox.files.write([{ "path": filename, "data": "test" }])
assert sandbox.files.exists(filename)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def test_list_directory(sandbox: Sandbox):
files = sandbox.files.list(dir_name)
assert len(files) == 0

sandbox.files.write(f"{dir_name}/test_file", "test")
sandbox.files.write([{ "path": f"{dir_name}/test_file", "data": "test" }])
files1 = sandbox.files.list(dir_name)
assert len(files1) == 1
assert files1[0].name == "test_file"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def test_read_file(sandbox):
filename = "test_read.txt"
content = "Hello, world!"

sandbox.files.write(filename, content)
sandbox.files.write([{ "path": filename, "data": content }])
read_content = sandbox.files.read(filename)
assert read_content == content

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def test_remove_file(sandbox: Sandbox):
filename = "test_remove.txt"
content = "This file will be removed."

sandbox.files.write(filename, content)
sandbox.files.write([{ "path": filename, "data": content }])

sandbox.files.remove(filename)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def test_rename_file(sandbox: Sandbox):
new_filename = "test_rename_new.txt"
content = "This file will be renamed."

sandbox.files.write(old_filename, content)
sandbox.files.write([{ "path": old_filename, "data": content }])

info = sandbox.files.rename(old_filename, new_filename)
assert info.path == f"/home/user/{new_filename}"
Expand Down
Loading

0 comments on commit 1f1e8aa

Please sign in to comment.