feat: migrate Python SDK to httpx with async/await support (#26726)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
250
sdks/python-client/tests/test_async_client.py
Normal file
250
sdks/python-client/tests/test_async_client.py
Normal file
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test suite for async client implementation in the Python SDK.
|
||||
|
||||
This test validates the async/await functionality using httpx.AsyncClient
|
||||
and ensures API parity with sync clients.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
|
||||
from dify_client.async_client import (
|
||||
AsyncDifyClient,
|
||||
AsyncChatClient,
|
||||
AsyncCompletionClient,
|
||||
AsyncWorkflowClient,
|
||||
AsyncWorkspaceClient,
|
||||
AsyncKnowledgeBaseClient,
|
||||
)
|
||||
|
||||
|
||||
class TestAsyncAPIParity(unittest.TestCase):
|
||||
"""Test that async clients have API parity with sync clients."""
|
||||
|
||||
def test_dify_client_api_parity(self):
|
||||
"""Test AsyncDifyClient has same methods as DifyClient."""
|
||||
from dify_client import DifyClient
|
||||
|
||||
sync_methods = {name for name in dir(DifyClient) if not name.startswith("_")}
|
||||
async_methods = {name for name in dir(AsyncDifyClient) if not name.startswith("_")}
|
||||
|
||||
# aclose is async-specific, close is sync-specific
|
||||
sync_methods.discard("close")
|
||||
async_methods.discard("aclose")
|
||||
|
||||
# Verify parity
|
||||
self.assertEqual(sync_methods, async_methods, "API parity mismatch for DifyClient")
|
||||
|
||||
def test_chat_client_api_parity(self):
|
||||
"""Test AsyncChatClient has same methods as ChatClient."""
|
||||
from dify_client import ChatClient
|
||||
|
||||
sync_methods = {name for name in dir(ChatClient) if not name.startswith("_")}
|
||||
async_methods = {name for name in dir(AsyncChatClient) if not name.startswith("_")}
|
||||
|
||||
sync_methods.discard("close")
|
||||
async_methods.discard("aclose")
|
||||
|
||||
self.assertEqual(sync_methods, async_methods, "API parity mismatch for ChatClient")
|
||||
|
||||
def test_completion_client_api_parity(self):
|
||||
"""Test AsyncCompletionClient has same methods as CompletionClient."""
|
||||
from dify_client import CompletionClient
|
||||
|
||||
sync_methods = {name for name in dir(CompletionClient) if not name.startswith("_")}
|
||||
async_methods = {name for name in dir(AsyncCompletionClient) if not name.startswith("_")}
|
||||
|
||||
sync_methods.discard("close")
|
||||
async_methods.discard("aclose")
|
||||
|
||||
self.assertEqual(sync_methods, async_methods, "API parity mismatch for CompletionClient")
|
||||
|
||||
def test_workflow_client_api_parity(self):
|
||||
"""Test AsyncWorkflowClient has same methods as WorkflowClient."""
|
||||
from dify_client import WorkflowClient
|
||||
|
||||
sync_methods = {name for name in dir(WorkflowClient) if not name.startswith("_")}
|
||||
async_methods = {name for name in dir(AsyncWorkflowClient) if not name.startswith("_")}
|
||||
|
||||
sync_methods.discard("close")
|
||||
async_methods.discard("aclose")
|
||||
|
||||
self.assertEqual(sync_methods, async_methods, "API parity mismatch for WorkflowClient")
|
||||
|
||||
def test_workspace_client_api_parity(self):
|
||||
"""Test AsyncWorkspaceClient has same methods as WorkspaceClient."""
|
||||
from dify_client import WorkspaceClient
|
||||
|
||||
sync_methods = {name for name in dir(WorkspaceClient) if not name.startswith("_")}
|
||||
async_methods = {name for name in dir(AsyncWorkspaceClient) if not name.startswith("_")}
|
||||
|
||||
sync_methods.discard("close")
|
||||
async_methods.discard("aclose")
|
||||
|
||||
self.assertEqual(sync_methods, async_methods, "API parity mismatch for WorkspaceClient")
|
||||
|
||||
def test_knowledge_base_client_api_parity(self):
|
||||
"""Test AsyncKnowledgeBaseClient has same methods as KnowledgeBaseClient."""
|
||||
from dify_client import KnowledgeBaseClient
|
||||
|
||||
sync_methods = {name for name in dir(KnowledgeBaseClient) if not name.startswith("_")}
|
||||
async_methods = {name for name in dir(AsyncKnowledgeBaseClient) if not name.startswith("_")}
|
||||
|
||||
sync_methods.discard("close")
|
||||
async_methods.discard("aclose")
|
||||
|
||||
self.assertEqual(sync_methods, async_methods, "API parity mismatch for KnowledgeBaseClient")
|
||||
|
||||
|
||||
class TestAsyncClientMocked(unittest.IsolatedAsyncioTestCase):
|
||||
"""Test async client with mocked httpx.AsyncClient."""
|
||||
|
||||
@patch("dify_client.async_client.httpx.AsyncClient")
|
||||
async def test_async_client_initialization(self, mock_httpx_async_client):
|
||||
"""Test async client initializes with httpx.AsyncClient."""
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_httpx_async_client.return_value = mock_client_instance
|
||||
|
||||
client = AsyncDifyClient("test-key", "https://api.dify.ai/v1")
|
||||
|
||||
# Verify httpx.AsyncClient was called
|
||||
mock_httpx_async_client.assert_called_once()
|
||||
self.assertEqual(client.api_key, "test-key")
|
||||
|
||||
await client.aclose()
|
||||
|
||||
@patch("dify_client.async_client.httpx.AsyncClient")
|
||||
async def test_async_context_manager(self, mock_httpx_async_client):
|
||||
"""Test async context manager works."""
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_httpx_async_client.return_value = mock_client_instance
|
||||
|
||||
async with AsyncDifyClient("test-key") as client:
|
||||
self.assertEqual(client.api_key, "test-key")
|
||||
|
||||
# Verify aclose was called
|
||||
mock_client_instance.aclose.assert_called_once()
|
||||
|
||||
@patch("dify_client.async_client.httpx.AsyncClient")
|
||||
async def test_async_send_request(self, mock_httpx_async_client):
|
||||
"""Test async _send_request method."""
|
||||
mock_response = AsyncMock()
|
||||
mock_response.json = AsyncMock(return_value={"result": "success"})
|
||||
mock_response.status_code = 200
|
||||
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.request = AsyncMock(return_value=mock_response)
|
||||
mock_httpx_async_client.return_value = mock_client_instance
|
||||
|
||||
async with AsyncDifyClient("test-key") as client:
|
||||
response = await client._send_request("GET", "/test")
|
||||
|
||||
# Verify request was called
|
||||
mock_client_instance.request.assert_called_once()
|
||||
call_args = mock_client_instance.request.call_args
|
||||
|
||||
# Verify parameters
|
||||
self.assertEqual(call_args[0][0], "GET")
|
||||
self.assertEqual(call_args[0][1], "/test")
|
||||
|
||||
@patch("dify_client.async_client.httpx.AsyncClient")
|
||||
async def test_async_chat_client(self, mock_httpx_async_client):
|
||||
"""Test AsyncChatClient functionality."""
|
||||
mock_response = AsyncMock()
|
||||
mock_response.text = '{"answer": "Hello!"}'
|
||||
mock_response.json = AsyncMock(return_value={"answer": "Hello!"})
|
||||
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.request = AsyncMock(return_value=mock_response)
|
||||
mock_httpx_async_client.return_value = mock_client_instance
|
||||
|
||||
async with AsyncChatClient("test-key") as client:
|
||||
response = await client.create_chat_message({}, "Hi", "user123")
|
||||
self.assertIn("answer", response.text)
|
||||
|
||||
@patch("dify_client.async_client.httpx.AsyncClient")
|
||||
async def test_async_completion_client(self, mock_httpx_async_client):
|
||||
"""Test AsyncCompletionClient functionality."""
|
||||
mock_response = AsyncMock()
|
||||
mock_response.text = '{"answer": "Response"}'
|
||||
mock_response.json = AsyncMock(return_value={"answer": "Response"})
|
||||
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.request = AsyncMock(return_value=mock_response)
|
||||
mock_httpx_async_client.return_value = mock_client_instance
|
||||
|
||||
async with AsyncCompletionClient("test-key") as client:
|
||||
response = await client.create_completion_message({"query": "test"}, "blocking", "user123")
|
||||
self.assertIn("answer", response.text)
|
||||
|
||||
@patch("dify_client.async_client.httpx.AsyncClient")
|
||||
async def test_async_workflow_client(self, mock_httpx_async_client):
|
||||
"""Test AsyncWorkflowClient functionality."""
|
||||
mock_response = AsyncMock()
|
||||
mock_response.json = AsyncMock(return_value={"result": "success"})
|
||||
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.request = AsyncMock(return_value=mock_response)
|
||||
mock_httpx_async_client.return_value = mock_client_instance
|
||||
|
||||
async with AsyncWorkflowClient("test-key") as client:
|
||||
response = await client.run({"input": "test"}, "blocking", "user123")
|
||||
data = await response.json()
|
||||
self.assertEqual(data["result"], "success")
|
||||
|
||||
@patch("dify_client.async_client.httpx.AsyncClient")
|
||||
async def test_async_workspace_client(self, mock_httpx_async_client):
|
||||
"""Test AsyncWorkspaceClient functionality."""
|
||||
mock_response = AsyncMock()
|
||||
mock_response.json = AsyncMock(return_value={"data": []})
|
||||
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.request = AsyncMock(return_value=mock_response)
|
||||
mock_httpx_async_client.return_value = mock_client_instance
|
||||
|
||||
async with AsyncWorkspaceClient("test-key") as client:
|
||||
response = await client.get_available_models("llm")
|
||||
data = await response.json()
|
||||
self.assertIn("data", data)
|
||||
|
||||
@patch("dify_client.async_client.httpx.AsyncClient")
|
||||
async def test_async_knowledge_base_client(self, mock_httpx_async_client):
|
||||
"""Test AsyncKnowledgeBaseClient functionality."""
|
||||
mock_response = AsyncMock()
|
||||
mock_response.json = AsyncMock(return_value={"data": [], "total": 0})
|
||||
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.request = AsyncMock(return_value=mock_response)
|
||||
mock_httpx_async_client.return_value = mock_client_instance
|
||||
|
||||
async with AsyncKnowledgeBaseClient("test-key") as client:
|
||||
response = await client.list_datasets()
|
||||
data = await response.json()
|
||||
self.assertIn("data", data)
|
||||
|
||||
@patch("dify_client.async_client.httpx.AsyncClient")
|
||||
async def test_all_async_client_classes(self, mock_httpx_async_client):
|
||||
"""Test all async client classes work with httpx.AsyncClient."""
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_httpx_async_client.return_value = mock_client_instance
|
||||
|
||||
clients = [
|
||||
AsyncDifyClient("key"),
|
||||
AsyncChatClient("key"),
|
||||
AsyncCompletionClient("key"),
|
||||
AsyncWorkflowClient("key"),
|
||||
AsyncWorkspaceClient("key"),
|
||||
AsyncKnowledgeBaseClient("key"),
|
||||
]
|
||||
|
||||
# Verify httpx.AsyncClient was called for each
|
||||
self.assertEqual(mock_httpx_async_client.call_count, 6)
|
||||
|
||||
# Clean up
|
||||
for client in clients:
|
||||
await client.aclose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
331
sdks/python-client/tests/test_httpx_migration.py
Normal file
331
sdks/python-client/tests/test_httpx_migration.py
Normal file
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test suite for httpx migration in the Python SDK.
|
||||
|
||||
This test validates that the migration from requests to httpx maintains
|
||||
backward compatibility and proper resource management.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from dify_client import (
|
||||
DifyClient,
|
||||
ChatClient,
|
||||
CompletionClient,
|
||||
WorkflowClient,
|
||||
WorkspaceClient,
|
||||
KnowledgeBaseClient,
|
||||
)
|
||||
|
||||
|
||||
class TestHttpxMigrationMocked(unittest.TestCase):
|
||||
"""Test cases for httpx migration with mocked requests."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.api_key = "test-api-key"
|
||||
self.base_url = "https://api.dify.ai/v1"
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_client_initialization(self, mock_httpx_client):
|
||||
"""Test that client initializes with httpx.Client."""
|
||||
mock_client_instance = Mock()
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
client = DifyClient(self.api_key, self.base_url)
|
||||
|
||||
# Verify httpx.Client was called with correct parameters
|
||||
mock_httpx_client.assert_called_once()
|
||||
call_kwargs = mock_httpx_client.call_args[1]
|
||||
self.assertEqual(call_kwargs["base_url"], self.base_url)
|
||||
|
||||
# Verify client properties
|
||||
self.assertEqual(client.api_key, self.api_key)
|
||||
self.assertEqual(client.base_url, self.base_url)
|
||||
|
||||
client.close()
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_context_manager_support(self, mock_httpx_client):
|
||||
"""Test that client works as context manager."""
|
||||
mock_client_instance = Mock()
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
with DifyClient(self.api_key, self.base_url) as client:
|
||||
self.assertEqual(client.api_key, self.api_key)
|
||||
|
||||
# Verify close was called
|
||||
mock_client_instance.close.assert_called_once()
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_manual_close(self, mock_httpx_client):
|
||||
"""Test manual close() method."""
|
||||
mock_client_instance = Mock()
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
client = DifyClient(self.api_key, self.base_url)
|
||||
client.close()
|
||||
|
||||
# Verify close was called
|
||||
mock_client_instance.close.assert_called_once()
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_send_request_httpx_compatibility(self, mock_httpx_client):
|
||||
"""Test _send_request uses httpx.Client.request properly."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_response.status_code = 200
|
||||
|
||||
mock_client_instance = Mock()
|
||||
mock_client_instance.request.return_value = mock_response
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
client = DifyClient(self.api_key, self.base_url)
|
||||
response = client._send_request("GET", "/test-endpoint")
|
||||
|
||||
# Verify httpx.Client.request was called correctly
|
||||
mock_client_instance.request.assert_called_once()
|
||||
call_args = mock_client_instance.request.call_args
|
||||
|
||||
# Verify method and endpoint
|
||||
self.assertEqual(call_args[0][0], "GET")
|
||||
self.assertEqual(call_args[0][1], "/test-endpoint")
|
||||
|
||||
# Verify headers contain authorization
|
||||
headers = call_args[1]["headers"]
|
||||
self.assertEqual(headers["Authorization"], f"Bearer {self.api_key}")
|
||||
self.assertEqual(headers["Content-Type"], "application/json")
|
||||
|
||||
client.close()
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_response_compatibility(self, mock_httpx_client):
|
||||
"""Test httpx.Response is compatible with requests.Response API."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"key": "value"}
|
||||
mock_response.text = '{"key": "value"}'
|
||||
mock_response.content = b'{"key": "value"}'
|
||||
mock_response.status_code = 200
|
||||
mock_response.headers = {"Content-Type": "application/json"}
|
||||
|
||||
mock_client_instance = Mock()
|
||||
mock_client_instance.request.return_value = mock_response
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
client = DifyClient(self.api_key, self.base_url)
|
||||
response = client._send_request("GET", "/test")
|
||||
|
||||
# Verify all common response methods work
|
||||
self.assertEqual(response.json(), {"key": "value"})
|
||||
self.assertEqual(response.text, '{"key": "value"}')
|
||||
self.assertEqual(response.content, b'{"key": "value"}')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers["Content-Type"], "application/json")
|
||||
|
||||
client.close()
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_all_client_classes_use_httpx(self, mock_httpx_client):
|
||||
"""Test that all client classes properly use httpx."""
|
||||
mock_client_instance = Mock()
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
clients = [
|
||||
DifyClient(self.api_key, self.base_url),
|
||||
ChatClient(self.api_key, self.base_url),
|
||||
CompletionClient(self.api_key, self.base_url),
|
||||
WorkflowClient(self.api_key, self.base_url),
|
||||
WorkspaceClient(self.api_key, self.base_url),
|
||||
KnowledgeBaseClient(self.api_key, self.base_url),
|
||||
]
|
||||
|
||||
# Verify httpx.Client was called for each client
|
||||
self.assertEqual(mock_httpx_client.call_count, 6)
|
||||
|
||||
# Clean up
|
||||
for client in clients:
|
||||
client.close()
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_json_parameter_handling(self, mock_httpx_client):
|
||||
"""Test that json parameter is passed correctly."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
|
||||
mock_client_instance = Mock()
|
||||
mock_client_instance.request.return_value = mock_response
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
client = DifyClient(self.api_key, self.base_url)
|
||||
test_data = {"key": "value", "number": 123}
|
||||
|
||||
client._send_request("POST", "/test", json=test_data)
|
||||
|
||||
# Verify json parameter was passed
|
||||
call_args = mock_client_instance.request.call_args
|
||||
self.assertEqual(call_args[1]["json"], test_data)
|
||||
|
||||
client.close()
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_params_parameter_handling(self, mock_httpx_client):
|
||||
"""Test that params parameter is passed correctly."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
|
||||
mock_client_instance = Mock()
|
||||
mock_client_instance.request.return_value = mock_response
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
client = DifyClient(self.api_key, self.base_url)
|
||||
test_params = {"page": 1, "limit": 20}
|
||||
|
||||
client._send_request("GET", "/test", params=test_params)
|
||||
|
||||
# Verify params parameter was passed
|
||||
call_args = mock_client_instance.request.call_args
|
||||
self.assertEqual(call_args[1]["params"], test_params)
|
||||
|
||||
client.close()
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_inheritance_chain(self, mock_httpx_client):
|
||||
"""Test that inheritance chain is maintained."""
|
||||
mock_client_instance = Mock()
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
# ChatClient inherits from DifyClient
|
||||
chat_client = ChatClient(self.api_key, self.base_url)
|
||||
self.assertIsInstance(chat_client, DifyClient)
|
||||
|
||||
# CompletionClient inherits from DifyClient
|
||||
completion_client = CompletionClient(self.api_key, self.base_url)
|
||||
self.assertIsInstance(completion_client, DifyClient)
|
||||
|
||||
# WorkflowClient inherits from DifyClient
|
||||
workflow_client = WorkflowClient(self.api_key, self.base_url)
|
||||
self.assertIsInstance(workflow_client, DifyClient)
|
||||
|
||||
# Clean up
|
||||
chat_client.close()
|
||||
completion_client.close()
|
||||
workflow_client.close()
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_nested_context_managers(self, mock_httpx_client):
|
||||
"""Test nested context managers work correctly."""
|
||||
mock_client_instance = Mock()
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
with DifyClient(self.api_key, self.base_url) as client1:
|
||||
with ChatClient(self.api_key, self.base_url) as client2:
|
||||
self.assertEqual(client1.api_key, self.api_key)
|
||||
self.assertEqual(client2.api_key, self.api_key)
|
||||
|
||||
# Both close methods should have been called
|
||||
self.assertEqual(mock_client_instance.close.call_count, 2)
|
||||
|
||||
|
||||
class TestChatClientHttpx(unittest.TestCase):
|
||||
"""Test ChatClient specific httpx integration."""
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_create_chat_message_httpx(self, mock_httpx_client):
|
||||
"""Test create_chat_message works with httpx."""
|
||||
mock_response = Mock()
|
||||
mock_response.text = '{"answer": "Hello!"}'
|
||||
mock_response.json.return_value = {"answer": "Hello!"}
|
||||
mock_response.status_code = 200
|
||||
|
||||
mock_client_instance = Mock()
|
||||
mock_client_instance.request.return_value = mock_response
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
with ChatClient("test-key") as client:
|
||||
response = client.create_chat_message({}, "Hi", "user123")
|
||||
self.assertIn("answer", response.text)
|
||||
self.assertEqual(response.json()["answer"], "Hello!")
|
||||
|
||||
|
||||
class TestCompletionClientHttpx(unittest.TestCase):
|
||||
"""Test CompletionClient specific httpx integration."""
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_create_completion_message_httpx(self, mock_httpx_client):
|
||||
"""Test create_completion_message works with httpx."""
|
||||
mock_response = Mock()
|
||||
mock_response.text = '{"answer": "Response"}'
|
||||
mock_response.json.return_value = {"answer": "Response"}
|
||||
mock_response.status_code = 200
|
||||
|
||||
mock_client_instance = Mock()
|
||||
mock_client_instance.request.return_value = mock_response
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
with CompletionClient("test-key") as client:
|
||||
response = client.create_completion_message({"query": "test"}, "blocking", "user123")
|
||||
self.assertIn("answer", response.text)
|
||||
|
||||
|
||||
class TestKnowledgeBaseClientHttpx(unittest.TestCase):
|
||||
"""Test KnowledgeBaseClient specific httpx integration."""
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_list_datasets_httpx(self, mock_httpx_client):
|
||||
"""Test list_datasets works with httpx."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"data": [], "total": 0}
|
||||
mock_response.status_code = 200
|
||||
|
||||
mock_client_instance = Mock()
|
||||
mock_client_instance.request.return_value = mock_response
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
with KnowledgeBaseClient("test-key") as client:
|
||||
response = client.list_datasets()
|
||||
data = response.json()
|
||||
self.assertIn("data", data)
|
||||
self.assertIn("total", data)
|
||||
|
||||
|
||||
class TestWorkflowClientHttpx(unittest.TestCase):
|
||||
"""Test WorkflowClient specific httpx integration."""
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_run_workflow_httpx(self, mock_httpx_client):
|
||||
"""Test run workflow works with httpx."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_response.status_code = 200
|
||||
|
||||
mock_client_instance = Mock()
|
||||
mock_client_instance.request.return_value = mock_response
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
with WorkflowClient("test-key") as client:
|
||||
response = client.run({"input": "test"}, "blocking", "user123")
|
||||
self.assertEqual(response.json()["result"], "success")
|
||||
|
||||
|
||||
class TestWorkspaceClientHttpx(unittest.TestCase):
|
||||
"""Test WorkspaceClient specific httpx integration."""
|
||||
|
||||
@patch("dify_client.client.httpx.Client")
|
||||
def test_get_available_models_httpx(self, mock_httpx_client):
|
||||
"""Test get_available_models works with httpx."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"data": []}
|
||||
mock_response.status_code = 200
|
||||
|
||||
mock_client_instance = Mock()
|
||||
mock_client_instance.request.return_value = mock_response
|
||||
mock_httpx_client.return_value = mock_client_instance
|
||||
|
||||
with WorkspaceClient("test-key") as client:
|
||||
response = client.get_available_models("llm")
|
||||
self.assertIn("data", response.json())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,416 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test suite for the new Service API functionality in the Python SDK.
|
||||
|
||||
This test validates the implementation of the missing Service API endpoints
|
||||
that were added to the Python SDK to achieve complete coverage.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import json
|
||||
|
||||
from dify_client import (
|
||||
DifyClient,
|
||||
ChatClient,
|
||||
WorkflowClient,
|
||||
KnowledgeBaseClient,
|
||||
WorkspaceClient,
|
||||
)
|
||||
|
||||
|
||||
class TestNewServiceAPIs(unittest.TestCase):
|
||||
"""Test cases for new Service API implementations."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.api_key = "test-api-key"
|
||||
self.base_url = "https://api.dify.ai/v1"
|
||||
|
||||
@patch("dify_client.client.requests.request")
|
||||
def test_app_info_apis(self, mock_request):
|
||||
"""Test application info APIs."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {
|
||||
"name": "Test App",
|
||||
"description": "Test Description",
|
||||
"tags": ["test", "api"],
|
||||
"mode": "chat",
|
||||
"author_name": "Test Author",
|
||||
}
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
client = DifyClient(self.api_key, self.base_url)
|
||||
|
||||
# Test get_app_info
|
||||
result = client.get_app_info()
|
||||
mock_request.assert_called_with(
|
||||
"GET",
|
||||
f"{self.base_url}/info",
|
||||
json=None,
|
||||
params=None,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
stream=False,
|
||||
)
|
||||
|
||||
# Test get_app_site_info
|
||||
client.get_app_site_info()
|
||||
mock_request.assert_called_with(
|
||||
"GET",
|
||||
f"{self.base_url}/site",
|
||||
json=None,
|
||||
params=None,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
stream=False,
|
||||
)
|
||||
|
||||
# Test get_file_preview
|
||||
file_id = "test-file-id"
|
||||
client.get_file_preview(file_id)
|
||||
mock_request.assert_called_with(
|
||||
"GET",
|
||||
f"{self.base_url}/files/{file_id}/preview",
|
||||
json=None,
|
||||
params=None,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
stream=False,
|
||||
)
|
||||
|
||||
@patch("dify_client.client.requests.request")
|
||||
def test_annotation_apis(self, mock_request):
|
||||
"""Test annotation APIs."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
client = ChatClient(self.api_key, self.base_url)
|
||||
|
||||
# Test annotation_reply_action - enable
|
||||
client.annotation_reply_action(
|
||||
action="enable",
|
||||
score_threshold=0.8,
|
||||
embedding_provider_name="openai",
|
||||
embedding_model_name="text-embedding-ada-002",
|
||||
)
|
||||
mock_request.assert_called_with(
|
||||
"POST",
|
||||
f"{self.base_url}/apps/annotation-reply/enable",
|
||||
json={
|
||||
"score_threshold": 0.8,
|
||||
"embedding_provider_name": "openai",
|
||||
"embedding_model_name": "text-embedding-ada-002",
|
||||
},
|
||||
params=None,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
stream=False,
|
||||
)
|
||||
|
||||
# Test annotation_reply_action - disable (now requires same fields as enable)
|
||||
client.annotation_reply_action(
|
||||
action="disable",
|
||||
score_threshold=0.5,
|
||||
embedding_provider_name="openai",
|
||||
embedding_model_name="text-embedding-ada-002",
|
||||
)
|
||||
|
||||
# Test annotation_reply_action with score_threshold=0 (edge case)
|
||||
client.annotation_reply_action(
|
||||
action="enable",
|
||||
score_threshold=0.0, # This should work and not raise ValueError
|
||||
embedding_provider_name="openai",
|
||||
embedding_model_name="text-embedding-ada-002",
|
||||
)
|
||||
|
||||
# Test get_annotation_reply_status
|
||||
client.get_annotation_reply_status("enable", "job-123")
|
||||
|
||||
# Test list_annotations
|
||||
client.list_annotations(page=1, limit=20, keyword="test")
|
||||
|
||||
# Test create_annotation
|
||||
client.create_annotation("Test question?", "Test answer.")
|
||||
|
||||
# Test update_annotation
|
||||
client.update_annotation("annotation-123", "Updated question?", "Updated answer.")
|
||||
|
||||
# Test delete_annotation
|
||||
client.delete_annotation("annotation-123")
|
||||
|
||||
# Verify all calls were made (8 calls: enable + disable + enable with 0.0 + 5 other operations)
|
||||
self.assertEqual(mock_request.call_count, 8)
|
||||
|
||||
@patch("dify_client.client.requests.request")
|
||||
def test_knowledge_base_advanced_apis(self, mock_request):
|
||||
"""Test advanced knowledge base APIs."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
dataset_id = "test-dataset-id"
|
||||
client = KnowledgeBaseClient(self.api_key, self.base_url, dataset_id)
|
||||
|
||||
# Test hit_testing
|
||||
client.hit_testing("test query", {"type": "vector"})
|
||||
mock_request.assert_called_with(
|
||||
"POST",
|
||||
f"{self.base_url}/datasets/{dataset_id}/hit-testing",
|
||||
json={"query": "test query", "retrieval_model": {"type": "vector"}},
|
||||
params=None,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
stream=False,
|
||||
)
|
||||
|
||||
# Test metadata operations
|
||||
client.get_dataset_metadata()
|
||||
client.create_dataset_metadata({"key": "value"})
|
||||
client.update_dataset_metadata("meta-123", {"key": "new_value"})
|
||||
client.get_built_in_metadata()
|
||||
client.manage_built_in_metadata("enable", {"type": "built_in"})
|
||||
client.update_documents_metadata([{"document_id": "doc1", "metadata": {"key": "value"}}])
|
||||
|
||||
# Test tag operations
|
||||
client.list_dataset_tags()
|
||||
client.bind_dataset_tags(["tag1", "tag2"])
|
||||
client.unbind_dataset_tag("tag1")
|
||||
client.get_dataset_tags()
|
||||
|
||||
# Verify multiple calls were made
|
||||
self.assertGreater(mock_request.call_count, 5)
|
||||
|
||||
@patch("dify_client.client.requests.request")
|
||||
def test_rag_pipeline_apis(self, mock_request):
|
||||
"""Test RAG pipeline APIs."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
dataset_id = "test-dataset-id"
|
||||
client = KnowledgeBaseClient(self.api_key, self.base_url, dataset_id)
|
||||
|
||||
# Test get_datasource_plugins
|
||||
client.get_datasource_plugins(is_published=True)
|
||||
mock_request.assert_called_with(
|
||||
"GET",
|
||||
f"{self.base_url}/datasets/{dataset_id}/pipeline/datasource-plugins",
|
||||
json=None,
|
||||
params={"is_published": True},
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
stream=False,
|
||||
)
|
||||
|
||||
# Test run_datasource_node
|
||||
client.run_datasource_node(
|
||||
node_id="node-123",
|
||||
inputs={"param": "value"},
|
||||
datasource_type="online_document",
|
||||
is_published=True,
|
||||
credential_id="cred-123",
|
||||
)
|
||||
|
||||
# Test run_rag_pipeline with blocking mode
|
||||
client.run_rag_pipeline(
|
||||
inputs={"query": "test"},
|
||||
datasource_type="online_document",
|
||||
datasource_info_list=[{"id": "ds1"}],
|
||||
start_node_id="start-node",
|
||||
is_published=True,
|
||||
response_mode="blocking",
|
||||
)
|
||||
|
||||
# Test run_rag_pipeline with streaming mode
|
||||
client.run_rag_pipeline(
|
||||
inputs={"query": "test"},
|
||||
datasource_type="online_document",
|
||||
datasource_info_list=[{"id": "ds1"}],
|
||||
start_node_id="start-node",
|
||||
is_published=True,
|
||||
response_mode="streaming",
|
||||
)
|
||||
|
||||
self.assertEqual(mock_request.call_count, 4)
|
||||
|
||||
@patch("dify_client.client.requests.request")
|
||||
def test_workspace_apis(self, mock_request):
|
||||
"""Test workspace APIs."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {
|
||||
"data": [{"name": "gpt-3.5-turbo", "type": "llm"}, {"name": "gpt-4", "type": "llm"}]
|
||||
}
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
client = WorkspaceClient(self.api_key, self.base_url)
|
||||
|
||||
# Test get_available_models
|
||||
result = client.get_available_models("llm")
|
||||
mock_request.assert_called_with(
|
||||
"GET",
|
||||
f"{self.base_url}/workspaces/current/models/model-types/llm",
|
||||
json=None,
|
||||
params=None,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
stream=False,
|
||||
)
|
||||
|
||||
@patch("dify_client.client.requests.request")
|
||||
def test_workflow_advanced_apis(self, mock_request):
|
||||
"""Test advanced workflow APIs."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
client = WorkflowClient(self.api_key, self.base_url)
|
||||
|
||||
# Test get_workflow_logs
|
||||
client.get_workflow_logs(keyword="test", status="succeeded", page=1, limit=20)
|
||||
mock_request.assert_called_with(
|
||||
"GET",
|
||||
f"{self.base_url}/workflows/logs",
|
||||
json=None,
|
||||
params={"page": 1, "limit": 20, "keyword": "test", "status": "succeeded"},
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
stream=False,
|
||||
)
|
||||
|
||||
# Test get_workflow_logs with additional filters
|
||||
client.get_workflow_logs(
|
||||
keyword="test",
|
||||
status="succeeded",
|
||||
page=1,
|
||||
limit=20,
|
||||
created_at__before="2024-01-01",
|
||||
created_at__after="2023-01-01",
|
||||
created_by_account="user123",
|
||||
)
|
||||
|
||||
# Test run_specific_workflow
|
||||
client.run_specific_workflow(
|
||||
workflow_id="workflow-123", inputs={"param": "value"}, response_mode="streaming", user="user-123"
|
||||
)
|
||||
|
||||
self.assertEqual(mock_request.call_count, 3)
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling for required parameters."""
|
||||
client = ChatClient(self.api_key, self.base_url)
|
||||
|
||||
# Test annotation_reply_action with missing required parameters would be a TypeError now
|
||||
# since parameters are required in method signature
|
||||
with self.assertRaises(TypeError):
|
||||
client.annotation_reply_action("enable")
|
||||
|
||||
# Test annotation_reply_action with explicit None values should raise ValueError
|
||||
with self.assertRaises(ValueError) as context:
|
||||
client.annotation_reply_action("enable", None, "provider", "model")
|
||||
|
||||
self.assertIn("cannot be None", str(context.exception))
|
||||
|
||||
# Test KnowledgeBaseClient without dataset_id
|
||||
kb_client = KnowledgeBaseClient(self.api_key, self.base_url)
|
||||
with self.assertRaises(ValueError) as context:
|
||||
kb_client.hit_testing("test query")
|
||||
|
||||
self.assertIn("dataset_id is not set", str(context.exception))
|
||||
|
||||
@patch("dify_client.client.open")
|
||||
@patch("dify_client.client.requests.request")
|
||||
def test_file_upload_apis(self, mock_request, mock_open):
|
||||
"""Test file upload APIs."""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
mock_file = MagicMock()
|
||||
mock_open.return_value.__enter__.return_value = mock_file
|
||||
|
||||
dataset_id = "test-dataset-id"
|
||||
client = KnowledgeBaseClient(self.api_key, self.base_url, dataset_id)
|
||||
|
||||
# Test upload_pipeline_file
|
||||
client.upload_pipeline_file("/path/to/test.pdf")
|
||||
|
||||
mock_open.assert_called_with("/path/to/test.pdf", "rb")
|
||||
mock_request.assert_called_once()
|
||||
|
||||
def test_comprehensive_coverage(self):
|
||||
"""Test that all previously missing APIs are now implemented."""
|
||||
|
||||
# Test DifyClient methods
|
||||
dify_methods = ["get_app_info", "get_app_site_info", "get_file_preview"]
|
||||
client = DifyClient(self.api_key)
|
||||
for method in dify_methods:
|
||||
self.assertTrue(hasattr(client, method), f"DifyClient missing method: {method}")
|
||||
|
||||
# Test ChatClient annotation methods
|
||||
chat_methods = [
|
||||
"annotation_reply_action",
|
||||
"get_annotation_reply_status",
|
||||
"list_annotations",
|
||||
"create_annotation",
|
||||
"update_annotation",
|
||||
"delete_annotation",
|
||||
]
|
||||
chat_client = ChatClient(self.api_key)
|
||||
for method in chat_methods:
|
||||
self.assertTrue(hasattr(chat_client, method), f"ChatClient missing method: {method}")
|
||||
|
||||
# Test WorkflowClient advanced methods
|
||||
workflow_methods = ["get_workflow_logs", "run_specific_workflow"]
|
||||
workflow_client = WorkflowClient(self.api_key)
|
||||
for method in workflow_methods:
|
||||
self.assertTrue(hasattr(workflow_client, method), f"WorkflowClient missing method: {method}")
|
||||
|
||||
# Test KnowledgeBaseClient advanced methods
|
||||
kb_methods = [
|
||||
"hit_testing",
|
||||
"get_dataset_metadata",
|
||||
"create_dataset_metadata",
|
||||
"update_dataset_metadata",
|
||||
"get_built_in_metadata",
|
||||
"manage_built_in_metadata",
|
||||
"update_documents_metadata",
|
||||
"list_dataset_tags",
|
||||
"bind_dataset_tags",
|
||||
"unbind_dataset_tag",
|
||||
"get_dataset_tags",
|
||||
"get_datasource_plugins",
|
||||
"run_datasource_node",
|
||||
"run_rag_pipeline",
|
||||
"upload_pipeline_file",
|
||||
]
|
||||
kb_client = KnowledgeBaseClient(self.api_key)
|
||||
for method in kb_methods:
|
||||
self.assertTrue(hasattr(kb_client, method), f"KnowledgeBaseClient missing method: {method}")
|
||||
|
||||
# Test WorkspaceClient methods
|
||||
workspace_methods = ["get_available_models"]
|
||||
workspace_client = WorkspaceClient(self.api_key)
|
||||
for method in workspace_methods:
|
||||
self.assertTrue(hasattr(workspace_client, method), f"WorkspaceClient missing method: {method}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user