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:
lyzno1
2025-10-11 17:45:42 +08:00
committed by GitHub
parent 5217017e69
commit a9b3539b90
11 changed files with 2183 additions and 488 deletions

View 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()

View 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()

View File

@@ -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()