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:
2025-08-13 22:42:53 -05:00
parent 8741dfd354
commit e7364fbc16
6 changed files with 247 additions and 33 deletions
+1 -1
View File
@@ -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
+1 -3
View File
@@ -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",
+58
View File
@@ -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()
+5 -13
View File
@@ -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
View File
@@ -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={})
+114
View File
@@ -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)