Skip to content
This repository has been archived by the owner on Nov 3, 2024. It is now read-only.

Commit

Permalink
[HN-153/HN-155] feat: file management (#13)
Browse files Browse the repository at this point in the history
* [HN-153/HN-155] feat: file management

* update docs
  • Loading branch information
ToJen authored May 27, 2024
1 parent 590081e commit 9c10e04
Show file tree
Hide file tree
Showing 7 changed files with 498 additions and 27 deletions.
61 changes: 59 additions & 2 deletions hive_agent_client/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import httpx
import logging

from typing import Dict
from typing import Dict, List

from hive_agent_client.chat import send_chat_message
from hive_agent_client.database import (
Expand All @@ -11,6 +10,12 @@
update_data,
delete_data
)
from hive_agent_client.files import (
upload_files,
list_files,
delete_file,
rename_file
)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -115,6 +120,58 @@ async def delete_data(self, table_name: str, row_id: int) -> Dict:
logger.error(f"Failed to delete data from table {table_name} with id {row_id}: {e}")
raise Exception(f"Failed to delete data: {e}")

async def upload_files(self, file_paths: List[str]) -> Dict:
"""
Upload files to the server.
:param file_paths: A list of file paths to be uploaded.
:return: A dictionary with the names of the uploaded files.
"""
try:
return await upload_files(self.http_client, self.base_url, file_paths)
except Exception as e:
logger.error(f"Failed to upload files {file_paths}: {e}")
raise Exception(f"Failed to upload files: {e}")

async def list_files(self) -> Dict:
"""
List all files stored on the server.
:return: A dictionary with a list of file names.
"""
try:
return await list_files(self.http_client, self.base_url)
except Exception as e:
logger.error(f"Failed to list files: {e}")
raise Exception(f"Failed to list files: {e}")

async def delete_file(self, filename: str) -> Dict:
"""
Delete a specified file from the server.
:param filename: The name of the file to be deleted.
:return: A dictionary with a message about the file deletion.
"""
try:
return await delete_file(self.http_client, self.base_url, filename)
except Exception as e:
logger.error(f"Failed to delete file {filename}: {e}")
raise Exception(f"Failed to delete file: {e}")

async def rename_file(self, old_filename: str, new_filename: str) -> Dict:
"""
Rename a specified file on the server.
:param old_filename: The current name of the file.
:param new_filename: The new name for the file.
:return: A dictionary with a message about the file renaming.
"""
try:
return await rename_file(self.http_client, self.base_url, old_filename, new_filename)
except Exception as e:
logger.error(f"Failed to rename file from {old_filename} to {new_filename}: {e}")
raise Exception(f"Failed to rename file: {e}")

async def close(self):
"""
Close the HTTP client session.
Expand Down
6 changes: 6 additions & 0 deletions hive_agent_client/files/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .files import (
upload_files,
list_files,
delete_file,
rename_file
)
118 changes: 118 additions & 0 deletions hive_agent_client/files/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import httpx
import logging
import os
import sys

from typing import List


def get_log_level():
HIVE_AGENT_LOG_LEVEL = os.getenv('HIVE_AGENT_LOG_LEVEL', 'INFO').upper()
return getattr(logging, HIVE_AGENT_LOG_LEVEL, logging.INFO)


logging.basicConfig(stream=sys.stdout, level=get_log_level())
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

logger = logging.getLogger()
logger.setLevel(get_log_level())


async def upload_files(http_client: httpx.AsyncClient, base_url: str, file_paths: List[str]) -> dict:
"""
Uploads files to the server.
:param http_client: An HTTP client for sending requests.
:param base_url: The base URL of the Hive Agent API.
:param file_paths: A list of file paths to be uploaded.
:return: A dictionary with the names of the uploaded files.
:raises Exception: If the HTTP request fails or the API returns an error response.
"""
endpoint = "/uploadfiles/"
url = f"{base_url}{endpoint}"
files = [("files", (os.path.basename(file_path), open(file_path, "rb"), "multipart/form-data")) for file_path in file_paths]

try:
logger.debug(f"Uploading files to {url}")
response = await http_client.post(url, files=files)
response.raise_for_status()
logger.debug(f"Response for uploading files to {url}: {response.json()}")
return response.json()
except httpx.HTTPStatusError as e:
logging.error(f"Failed to upload files: {e}")
raise Exception(f"Failed to upload files: {e.response.text}") from e
finally:
for _, (name, file, _) in files:
file.close()


async def list_files(http_client: httpx.AsyncClient, base_url: str) -> dict:
"""
Lists all files stored on the server.
:param http_client: An HTTP client for sending requests.
:param base_url: The base URL of the Hive Agent API.
:return: A dictionary with a list of file names.
:raises Exception: If the HTTP request fails or the API returns an error response.
"""
endpoint = "/files/"
url = f"{base_url}{endpoint}"

try:
logger.debug(f"Listing files at {url}")
response = await http_client.get(url)
response.raise_for_status()
logger.debug(f"Response for listing files at {url}: {response.json()}")
return response.json()
except httpx.HTTPStatusError as e:
logging.error(f"Failed to list files: {e}")
raise Exception(f"Failed to list files: {e.response.text}") from e


async def delete_file(http_client: httpx.AsyncClient, base_url: str, filename: str) -> dict:
"""
Deletes a specified file from the server.
:param http_client: An HTTP client for sending requests.
:param base_url: The base URL of the Hive Agent API.
:param filename: The name of the file to be deleted.
:return: A dictionary with a message about the file deletion.
:raises Exception: If the HTTP request fails or the API returns an error response.
"""
endpoint = f"/files/{filename}"
url = f"{base_url}{endpoint}"

try:
logger.debug(f"Deleting file {filename} at {url}")
response = await http_client.delete(url)
response.raise_for_status()
logger.debug(f"Response for deleting file {filename} at {url}: {response.json()}")
return response.json()
except httpx.HTTPStatusError as e:
logging.error(f"Failed to delete file {filename}: {e}")
raise Exception(f"Failed to delete file {filename}: {e.response.text}") from e


async def rename_file(http_client: httpx.AsyncClient, base_url: str, old_filename: str, new_filename: str) -> dict:
"""
Renames a specified file on the server.
:param http_client: An HTTP client for sending requests.
:param base_url: The base URL of the Hive Agent API.
:param old_filename: The current name of the file.
:param new_filename: The new name for the file.
:return: A dictionary with a message about the file renaming.
:raises Exception: If the HTTP request fails or the API returns an error response.
"""
endpoint = f"/files/{old_filename}/{new_filename}"
url = f"{base_url}{endpoint}"

try:
logger.debug(f"Renaming file from {old_filename} to {new_filename} at {url}")
response = await http_client.put(url)
response.raise_for_status()
logger.debug(f"Response for renaming file from {old_filename} to {new_filename} at {url}: {response.json()}")
return response.json()
except httpx.HTTPStatusError as e:
logging.error(f"Failed to rename file from {old_filename} to {new_filename}: {e}")
raise Exception(f"Failed to rename file from {old_filename} to {new_filename}: {e.response.text}") from e
Empty file added tests/files/__init__.py
Empty file.
132 changes: 132 additions & 0 deletions tests/files/test_file_management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import os
import httpx
import pytest
import respx

from hive_agent_client.files import (
upload_files,
list_files,
delete_file,
rename_file
)

base_url = "http://example.com"


@pytest.fixture
def temp_files():
file_paths = ["test1.txt", "test2.txt"]
for file_path in file_paths:
with open(file_path, "w") as f:
f.write("test content")
yield file_paths
for file_path in file_paths:
os.remove(file_path)


@pytest.mark.asyncio
async def test_upload_files_success(temp_files):
expected_response = {"filenames": ["test1.txt", "test2.txt"]}

with respx.mock() as mock:
mock.post(f"{base_url}/uploadfiles/").mock(
return_value=httpx.Response(200, json=expected_response))

async with httpx.AsyncClient() as client:
response = await upload_files(client, base_url, temp_files)
assert response == expected_response


@pytest.mark.asyncio
async def test_upload_files_http_error(temp_files):
with respx.mock() as mock:
mock.post(f"{base_url}/uploadfiles/").mock(
return_value=httpx.Response(400))

async with httpx.AsyncClient() as client:
with pytest.raises(Exception) as excinfo:
await upload_files(client, base_url, temp_files)
assert "Failed to upload files" in str(excinfo.value)


@pytest.mark.asyncio
async def test_list_files_success():
expected_response = {"files": ["test1.txt", "test2.txt"]}

with respx.mock() as mock:
mock.get(f"{base_url}/files/").mock(
return_value=httpx.Response(200, json=expected_response))

async with httpx.AsyncClient() as client:
response = await list_files(client, base_url)
assert response == expected_response


@pytest.mark.asyncio
async def test_list_files_http_error():
with respx.mock() as mock:
mock.get(f"{base_url}/files/").mock(
return_value=httpx.Response(400))

async with httpx.AsyncClient() as client:
with pytest.raises(Exception) as excinfo:
await list_files(client, base_url)
assert "Failed to list files" in str(excinfo.value)


@pytest.mark.asyncio
async def test_delete_file_success():
filename = "test_delete.txt"
expected_response = {"message": f"File {filename} deleted successfully."}

with respx.mock() as mock:
mock.delete(f"{base_url}/files/{filename}").mock(
return_value=httpx.Response(200, json=expected_response))

async with httpx.AsyncClient() as client:
response = await delete_file(client, base_url, filename)
assert response == expected_response


@pytest.mark.asyncio
async def test_delete_file_http_error():
filename = "test_delete.txt"

with respx.mock() as mock:
mock.delete(f"{base_url}/files/{filename}").mock(
return_value=httpx.Response(400))

async with httpx.AsyncClient() as client:
with pytest.raises(Exception) as excinfo:
await delete_file(client, base_url, filename)
assert "Failed to delete file" in str(excinfo.value)


@pytest.mark.asyncio
async def test_rename_file_success():
old_filename = "old_name.txt"
new_filename = "new_name.txt"
expected_response = {"message": f"File {old_filename} renamed to {new_filename} successfully."}

with respx.mock() as mock:
mock.put(f"{base_url}/files/{old_filename}/{new_filename}").mock(
return_value=httpx.Response(200, json=expected_response))

async with httpx.AsyncClient() as client:
response = await rename_file(client, base_url, old_filename, new_filename)
assert response == expected_response


@pytest.mark.asyncio
async def test_rename_file_http_error():
old_filename = "old_name.txt"
new_filename = "new_name.txt"

with respx.mock() as mock:
mock.put(f"{base_url}/files/{old_filename}/{new_filename}").mock(
return_value=httpx.Response(400))

async with httpx.AsyncClient() as client:
with pytest.raises(Exception) as excinfo:
await rename_file(client, base_url, old_filename, new_filename)
assert "Failed to rename file" in str(excinfo.value)
Loading

0 comments on commit 9c10e04

Please sign in to comment.