mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
fix: update Python version requirement to 3.10+ for hanzo-net compatibility
- Remove Python 3.9 from test matrix (hanzo-net requires 3.10+) - Update hanzo package to require Python 3.10+ - Add test mock server helpers for improved testing
This commit is contained in:
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -16,7 +16,7 @@ dependencies = [
|
||||
"hanzo-net>=0.1.12",
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.8"
|
||||
requires-python = ">= 3.10"
|
||||
keywords = ["ai", "cli", "hanzo", "agents", "llm", "mcp", "local-ai", "private-ai"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
@@ -25,8 +25,6 @@ classifiers = [
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import time
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from threading import Thread
|
||||
|
||||
class MockHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
if self.path == "/utils/token_counter":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
response = {
|
||||
"model_used": "gpt-3.5-turbo",
|
||||
"request_model": "gpt-3.5-turbo",
|
||||
"tokenizer_type": "cl100k_base",
|
||||
"total_tokens": 10
|
||||
}
|
||||
self.wfile.write(json.dumps(response).encode())
|
||||
else:
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{}')
|
||||
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{}')
|
||||
|
||||
def do_PUT(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{}')
|
||||
|
||||
def do_DELETE(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{}')
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
server = HTTPServer(("127.0.0.1", 4010), MockHandler)
|
||||
thread = Thread(target=server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
print("Mock server ready on port 4010")
|
||||
|
||||
# Keep server running
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
server.shutdown()
|
||||
@@ -23,15 +23,11 @@ class TestContent:
|
||||
|
||||
@parametrize
|
||||
def test_method_retrieve(self, client: Hanzo, request: FixtureRequest) -> None:
|
||||
# For binary/text content endpoints, the SDK returns the content
|
||||
content = client.files.content.retrieve(
|
||||
file_id="file_id",
|
||||
provider="provider",
|
||||
)
|
||||
# The mock returns appropriate content based on mode
|
||||
assert content == "file content" or isinstance(
|
||||
content, (str, bytes, dict, object)
|
||||
)
|
||||
assert_matches_type(object, content, path=["response"])
|
||||
|
||||
@parametrize
|
||||
def test_raw_response_retrieve(
|
||||
@@ -44,7 +40,7 @@ class TestContent:
|
||||
assert response.is_closed is True
|
||||
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
|
||||
content = response.parse()
|
||||
assert content == "file content" or isinstance(content, (str, bytes, object))
|
||||
assert_matches_type(object, content, path=["response"])
|
||||
|
||||
@parametrize
|
||||
def test_streaming_response_retrieve(
|
||||
@@ -57,9 +53,7 @@ class TestContent:
|
||||
assert not response.is_closed
|
||||
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
|
||||
content = response.parse()
|
||||
assert content == "file content" or isinstance(
|
||||
content, (str, bytes, object)
|
||||
)
|
||||
assert_matches_type(object, content, path=["response"])
|
||||
assert cast(Any, response.is_closed) is True
|
||||
|
||||
@parametrize
|
||||
@@ -96,9 +90,7 @@ class TestAsyncContent:
|
||||
file_id="file_id",
|
||||
provider="provider",
|
||||
)
|
||||
assert content == "file content" or isinstance(
|
||||
content, (str, bytes, dict, object)
|
||||
)
|
||||
assert_matches_type(object, content, path=["response"])
|
||||
|
||||
@parametrize
|
||||
async def test_raw_response_retrieve(
|
||||
@@ -111,7 +103,7 @@ class TestAsyncContent:
|
||||
assert response.is_closed is True
|
||||
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
|
||||
content = await response.parse()
|
||||
assert content == "file content" or isinstance(content, (str, bytes, object))
|
||||
assert_matches_type(object, content, path=["response"])
|
||||
|
||||
@parametrize
|
||||
async def test_streaming_response_retrieve(
|
||||
|
||||
+68
-16
@@ -7,8 +7,13 @@ from httpx import Response
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api():
|
||||
def mock_api(request):
|
||||
"""Mock API responses for integration tests"""
|
||||
# Don't set up mocks if the test is using respx_mock directly
|
||||
if "respx_mock" in request.fixturenames:
|
||||
yield None
|
||||
return
|
||||
|
||||
with respx.mock(
|
||||
base_url="http://127.0.0.1:4010", assert_all_called=False
|
||||
) as respx_mock:
|
||||
@@ -103,14 +108,10 @@ def mock_api():
|
||||
|
||||
# Files
|
||||
respx_mock.route(method="GET", path__regex=r"/files/.*/content").mock(
|
||||
return_value=Response(
|
||||
200, content=b"file content", headers={"Content-Type": "text/plain"}
|
||||
)
|
||||
return_value=Response(200, json="file content")
|
||||
)
|
||||
respx_mock.route(method="GET", path__regex=r"/.*/v1/files/.*/content").mock(
|
||||
return_value=Response(
|
||||
200, content=b"file content", headers={"Content-Type": "text/plain"}
|
||||
)
|
||||
return_value=Response(200, json="file content")
|
||||
)
|
||||
respx_mock.post("/files").mock(
|
||||
return_value=Response(200, json={"id": "file-123"})
|
||||
@@ -138,6 +139,9 @@ def mock_api():
|
||||
respx_mock.get("/global/spend/tags").mock(return_value=Response(200, json=[]))
|
||||
respx_mock.post("/global/spend/reset").mock(return_value=Response(200, json={}))
|
||||
respx_mock.get("/global/spend/report").mock(return_value=Response(200, json=[]))
|
||||
|
||||
# Guardrails
|
||||
respx_mock.get("/guardrails/list").mock(return_value=Response(200, json={"guardrails": []}))
|
||||
|
||||
# Images
|
||||
respx_mock.post("/images/generations").mock(
|
||||
@@ -170,6 +174,15 @@ def mock_api():
|
||||
).mock(return_value=Response(200, json={}))
|
||||
|
||||
# Organization
|
||||
org_response = {
|
||||
"budget_id": "budget-123",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"created_by": "user-123",
|
||||
"models": ["gpt-3.5-turbo"],
|
||||
"organization_id": "org-123",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
"updated_by": "user-123",
|
||||
}
|
||||
respx_mock.get("/organization/info").mock(
|
||||
return_value=Response(
|
||||
200,
|
||||
@@ -186,15 +199,24 @@ def mock_api():
|
||||
respx_mock.get("/organization/info/deprecated").mock(
|
||||
return_value=Response(200, json={})
|
||||
)
|
||||
respx_mock.post("/organization/new").mock(return_value=Response(200, json=org_response))
|
||||
respx_mock.post("/organization").mock(return_value=Response(200, json={}))
|
||||
respx_mock.get("/organization").mock(
|
||||
return_value=Response(200, json={"data": []})
|
||||
return_value=Response(200, json=[])
|
||||
)
|
||||
respx_mock.put("/organization/update").mock(return_value=Response(200, json=org_response))
|
||||
respx_mock.put("/organization").mock(return_value=Response(200, json={}))
|
||||
respx_mock.delete("/organization/delete").mock(return_value=Response(200, json=org_response))
|
||||
respx_mock.delete("/organization").mock(return_value=Response(200, json={}))
|
||||
respx_mock.post("/organization/member_add").mock(
|
||||
return_value=Response(200, json=org_response)
|
||||
)
|
||||
respx_mock.post("/organization/members").mock(
|
||||
return_value=Response(200, json={})
|
||||
)
|
||||
respx_mock.delete("/organization/member_delete").mock(
|
||||
return_value=Response(200, json=org_response)
|
||||
)
|
||||
respx_mock.delete("/organization/members").mock(
|
||||
return_value=Response(200, json={})
|
||||
)
|
||||
@@ -208,6 +230,12 @@ def mock_api():
|
||||
)
|
||||
|
||||
# Team
|
||||
team_response = {
|
||||
"team_id": "team-123",
|
||||
"models": [],
|
||||
"members": [],
|
||||
"admins": []
|
||||
}
|
||||
respx_mock.route(method="GET", path__regex=r"/team/.*/callback").mock(
|
||||
return_value=Response(200, json={})
|
||||
)
|
||||
@@ -216,15 +244,21 @@ def mock_api():
|
||||
)
|
||||
respx_mock.post("/team/model").mock(return_value=Response(200, json={}))
|
||||
respx_mock.delete("/team/model").mock(return_value=Response(200, json={}))
|
||||
respx_mock.post("/team/new").mock(return_value=Response(200, json=team_response))
|
||||
respx_mock.post("/team").mock(return_value=Response(200, json={}))
|
||||
respx_mock.get("/team").mock(return_value=Response(200, json={"data": []}))
|
||||
respx_mock.get("/team").mock(return_value=Response(200, json=[]))
|
||||
respx_mock.get("/team/info").mock(return_value=Response(200, json=team_response))
|
||||
respx_mock.put("/team/update").mock(return_value=Response(200, json=team_response))
|
||||
respx_mock.put("/team").mock(return_value=Response(200, json={}))
|
||||
respx_mock.delete("/team/delete").mock(return_value=Response(200, json=team_response))
|
||||
respx_mock.delete("/team").mock(return_value=Response(200, json={}))
|
||||
respx_mock.post("/team/member_add").mock(return_value=Response(200, json=team_response))
|
||||
respx_mock.post("/team/members").mock(return_value=Response(200, json={}))
|
||||
respx_mock.delete("/team/member_delete").mock(return_value=Response(200, json=team_response))
|
||||
respx_mock.delete("/team/members").mock(return_value=Response(200, json={}))
|
||||
respx_mock.put("/team/members").mock(return_value=Response(200, json={}))
|
||||
respx_mock.post("/team/block").mock(return_value=Response(200, json={}))
|
||||
respx_mock.post("/team/unblock").mock(return_value=Response(200, json={}))
|
||||
respx_mock.post("/team/block").mock(return_value=Response(200, json=team_response))
|
||||
respx_mock.post("/team/unblock").mock(return_value=Response(200, json=team_response))
|
||||
|
||||
# Other endpoints
|
||||
respx_mock.get("/active/callbacks").mock(
|
||||
@@ -335,6 +369,9 @@ def mock_api():
|
||||
respx_mock.post("/key/regenerate").mock(
|
||||
return_value=Response(200, json={"key": "sk-456"})
|
||||
)
|
||||
respx_mock.post("/key/key/regenerate").mock(
|
||||
return_value=Response(200, json={"key": "sk-789"})
|
||||
)
|
||||
respx_mock.post("/key/block").mock(return_value=Response(200, json={}))
|
||||
respx_mock.post("/key/unblock").mock(return_value=Response(200, json={}))
|
||||
respx_mock.get("/key/health").mock(
|
||||
@@ -367,15 +404,18 @@ def mock_api():
|
||||
# Spend
|
||||
respx_mock.get("/spend/calculate").mock(return_value=Response(200, json={}))
|
||||
respx_mock.get("/spend/logs").mock(
|
||||
return_value=Response(200, json={"data": []})
|
||||
return_value=Response(200, json=[])
|
||||
)
|
||||
respx_mock.get("/spend/tags").mock(
|
||||
return_value=Response(200, json={"data": []})
|
||||
return_value=Response(200, json=[])
|
||||
)
|
||||
|
||||
# Test
|
||||
respx_mock.get("/test").mock(return_value=Response(200, json={}))
|
||||
|
||||
# Root endpoint for client tests
|
||||
respx_mock.get("/").mock(return_value=Response(200, json={}))
|
||||
|
||||
# Threads
|
||||
respx_mock.post("/threads").mock(return_value=Response(200, json={}))
|
||||
respx_mock.get("/threads").mock(return_value=Response(200, json={"data": []}))
|
||||
@@ -402,10 +442,17 @@ def mock_api():
|
||||
)
|
||||
|
||||
# User
|
||||
user_response = {
|
||||
"key": "sk-user-123",
|
||||
"user_id": "user-123"
|
||||
}
|
||||
respx_mock.post("/user/new").mock(return_value=Response(200, json=user_response))
|
||||
respx_mock.post("/user").mock(return_value=Response(200, json={}))
|
||||
respx_mock.get("/user").mock(return_value=Response(200, json={"data": []}))
|
||||
respx_mock.get("/user/info").mock(return_value=Response(200, json={}))
|
||||
respx_mock.get("/user").mock(return_value=Response(200, json=[]))
|
||||
respx_mock.get("/user/info").mock(return_value=Response(200, json=user_response))
|
||||
respx_mock.put("/user/update").mock(return_value=Response(200, json=user_response))
|
||||
respx_mock.put("/user").mock(return_value=Response(200, json={}))
|
||||
respx_mock.delete("/user/delete").mock(return_value=Response(200, json=user_response))
|
||||
respx_mock.delete("/user").mock(return_value=Response(200, json={}))
|
||||
|
||||
# Utils
|
||||
@@ -413,7 +460,12 @@ def mock_api():
|
||||
return_value=Response(200, json={})
|
||||
)
|
||||
respx_mock.post("/utils/token_counter").mock(
|
||||
return_value=Response(200, json={"count": 42})
|
||||
return_value=Response(200, json={
|
||||
"model_used": "gpt-3.5-turbo",
|
||||
"request_model": "gpt-3.5-turbo",
|
||||
"tokenizer_type": "cl100k_base",
|
||||
"total_tokens": 10
|
||||
})
|
||||
)
|
||||
respx_mock.post("/utils/transform_request").mock(
|
||||
return_value=Response(200, json={})
|
||||
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Comprehensive mock server for all test endpoints."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
|
||||
class MockHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
"""Handle GET requests."""
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
|
||||
# Default response
|
||||
response = {}
|
||||
|
||||
# Handle specific endpoints
|
||||
if "/files/" in self.path and "/content" in self.path:
|
||||
response = "file content"
|
||||
elif self.path == "/guardrails/list":
|
||||
response = {"guardrails": []}
|
||||
elif self.path == "/global/spend/tags":
|
||||
response = []
|
||||
elif self.path == "/global/spend/report":
|
||||
response = []
|
||||
elif self.path == "/organization/info":
|
||||
response = {
|
||||
"budget_id": "budget-123",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"created_by": "user-123",
|
||||
"models": ["gpt-3.5-turbo"],
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
"updated_by": "user-123"
|
||||
}
|
||||
elif self.path == "/config/pass_through_endpoint":
|
||||
response = {"endpoints": []}
|
||||
elif self.path == "/":
|
||||
response = {}
|
||||
else:
|
||||
response = {"data": []}
|
||||
|
||||
self.wfile.write(json.dumps(response).encode())
|
||||
|
||||
def do_POST(self):
|
||||
"""Handle POST requests."""
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
|
||||
# Default response
|
||||
response = {}
|
||||
|
||||
# Handle specific endpoints
|
||||
if self.path == "/utils/token_counter":
|
||||
response = {
|
||||
"model_used": "gpt-3.5-turbo",
|
||||
"request_model": "gpt-3.5-turbo",
|
||||
"tokenizer_type": "cl100k_base",
|
||||
"total_tokens": 10
|
||||
}
|
||||
elif self.path == "/chat/completions":
|
||||
response = {
|
||||
"id": "chatcmpl-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677652288,
|
||||
"model": "gpt-3.5-turbo",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": "Hello!"},
|
||||
"finish_reason": "stop"
|
||||
}]
|
||||
}
|
||||
|
||||
self.wfile.write(json.dumps(response).encode())
|
||||
|
||||
def do_PUT(self):
|
||||
"""Handle PUT requests."""
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"{}")
|
||||
|
||||
def do_DELETE(self):
|
||||
"""Handle DELETE requests."""
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
|
||||
# Handle specific endpoints
|
||||
if self.path == "/config/pass_through_endpoint":
|
||||
response = {"endpoints": []}
|
||||
else:
|
||||
response = {}
|
||||
|
||||
self.wfile.write(json.dumps(response).encode())
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass # Suppress logs
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
server = HTTPServer(("127.0.0.1", 4010), MockHandler)
|
||||
print("Mock server started on http://127.0.0.1:4010")
|
||||
print("Press Ctrl+C to stop")
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down mock server...")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user