From 4965c92b0310c0460fd04c9bda016906249c5f35 Mon Sep 17 00:00:00 2001 From: Tomisin Jenrola Date: Mon, 27 May 2024 11:31:14 -0400 Subject: [PATCH] [HN-111/HN-116] refactor: agent data storage --- hive_agent_client/client.py | 113 ++++++-------- hive_agent_client/database/__init__.py | 7 + hive_agent_client/database/database.py | 154 +++++++++++++++++++ hive_agent_client/entry/__init__.py | 8 - hive_agent_client/entry/entry.py | 168 --------------------- tests/{entry => database}/__init__.py | 0 tests/database/test_database.py | 169 +++++++++++++++++++++ tests/entry/test_entry.py | 148 ------------------ tests/test_client.py | 200 ++++++++++++++----------- tutorial.md | 77 +++++----- 10 files changed, 528 insertions(+), 516 deletions(-) create mode 100644 hive_agent_client/database/__init__.py create mode 100644 hive_agent_client/database/database.py delete mode 100644 hive_agent_client/entry/__init__.py delete mode 100644 hive_agent_client/entry/entry.py rename tests/{entry => database}/__init__.py (100%) create mode 100644 tests/database/test_database.py delete mode 100644 tests/entry/test_entry.py diff --git a/hive_agent_client/client.py b/hive_agent_client/client.py index 5b54282..af14b00 100644 --- a/hive_agent_client/client.py +++ b/hive_agent_client/client.py @@ -1,21 +1,21 @@ import httpx import logging -from typing import AsyncGenerator, Dict +from typing import Dict from hive_agent_client.chat import send_chat_message -from hive_agent_client.entry import ( - create_entry, - stream_entry, - get_entries, - get_entry_by_id, - update_entry, - delete_entry +from hive_agent_client.database import ( + create_table, + insert_data, + read_data, + update_data, + delete_data ) logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + class HiveAgentClient: """ Client for interacting with a Hive Agent's API. @@ -44,90 +44,76 @@ async def chat(self, content: str) -> str: logger.error(f"Failed to send chat message - {content}: {e}") raise Exception(f"Failed to send chat message: {e}") - async def create_entry(self, namespace: str, data: dict) -> Dict: - """ - Create a new entry in the specified namespace. - - :param namespace: The namespace in which to create the entry. - :param data: The data to be stored in the entry. - :return: A dictionary representing the created entry. - """ - try: - return await create_entry(self.http_client, self.base_url, namespace, data) - except Exception as e: - logger.error(f"Failed to create entry {data} in {namespace}: {e}") - raise Exception(f"Failed to create entry: {e}") - - async def stream_entry_data(self, namespace: str, data_stream: AsyncGenerator) -> AsyncGenerator: + async def create_table(self, table_name: str, columns: dict) -> Dict: """ - Stream data to the entry endpoint. + Create a new table in the database. - :param namespace: The namespace in which to stream the data. - :param data_stream: The async generator providing the data to stream. - :yield: Messages received in response to the streamed data. + :param table_name: The name of the table to create. + :param columns: The columns of the table to create. + :return: A dictionary with a message about the table creation. """ try: - async for message in stream_entry(self.http_client, self.base_url, namespace, data_stream): - yield message + return await create_table(self.http_client, self.base_url, table_name, columns) except Exception as e: - logger.error(f"Failed to stream entry data from {namespace}: {e}") - raise Exception(f"Failed to stream entry data: {e}") + logger.error(f"Failed to create table {table_name} with columns {columns}: {e}") + raise Exception(f"Failed to create table: {e}") - async def get_entries(self, namespace: str) -> Dict: + async def insert_data(self, table_name: str, data: dict) -> Dict: """ - Retrieve all entries from the specified namespace. + Insert data into a table. - :param namespace: The namespace from which to retrieve entries. - :return: A dictionary containing the retrieved entries. + :param table_name: The name of the table to insert data into. + :param data: The data to insert. + :return: A dictionary with a message and the id of the inserted data. """ try: - return await get_entries(self.http_client, self.base_url, namespace) + return await insert_data(self.http_client, self.base_url, table_name, data) except Exception as e: - logger.error(f"Failed to get entries in {namespace}: {e}") - raise Exception(f"Failed to get entries: {e}") + logger.error(f"Failed to insert data {data} into table {table_name}: {e}") + raise Exception(f"Failed to insert data: {e}") - async def get_entry_by_id(self, namespace: str, entry_id: str) -> Dict: + async def read_data(self, table_name: str, filters: dict = None) -> Dict: """ - Retrieve a specific entry by its ID. + Retrieve data from a table. - :param namespace: The namespace of the entry. - :param entry_id: The ID of the entry to retrieve. - :return: A dictionary representing the retrieved entry. + :param table_name: The name of the table to read data from. + :param filters: Optional filters to apply when reading data. + :return: A list of dictionaries representing the read data. """ try: - return await get_entry_by_id(self.http_client, self.base_url, namespace, entry_id) + return await read_data(self.http_client, self.base_url, table_name, filters) except Exception as e: - logger.error(f"Failed to get entry {entry_id} from {namespace}: {e}") - raise Exception(f"Failed to get entry by ID: {e}") + logger.error(f"Failed to read data from table {table_name} with filters {filters}: {e}") + raise Exception(f"Failed to read data: {e}") - async def update_entry(self, namespace: str, entry_id: str, data: dict) -> Dict: + async def update_data(self, table_name: str, row_id: int, new_data: dict) -> Dict: """ - Update a specific entry by its ID. + Update data in a table. - :param namespace: The namespace of the entry. - :param entry_id: The ID of the entry to update. - :param data: The data to update the entry with. - :return: A dictionary representing the updated entry. + :param table_name: The name of the table to update data in. + :param row_id: The ID of the row to update. + :param new_data: The new data to update in the table. + :return: A dictionary with a message about the update. """ try: - return await update_entry(self.http_client, self.base_url, namespace, entry_id, data) + return await update_data(self.http_client, self.base_url, table_name, row_id, new_data) except Exception as e: - logger.error(f"Failed to update entry {entry_id} from {namespace} with {data}: {e}") - raise Exception(f"Failed to update entry: {e}") + logger.error(f"Failed to update data in table {table_name} with id {row_id} and data {new_data}: {e}") + raise Exception(f"Failed to update data: {e}") - async def delete_entry(self, namespace: str, entry_id: str) -> Dict: + async def delete_data(self, table_name: str, row_id: int) -> Dict: """ - Delete a specific entry by its ID. + Delete data from a table. - :param namespace: The namespace of the entry. - :param entry_id: The ID of the entry to delete. - :return: A dictionary containing the status of the deletion. + :param table_name: The name of the table to delete data from. + :param row_id: The ID of the row to delete. + :return: A dictionary with a message about the deletion. """ try: - return await delete_entry(self.http_client, self.base_url, namespace, entry_id) + return await delete_data(self.http_client, self.base_url, table_name, row_id) except Exception as e: - logger.error(f"Failed to delete entry {entry_id} from {namespace}: {e}") - raise Exception(f"Failed to delete entry: {e}") + 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 close(self): """ @@ -135,4 +121,3 @@ async def close(self): """ logger.debug("Closing HTTP client session...") await self.http_client.aclose() - \ No newline at end of file diff --git a/hive_agent_client/database/__init__.py b/hive_agent_client/database/__init__.py new file mode 100644 index 0000000..0acd818 --- /dev/null +++ b/hive_agent_client/database/__init__.py @@ -0,0 +1,7 @@ +from .database import ( + create_table, + insert_data, + read_data, + update_data, + delete_data +) diff --git a/hive_agent_client/database/database.py b/hive_agent_client/database/database.py new file mode 100644 index 0000000..5444dc9 --- /dev/null +++ b/hive_agent_client/database/database.py @@ -0,0 +1,154 @@ +import json + +import httpx +import logging +import os +import sys + +from typing import Optional + + +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 create_table(http_client: httpx.AsyncClient, base_url: str, table_name: str, columns: dict) -> dict: + """ + Creates a table in the database. + + :param http_client: An HTTP client for sending requests. + :param base_url: The base URL of the Hive Agent API. + :param table_name: The name of the table to be created. + :param columns: The columns of the table to be created. + :return: A dictionary with a message about the table creation. + :raises Exception: If the HTTP request fails or the API returns an error response. + """ + endpoint = "/database/create-table" + url = f"{base_url}{endpoint}" + data = {"table_name": table_name, "columns": columns} + + try: + logger.debug(f"Creating table {table_name} at {url}") + response = await http_client.post(url, json=data) + response.raise_for_status() + logger.debug(f"Response for creating table {table_name} at {url}: {response.json()}") + return response.json() + except httpx.HTTPStatusError as e: + logging.error(f"Failed to create table {table_name}: {e}") + raise Exception(f"Failed to create table {table_name}: {e.response.text}") from e + + +async def insert_data(http_client: httpx.AsyncClient, base_url: str, table_name: str, data: dict) -> dict: + """ + Inserts data into a table in the database. + + :param http_client: An HTTP client for sending requests. + :param base_url: The base URL of the Hive Agent API. + :param table_name: The name of the table where the data will be inserted. + :param data: The data to be inserted. + :return: A dictionary with a message and the id of the inserted data. + :raises Exception: If the HTTP request fails or the API returns an error response. + """ + endpoint = "/database/insert-data" + url = f"{base_url}{endpoint}" + payload = {"table_name": table_name, "data": data} + + try: + logger.debug(f"Inserting data into {table_name} at {url}") + response = await http_client.post(url, json=payload) + response.raise_for_status() + logger.debug(f"Response for inserting data into {table_name} at {url}: {response.json()}") + return response.json() + except httpx.HTTPStatusError as e: + logging.error(f"Failed to insert data into {table_name}: {e}") + raise Exception(f"Failed to insert data into {table_name}: {e.response.text}") from e + + +async def read_data(http_client: httpx.AsyncClient, base_url: str, table_name: str, + filters: Optional[dict] = None) -> list: + """ + Reads data from a table in the database. + + :param http_client: An HTTP client for sending requests. + :param base_url: The base URL of the Hive Agent API. + :param table_name: The name of the table from which to read data. + :param filters: Optional filters to apply when reading data. + :return: A list of dictionaries representing the read data. + :raises Exception: If the HTTP request fails or the API returns an error response. + """ + endpoint = "/database/read-data" + url = f"{base_url}{endpoint}" + payload = {"table_name": table_name, "filters": filters} + + try: + logger.debug(f"Reading data from {table_name} at {url} with filters: {filters}") + response = await http_client.post(url, json=payload) + response.raise_for_status() + logger.debug(f"Response for reading data from {table_name} at {url}: {response.json()}") + return response.json() + except httpx.HTTPStatusError as e: + logging.error(f"Failed to read data from {table_name}: {e}") + raise Exception(f"Failed to read data from {table_name}: {e.response.text}") from e + + +async def update_data(http_client: httpx.AsyncClient, base_url: str, table_name: str, row_id: int, + new_data: dict) -> dict: + """ + Updates data in a table in the database. + + :param http_client: An HTTP client for sending requests. + :param base_url: The base URL of the Hive Agent API. + :param table_name: The name of the table where the data will be updated. + :param row_id: The ID of the row to be updated. + :param new_data: The new data to update in the table. + :return: A dictionary with a message about the update. + :raises Exception: If the HTTP request fails or the API returns an error response. + """ + endpoint = "/database/update-data" + url = f"{base_url}{endpoint}" + payload = {"table_name": table_name, "id": row_id, "data": new_data} + + try: + logger.debug(f"Updating data in {table_name} with id {row_id} at {url} with new data: {new_data}") + response = await http_client.put(url, json=payload) + response.raise_for_status() + logger.debug(f"Response for updating data in {table_name} with id {row_id} at {url}: {response.json()}") + return response.json() + except httpx.HTTPStatusError as e: + logging.error(f"Failed to update data in {table_name} with id {row_id}: {e}") + raise Exception(f"Failed to update data in {table_name} with id {row_id}: {e.response.text}") from e + + +async def delete_data(http_client: httpx.AsyncClient, base_url: str, table_name: str, row_id: int) -> dict: + """ + Deletes data from a table in the database. + + :param http_client: An HTTP client for sending requests. + :param base_url: The base URL of the Hive Agent API. + :param table_name: The name of the table where the data will be deleted. + :param row_id: The ID of the row to be deleted. + :return: A dictionary with a message about the deletion. + :raises Exception: If the HTTP request fails or the API returns an error response. + """ + endpoint = "/database/delete-data" + url = f"{base_url}{endpoint}" + payload = {"table_name": table_name, "id": row_id} + + try: + logger.debug(f"Deleting data from {table_name} with id {row_id} at {url}") + response = await http_client.request("DELETE", url, content=json.dumps(payload)) + response.raise_for_status() + logger.debug(f"Response for deleting data from {table_name} with id {row_id} at {url}: {response.json()}") + return response.json() + except httpx.HTTPStatusError as e: + logging.error(f"Failed to delete data from {table_name} with id {row_id}: {e}") + raise Exception(f"Failed to delete data from {table_name} with id {row_id}: {e.response.text}") from e + diff --git a/hive_agent_client/entry/__init__.py b/hive_agent_client/entry/__init__.py deleted file mode 100644 index 7f20504..0000000 --- a/hive_agent_client/entry/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .entry import ( - create_entry, - stream_entry, - get_entries, - get_entry_by_id, - update_entry, - delete_entry -) diff --git a/hive_agent_client/entry/entry.py b/hive_agent_client/entry/entry.py deleted file mode 100644 index 77cc4a1..0000000 --- a/hive_agent_client/entry/entry.py +++ /dev/null @@ -1,168 +0,0 @@ -import httpx -import logging -import os -import sys - -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 create_entry(http_client: httpx.AsyncClient, base_url: str, namespace: str, data: dict) -> dict: - """ - Creates an entry in the specified namespace. - - :param http_client: An HTTP client for sending requests. - :param base_url: The base URL of the Hive Agent API. - :param namespace: The namespace where the entry will be created. - :param data: The data of the entry to be created. - :return: The created entry as a dictionary. - :raises Exception: If the HTTP request fails or the API returns an error response. - """ - endpoint = f"/api/entry/{namespace}" - url = f"{base_url}{endpoint}" - - try: - logger.debug(f"Creating entry in {namespace} at {url}") - response = await http_client.post(url, json=data) - response.raise_for_status() - logger.debug(f"Response for creating entry in {namespace} at {url}: {response.json()}") - return response.json() - except httpx.HTTPStatusError as e: - logging.error(f"Failed to create entry in {namespace}: {e}") - raise Exception(f"Failed to create entry in {namespace}: {e.response.text}") from e - - -async def stream_entry(http_client: httpx.AsyncClient, base_url: str, namespace: str, data_stream): - """ - Streams data to the specified namespace entry endpoint and yields responses. - - :param http_client: An HTTP client for sending requests. - :param base_url: The base URL of the Hive Agent API. - :param namespace: The namespace where the data will be streamed. - :param data_stream: An asynchronous iterable providing the data to be streamed. - :raises RuntimeError: If WebSocket communication or data processing fails. - """ - endpoint = f"/api/entry/{namespace}/stream" - url = f"{base_url}{endpoint}" - - try: - logger.debug(f"Streaming data to {namespace} at {url}") - async with http_client.ws(url) as ws: - try: - async for data in data_stream: - await ws.send_json(data) - response = await ws.receive_json() - yield response - except httpx.WebSocketException as e: - logging.error(f"WebSocket communication error in namespace {namespace}: {e}") - raise RuntimeError(f"WebSocket communication error in namespace {namespace}") from e - except httpx.RequestError as e: - logging.error(f"Request error while establishing WebSocket connection to {url}: {e}") - raise RuntimeError(f"Failed to establish WebSocket connection to {url}") from e - - -async def get_entries(http_client: httpx.AsyncClient, base_url: str, namespace: str) -> dict: - """ - Retrieves all entries from the specified namespace. - - :param http_client: An HTTP client for sending requests. - :param base_url: The base URL of the Hive Agent API. - :param namespace: The namespace from which to retrieve entries. - :return: A dictionary containing all entries from the namespace. - :raises Exception: If the HTTP request fails or the API returns an error response. - """ - endpoint = f"/api/entry/{namespace}" - url = f"{base_url}{endpoint}" - - try: - logger.debug(f"Getting all entries in {namespace} at {url}") - response = await http_client.get(url) - response.raise_for_status() - logger.debug(f"Response for getting all entries in {namespace} at {url}: {response.json()}") - return response.json() - except httpx.HTTPStatusError as e: - logging.error(f"Failed to get entries from {namespace}: {e}") - raise Exception(f"Failed to get entries from {namespace}: {e.response.text}") from e - - -async def get_entry_by_id(http_client: httpx.AsyncClient, base_url: str, namespace: str, entry_id: str) -> dict: - """ - Retrieves a specific entry by ID from the given namespace. - - :param http_client: An HTTP client for sending requests. - :param base_url: The base URL of the Hive Agent API. - :param namespace: The namespace from which to retrieve the entry. - :param entry_id: The ID of the entry to retrieve. - :return: A dictionary representing the requested entry. - :raises Exception: If the HTTP request fails or the API returns an error response. - """ - endpoint = f"/api/entry/{namespace}/{entry_id}" - url = f"{base_url}{endpoint}" - - try: - logger.debug(f"Getting entry {entry_id} from {namespace} at {url}") - response = await http_client.get(url) - response.raise_for_status() - logger.debug(f"Response for getting entry {entry_id} from {namespace} at {url}: {response.json()}") - return response.json() - except httpx.HTTPStatusError as e: - logging.error(f"Failed to get entry {entry_id} from {namespace}: {e}") - raise Exception(f"Failed to get entry {entry_id} from {namespace}: {e.response.text}") from e - - -async def update_entry(http_client: httpx.AsyncClient, base_url: str, namespace: str, entry_id: str, data: dict) -> dict: - """ - Updates a specific entry by ID in the given namespace. - - :param http_client: An HTTP client for sending requests. - :param base_url: The base URL of the Hive Agent API. - :param namespace: The namespace where the entry exists. - :param entry_id: The ID of the entry to update. - :param data: The updated data for the entry. - :return: A dictionary representing the updated entry. - :raises Exception: If the HTTP request fails or the API returns an error response. - """ - endpoint = f"/api/entry/{namespace}/{entry_id}" - url = f"{base_url}{endpoint}" - - try: - logger.debug(f"Updating entry {entry_id} from {namespace} with {data} at {url}") - response = await http_client.put(url, json=data) - response.raise_for_status() - logger.debug(f"Response for updating entry {entry_id} from {namespace} at {url}: {response.json()}") - return response.json() - except httpx.HTTPStatusError as e: - logging.error(f"Failed to update entry {entry_id} in {namespace}: {e}") - raise Exception(f"Failed to update entry {entry_id} in {namespace}: {e.response.text}") from e - - -async def delete_entry(http_client: httpx.AsyncClient, base_url: str, namespace: str, entry_id: str) -> dict: - """ - Deletes a specific entry by ID from the given namespace. - - :param http_client: An HTTP client for sending requests. - :param base_url: The base URL of the Hive Agent API. - :param namespace: The namespace where the entry exists. - :param entry_id: The ID of the entry to delete. - :return: A dictionary indicating the result of the deletion operation. - :raises Exception: If the HTTP request fails or the API returns an error response. - """ - endpoint = f"/api/entry/{namespace}/{entry_id}" - url = f"{base_url}{endpoint}" - - try: - logger.debug(f"Deleting {entry_id} from {namespace} at {url}") - response = await http_client.delete(url) - response.raise_for_status() - logger.debug(f"Response for deleting entry {entry_id} from {namespace} at {url}: {response.json()}") - return response.json() - except httpx.HTTPStatusError as e: - logging.error(f"Failed to delete entry {entry_id} from {namespace}: {e}") - raise Exception(f"Failed to delete entry {entry_id} from {namespace}: {e.response.text}") from e diff --git a/tests/entry/__init__.py b/tests/database/__init__.py similarity index 100% rename from tests/entry/__init__.py rename to tests/database/__init__.py diff --git a/tests/database/test_database.py b/tests/database/test_database.py new file mode 100644 index 0000000..191dba7 --- /dev/null +++ b/tests/database/test_database.py @@ -0,0 +1,169 @@ +import json + +import httpx +import pytest +import respx + +from hive_agent_client.database import ( + create_table, + insert_data, + read_data, + update_data, + delete_data +) + +base_url = "http://example.com" + + +@pytest.mark.asyncio +async def test_create_table_success(): + table_name = "test_table" + columns = {"id": "Integer", "name": "String"} + expected_response = {"message": f"Table {table_name} created successfully."} + + with respx.mock() as mock: + mock.post(f"{base_url}/database/create-table", json={"table_name": table_name, "columns": columns}).mock( + return_value=httpx.Response(200, json=expected_response)) + + async with httpx.AsyncClient() as client: + response = await create_table(client, base_url, table_name, columns) + assert response == expected_response + + +@pytest.mark.asyncio +async def test_create_table_http_error(): + table_name = "test_table" + columns = {"id": "Integer", "name": "String"} + + with respx.mock() as mock: + mock.post(f"{base_url}/database/create-table", json={"table_name": table_name, "columns": columns}).mock( + return_value=httpx.Response(400)) + + async with httpx.AsyncClient() as client: + with pytest.raises(Exception) as excinfo: + await create_table(client, base_url, table_name, columns) + assert "Failed to create table" in str(excinfo.value) + + +@pytest.mark.asyncio +async def test_insert_data_success(): + table_name = "test_table" + data = {"name": "Test"} + expected_response = {"message": "Data inserted successfully.", "id": 1} + + with respx.mock() as mock: + mock.post(f"{base_url}/database/insert-data", json={"table_name": table_name, "data": data}).mock( + return_value=httpx.Response(200, json=expected_response)) + + async with httpx.AsyncClient() as client: + response = await insert_data(client, base_url, table_name, data) + assert response == expected_response + + +@pytest.mark.asyncio +async def test_insert_data_http_error(): + table_name = "test_table" + data = {"name": "Test"} + + with respx.mock() as mock: + mock.post(f"{base_url}/database/insert-data", json={"table_name": table_name, "data": data}).mock( + return_value=httpx.Response(400)) + + async with httpx.AsyncClient() as client: + with pytest.raises(Exception) as excinfo: + await insert_data(client, base_url, table_name, data) + assert "Failed to insert data" in str(excinfo.value) + + +@pytest.mark.asyncio +async def test_read_data_success(): + table_name = "test_table" + filters = {"id": [1]} + expected_response = [{"id": 1, "name": "Test"}] + + with respx.mock() as mock: + mock.post(f"{base_url}/database/read-data", json={"table_name": table_name, "filters": filters}).mock( + return_value=httpx.Response(200, json=expected_response)) + + async with httpx.AsyncClient() as client: + response = await read_data(client, base_url, table_name, filters) + assert response == expected_response + + +@pytest.mark.asyncio +async def test_read_data_http_error(): + table_name = "test_table" + filters = {"id": [1]} + + with respx.mock() as mock: + mock.post(f"{base_url}/database/read-data", json={"table_name": table_name, "filters": filters}).mock( + return_value=httpx.Response(400)) + + async with httpx.AsyncClient() as client: + with pytest.raises(Exception) as excinfo: + await read_data(client, base_url, table_name, filters) + assert "Failed to read data" in str(excinfo.value) + + +@pytest.mark.asyncio +async def test_update_data_success(): + table_name = "test_table" + row_id = 1 + new_data = {"name": "Updated Test"} + expected_response = {"message": "Data updated successfully."} + + with respx.mock() as mock: + mock.put(f"{base_url}/database/update-data", + json={"table_name": table_name, "id": row_id, "data": new_data}).mock( + return_value=httpx.Response(200, json=expected_response)) + + async with httpx.AsyncClient() as client: + response = await update_data(client, base_url, table_name, row_id, new_data) + assert response == expected_response + + +@pytest.mark.asyncio +async def test_update_data_http_error(): + table_name = "test_table" + row_id = 1 + new_data = {"name": "Updated Test"} + + with respx.mock() as mock: + mock.put(f"{base_url}/database/update-data", + json={"table_name": table_name, "id": row_id, "data": new_data}).mock( + return_value=httpx.Response(400)) + + async with httpx.AsyncClient() as client: + with pytest.raises(Exception) as excinfo: + await update_data(client, base_url, table_name, row_id, new_data) + assert "Failed to update data" in str(excinfo.value) + + +@pytest.mark.asyncio +async def test_delete_data_success(): + table_name = "test_table" + row_id = 1 + expected_response = {"message": "Data deleted successfully."} + + with respx.mock() as mock: + mock.request("DELETE", f"{base_url}/database/delete-data", content=json.dumps({"table_name": table_name, "id": row_id})).mock( + return_value=httpx.Response(200, json=expected_response)) + + async with httpx.AsyncClient() as client: + response = await delete_data(client, base_url, table_name, row_id) + assert response == expected_response + + +@pytest.mark.asyncio +async def test_delete_data_http_error(): + table_name = "test_table" + row_id = 1 + + with respx.mock() as mock: + mock.request("DELETE", f"{base_url}/database/delete-data", content=json.dumps({"table_name": table_name, "id": row_id})).mock( + return_value=httpx.Response(400)) + + async with httpx.AsyncClient() as client: + with pytest.raises(Exception) as excinfo: + await delete_data(client, base_url, table_name, row_id) + assert "Failed to delete data" in str(excinfo.value) diff --git a/tests/entry/test_entry.py b/tests/entry/test_entry.py deleted file mode 100644 index b4d2488..0000000 --- a/tests/entry/test_entry.py +++ /dev/null @@ -1,148 +0,0 @@ -import httpx -import pytest -import respx - -from hive_agent_client.entry import ( - create_entry, - stream_entry, - get_entries, - get_entry_by_id, - update_entry, - delete_entry -) - -base_url = "http://example.com" -namespace = "test-namespace" - - -@pytest.mark.asyncio -async def test_create_entry_success(): - data = {"key": "value"} - expected_response = {"id": "123", "key": "value"} - - with respx.mock() as mock: - mock.post(f"{base_url}/api/entry/{namespace}", json=data).mock( - return_value=httpx.Response(200, json=expected_response)) - - async with httpx.AsyncClient() as client: - response = await create_entry(client, base_url, namespace, data) - assert response == expected_response - - -@pytest.mark.asyncio -async def test_create_entry_http_error(): - data = {"key": "value"} - - with respx.mock() as mock: - mock.post(f"{base_url}/api/entry/{namespace}", json=data).mock(return_value=httpx.Response(400)) - - async with httpx.AsyncClient() as client: - with pytest.raises(Exception) as excinfo: - await create_entry(client, base_url, namespace, data) - assert "Failed to create entry" in str(excinfo.value) - - -@pytest.mark.asyncio -async def test_get_entries_success(): - expected_response = [{"id": "123", "key": "value"}] - - with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}").mock(return_value=httpx.Response(200, json=expected_response)) - - async with httpx.AsyncClient() as client: - response = await get_entries(client, base_url, namespace) - assert response == expected_response - - -@pytest.mark.asyncio -async def test_get_entries_http_error(): - with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}").mock(return_value=httpx.Response(400)) - - async with httpx.AsyncClient() as client: - with pytest.raises(Exception) as excinfo: - await get_entries(client, base_url, namespace) - assert "Failed to get entries" in str(excinfo.value) - - -@pytest.mark.asyncio -async def test_get_entry_by_id_success(): - entry_id = "123" - expected_response = {"id": "123", "key": "value"} - - with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock( - return_value=httpx.Response(200, json=expected_response)) - - async with httpx.AsyncClient() as client: - response = await get_entry_by_id(client, base_url, namespace, entry_id) - assert response == expected_response - - -@pytest.mark.asyncio -async def test_get_entry_by_id_http_error(): - entry_id = "123" - - with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(400)) - - async with httpx.AsyncClient() as client: - with pytest.raises(Exception) as excinfo: - await get_entry_by_id(client, base_url, namespace, entry_id) - assert "Failed to get entry" in str(excinfo.value) - - -@pytest.mark.asyncio -async def test_update_entry_success(): - entry_id = "123" - data = {"key": "updated value"} - expected_response = {"id": "123", "key": "updated value"} - - with respx.mock() as mock: - mock.put(f"{base_url}/api/entry/{namespace}/{entry_id}", json=data).mock( - return_value=httpx.Response(200, json=expected_response)) - - async with httpx.AsyncClient() as client: - response = await update_entry(client, base_url, namespace, entry_id, data) - assert response == expected_response - - -@pytest.mark.asyncio -async def test_update_entry_http_error(): - entry_id = "123" - data = {"key": "updated value"} - - with respx.mock() as mock: - mock.put(f"{base_url}/api/entry/{namespace}/{entry_id}", json=data).mock(return_value=httpx.Response(400)) - - async with httpx.AsyncClient() as client: - with pytest.raises(Exception) as excinfo: - await update_entry(client, base_url, namespace, entry_id, data) - assert "Failed to update entry" in str(excinfo.value) - - -@pytest.mark.asyncio -async def test_delete_entry_success(): - entry_id = "123" - expected_response = {"message": "Entry deleted"} - - with respx.mock() as mock: - mock.delete(f"{base_url}/api/entry/{namespace}/{entry_id}").mock( - return_value=httpx.Response(200, json=expected_response)) - - async with httpx.AsyncClient() as client: - response = await delete_entry(client, base_url, namespace, entry_id) - assert response == expected_response - - -@pytest.mark.asyncio -async def test_delete_entry_http_error(): - entry_id = "123" - - with respx.mock() as mock: - mock.delete(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(400)) - - async with httpx.AsyncClient() as client: - with pytest.raises(Exception) as excinfo: - await delete_entry(client, base_url, namespace, entry_id) - assert "Failed to delete entry" in str(excinfo.value) diff --git a/tests/test_client.py b/tests/test_client.py index 90762c2..4d2e5f8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,17 +1,20 @@ import httpx +import json import pytest import respx -from hive_agent_client import HiveAgentClient +from hive_agent_client.client import HiveAgentClient base_url = "http://example.com" -def check_response(response: int) -> int: + +def check_response(response: int) -> int: if response != 200: raise Exception(f"Unexpected status code: {response}") else: return response - + + @pytest.mark.asyncio async def test_chat_success(): content = "Hello" @@ -39,147 +42,155 @@ async def test_chat_failure(): @pytest.mark.asyncio -async def test_create_entry_success(): - namespace = "test" - data = {"key": "value"} - expected_response = {"id": "123", "key": "value"} +async def test_create_table_success(): + table_name = "test_table" + columns = {"id": "Integer", "name": "String"} + expected_response = {"message": f"Table {table_name} created successfully."} with respx.mock() as mock: - mock.post(f"{base_url}/api/entry/{namespace}", json=data).mock( + mock.post(f"{base_url}/database/create-table", json={"table_name": table_name, "columns": columns}).mock( return_value=httpx.Response(200, json=expected_response)) client = HiveAgentClient(base_url) - response = await client.create_entry(namespace, data) + response = await client.create_table(table_name, columns) assert response == expected_response @pytest.mark.asyncio -async def test_create_entry_failure(): - namespace = "test" - data = {"key": "value"} +async def test_create_table_failure(): + table_name = "test_table" + columns = {"id": "Integer", "name": "String"} with respx.mock() as mock: - mock.post(f"{base_url}/api/entry/{namespace}", json=data).mock(return_value=httpx.Response(400)) + mock.post(f"{base_url}/database/create-table", json={"table_name": table_name, "columns": columns}).mock( + return_value=httpx.Response(400)) client = HiveAgentClient(base_url) with pytest.raises(Exception) as excinfo: - await client.create_entry(namespace, data) - assert "Failed to create entry" in str(excinfo.value) + await client.create_table(table_name, columns) + assert "Failed to create table" in str(excinfo.value) @pytest.mark.asyncio -async def test_get_entries_success(): - namespace = "test" - expected_response = [{"id": "1", "key": "value"}] +async def test_insert_data_success(): + table_name = "test_table" + data = {"name": "Test"} + expected_response = {"message": "Data inserted successfully.", "id": 1} with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}").mock(return_value=httpx.Response(200, json=expected_response)) + mock.post(f"{base_url}/database/insert-data", json={"table_name": table_name, "data": data}).mock( + return_value=httpx.Response(200, json=expected_response)) client = HiveAgentClient(base_url) - response = await client.get_entries(namespace) + response = await client.insert_data(table_name, data) assert response == expected_response @pytest.mark.asyncio -async def test_get_entries_failure(): - namespace = "test" +async def test_insert_data_failure(): + table_name = "test_table" + data = {"name": "Test"} with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}").mock(return_value=httpx.Response(400)) + mock.post(f"{base_url}/database/insert-data", json={"table_name": table_name, "data": data}).mock( + return_value=httpx.Response(400)) client = HiveAgentClient(base_url) with pytest.raises(Exception) as excinfo: - await client.get_entries(namespace) - assert "Failed to get entries" in str(excinfo.value) + await client.insert_data(table_name, data) + assert "Failed to insert data" in str(excinfo.value) @pytest.mark.asyncio -async def test_get_entry_by_id_success(): - namespace = "test" - entry_id = "1" - expected_response = {"id": "1", "key": "value"} +async def test_read_data_success(): + table_name = "test_table" + filters = {"id": [1]} + expected_response = [{"id": 1, "name": "Test"}] with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock( + mock.post(f"{base_url}/database/read-data", json={"table_name": table_name, "filters": filters}).mock( return_value=httpx.Response(200, json=expected_response)) client = HiveAgentClient(base_url) - response = await client.get_entry_by_id(namespace, entry_id) + response = await client.read_data(table_name, filters) assert response == expected_response @pytest.mark.asyncio -async def test_get_entry_by_id_failure(): - namespace = "test" - entry_id = "1" +async def test_read_data_failure(): + table_name = "test_table" + filters = {"id": [1]} with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(400)) + mock.post(f"{base_url}/database/read-data", json={"table_name": table_name, "filters": filters}).mock( + return_value=httpx.Response(400)) client = HiveAgentClient(base_url) with pytest.raises(Exception) as excinfo: - await client.get_entry_by_id(namespace, entry_id) - assert "Failed to get entry by ID" in str(excinfo.value) + await client.read_data(table_name, filters) + assert "Failed to read data" in str(excinfo.value) @pytest.mark.asyncio -async def test_update_entry_success(): - namespace = "test" - entry_id = "1" - data = {"key": "updated value"} - expected_response = {"id": "1", "key": "updated value"} +async def test_update_data_success(): + table_name = "test_table" + row_id = 1 + data = {"name": "Updated Test"} + expected_response = {"message": "Data updated successfully."} with respx.mock() as mock: - mock.put(f"{base_url}/api/entry/{namespace}/{entry_id}", json=data).mock( + mock.put(f"{base_url}/database/update-data", json={"table_name": table_name, "id": row_id, "data": data}).mock( return_value=httpx.Response(200, json=expected_response)) client = HiveAgentClient(base_url) - response = await client.update_entry(namespace, entry_id, data) + response = await client.update_data(table_name, row_id, data) assert response == expected_response @pytest.mark.asyncio -async def test_update_entry_failure(): - namespace = "test" - entry_id = "1" - data = {"key": "updated value"} +async def test_update_data_failure(): + table_name = "test_table" + row_id = 1 + data = {"name": "Updated Test"} with respx.mock() as mock: - mock.put(f"{base_url}/api/entry/{namespace}/{entry_id}", json=data).mock(return_value=httpx.Response(400)) + mock.put(f"{base_url}/database/update-data", json={"table_name": table_name, "id": row_id, "data": data}).mock( + return_value=httpx.Response(400)) client = HiveAgentClient(base_url) with pytest.raises(Exception) as excinfo: - await client.update_entry(namespace, entry_id, data) - assert "Failed to update entry" in str(excinfo.value) + await client.update_data(table_name, row_id, data) + assert "Failed to update data" in str(excinfo.value) @pytest.mark.asyncio -async def test_delete_entry_success(): - namespace = "test" - entry_id = "1" - expected_response = {"message": "Entry deleted successfully"} +async def test_delete_data_success(): + table_name = "test_table" + row_id = 1 + expected_response = {"message": "Data deleted successfully."} with respx.mock() as mock: - mock.delete(f"{base_url}/api/entry/{namespace}/{entry_id}").mock( + mock.request("DELETE", f"{base_url}/database/delete-data", content=json.dumps({"table_name": table_name, "id": row_id})).mock( return_value=httpx.Response(200, json=expected_response)) client = HiveAgentClient(base_url) - response = await client.delete_entry(namespace, entry_id) + response = await client.delete_data(table_name, row_id) assert response == expected_response @pytest.mark.asyncio -async def test_delete_entry_failure(): - namespace = "test" - entry_id = "1" +async def test_delete_data_failure(): + table_name = "test_table" + row_id = 1 with respx.mock() as mock: - mock.delete(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(400)) + mock.request("DELETE", f"{base_url}/database/delete-data", content=json.dumps({"table_name": table_name, "id": row_id})).mock( + return_value=httpx.Response(400)) client = HiveAgentClient(base_url) with pytest.raises(Exception) as excinfo: - await client.delete_entry(namespace, entry_id) - assert "Failed to delete entry" in str(excinfo.value) + await client.delete_data(table_name, row_id) + assert "Failed to delete data" in str(excinfo.value) @pytest.mark.asyncio @@ -187,101 +198,108 @@ async def test_close_http_client(): client = HiveAgentClient(base_url) await client.close() # test passes if this completes without error + # Negative Tests @pytest.mark.asyncio async def test_network_failure_handling(): - namespace = "test" - entry_id = "1" + table_name = "test_table" + row_id = 1 with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(504)) + mock.get(f"{base_url}/database/read-data").mock(return_value=httpx.Response(504)) - response = await httpx.AsyncClient().get(f"{base_url}/api/entry/{namespace}/{entry_id}") + response = await httpx.AsyncClient().get(f"{base_url}/database/read-data") with pytest.raises(Exception) as excinfo: check_response(response.status_code) assert "Unexpected status code" in str(excinfo.value) + @pytest.mark.asyncio async def test_out_of_scope(): - namespace = "test" - entry_id = "1" + table_name = "test_table" + row_id = 1 with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(404)) + mock.get(f"{base_url}/database/read-data").mock(return_value=httpx.Response(404)) - response = await httpx.AsyncClient().get(f"{base_url}/api/entry/{namespace}/{entry_id}") + response = await httpx.AsyncClient().get(f"{base_url}/database/read-data") with pytest.raises(Exception) as excinfo: check_response(response.status_code) assert "Unexpected status code" in str(excinfo.value) + @pytest.mark.asyncio async def test_heavy_load(): - namespace = "test" - entry_id = "1" + table_name = "test_table" + row_id = 1 with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(429)) + mock.get(f"{base_url}/database/read-data").mock(return_value=httpx.Response(429)) - response = await httpx.AsyncClient().get(f"{base_url}/api/entry/{namespace}/{entry_id}") + response = await httpx.AsyncClient().get(f"{base_url}/database/read-data") with pytest.raises(Exception) as excinfo: check_response(response.status_code) assert "Unexpected status code" in str(excinfo.value) + @pytest.mark.asyncio async def test_internal_server(): - namespace = "test" - entry_id = "1" + table_name = "test_table" + row_id = 1 with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(500)) + mock.get(f"{base_url}/database/read-data").mock(return_value=httpx.Response(500)) - response = await httpx.AsyncClient().get(f"{base_url}/api/entry/{namespace}/{entry_id}") + response = await httpx.AsyncClient().get(f"{base_url}/database/read-data") with pytest.raises(Exception) as excinfo: check_response(response.status_code) assert "Unexpected status code" in str(excinfo.value) + @pytest.mark.asyncio async def test_large_data_entry(): - namespace = "test" - entry_id = "1" + table_name = "test_table" + row_id = 1 with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(413)) + mock.get(f"{base_url}/database/read-data").mock(return_value=httpx.Response(413)) - response = await httpx.AsyncClient().get(f"{base_url}/api/entry/{namespace}/{entry_id}") + response = await httpx.AsyncClient().get(f"{base_url}/database/read-data") with pytest.raises(Exception) as excinfo: check_response(response.status_code) assert "Unexpected status code" in str(excinfo.value) + @pytest.mark.asyncio async def test_unprocessable_data_entry(): - namespace = "test" - entry_id = "1" + table_name = "test_table" + row_id = 1 with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(422)) + mock.get(f"{base_url}/database/read-data").mock(return_value=httpx.Response(422)) - response = await httpx.AsyncClient().get(f"{base_url}/api/entry/{namespace}/{entry_id}") + response = await httpx.AsyncClient().get(f"{base_url}/database/read-data") with pytest.raises(Exception) as excinfo: check_response(response.status_code) assert "Unexpected status code" in str(excinfo.value) + @pytest.mark.asyncio async def test_response_success(): - namespace = "test" - entry_id = "1" + table_name = "test_table" + row_id = 1 with respx.mock() as mock: - mock.get(f"{base_url}/api/entry/{namespace}/{entry_id}").mock(return_value=httpx.Response(200)) + mock.get(f"{base_url}/database/read-data").mock(return_value=httpx.Response(200)) - response = await httpx.AsyncClient().get(f"{base_url}/api/entry/{namespace}/{entry_id}") + response = await httpx.AsyncClient().get(f"{base_url}/database/read-data") check_response(response.status_code) assert response.status_code == 200 diff --git a/tutorial.md b/tutorial.md index afc2b93..e5f3d86 100644 --- a/tutorial.md +++ b/tutorial.md @@ -1,6 +1,6 @@ # Using the Hive Agent Client: A Tutorial -This tutorial will guide you through using the `HiveAgentClient`, a Python class designed to interact with a Hive Agent's API. The client facilitates various operations such as sending chat messages, managing entries, and streaming data. +This tutorial will guide you through using the `HiveAgentClient`, a Python class designed to interact with a Hive Agent's API. The client facilitates various operations such as sending chat messages and sending data to a Hive Agent. ## Setup @@ -36,79 +36,82 @@ async def send_message(content): print("Error:", e) ``` -## Creating an Entry +## Creating a Table -Create a new entry in a specified namespace: +Create a new table in the database: ```python -async def create_new_entry(namespace, data): +async def create_new_table(table_name, columns): try: - entry = await client.create_entry(namespace, data) - print("Created entry:", entry) + response = await client.create_table(table_name, columns) + print("Table creation response:", response) except Exception as e: print("Error:", e) ``` -## Streaming Entry Data +## Inserting Data -To stream data to an entry, use the `stream_entry_data` method. Ensure your data source is an asynchronous generator: +Insert data into a specified table: ```python -async def stream_data(namespace, data_stream): +async def insert_new_data(table_name, data): try: - async for message in client.stream_entry_data(namespace, data_stream): - print("Stream response:", message) + response = await client.insert_data(table_name, data) + print("Data insertion response:", response) except Exception as e: print("Error:", e) ``` -## Retrieving Entries +## Read Data -To get all entries from a namespace: +To get data from a table with optional filters: ```python -async def retrieve_entries(namespace): +async def retrieve_data(table_name, filters=None): try: - entries = await client.get_entries(namespace) - print("Entries:", entries) + data = await client.read_data(table_name, filters) + print("Retrieved data:", data) except Exception as e: print("Error:", e) ``` -## Retrieving a Specific Entry +## Read Specific Data with Filters -To retrieve a specific entry by ID: +To read specific data, use filters: ```python -async def retrieve_entry_by_id(namespace, entry_id): +async def retrieve_filtered_data(table_name): + filters = {"id": [1], "name": ["Test"]} try: - entry = await client.get_entry_by_id(namespace, entry_id) - print("Entry:", entry) + data = await client.read_data(table_name, filters) + print("Filtered data:", data) except Exception as e: print("Error:", e) ``` -## Updating an Entry +## Updating Data + +To update existing data in a table: -To update an existing entry: ```python -async def update_existing_entry(namespace, entry_id, data): +async def update_existing_data(table_name, row_id, new_data): try: - updated_entry = await client.update_entry(namespace, entry_id, data) - print("Updated entry:", updated_entry) + updated_data = await client.update_data(table_name, row_id, new_data) + print("Updated data:", updated_data) except Exception as e: print("Error:", e) ``` -## Deleting an Entry +## Deleting Data + +To delete data from a table: -To delete an entry: ```python -async def delete_existing_entry(namespace, entry_id): +async def delete_existing_data(table_name, row_id): try: - result = await client.delete_entry(namespace, entry_id) + result = await client.delete_data(table_name, row_id) print("Delete result:", result) except Exception as e: print("Error:", e) @@ -132,16 +135,16 @@ import asyncio async def main(): await send_message("Hello, world!") - await create_new_entry("my_namespace", {"key": "value"}) - await retrieve_entries("my_namespace") - await retrieve_entry_by_id("my_namespace", "entry_id") - await update_existing_entry("my_namespace", "entry_id", {"key": "new value"}) - await delete_existing_entry("my_namespace", "entry_id") + await create_new_table("my_table", {"id": "Integer", "name": "String"}) + await insert_new_data("my_table", {"name": "Test"}) + await retrieve_data("my_table", {"id": [1]}) + await update_existing_data("my_table", 1, {"name": "Updated Test"}) + await delete_existing_data("my_table", 1) await close_client() asyncio.run(main()) ``` -Replace `"my_namespace"`, `"entry_id"`, and other placeholders with your actual data. +Replace "my_table", {"id": "Integer", "name": "String"}, and other placeholders with your actual data. -This tutorial provides a basic overview of how to interact with the Hive Agent API using the `HiveAgentClient`. Adapt the examples to fit your application's requirements. +This tutorial provides a basic overview of how to interact with the Hive Agent API using the HiveAgentClient.