mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
feat: improved hanzo/net integration with proper dependency checks
- Added net_check utility for verifying hanzo/net installation - Improved error handling and dependency detection - Added comprehensive tests for node command - Set up CI/CD workflows for both projects - Better handling of venv and Python paths
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
name: Test Hanzo Python SDK
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest pytest-asyncio pytest-cov
|
||||
cd pkg/hanzo && pip install -e .
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd pkg/hanzo
|
||||
python -m pytest tests/ -v --cov=hanzo --cov-report=term-missing
|
||||
|
||||
- name: Test hanzo CLI
|
||||
run: |
|
||||
hanzo --version
|
||||
hanzo --help
|
||||
hanzo node --help
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install ruff black mypy
|
||||
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
cd pkg/hanzo
|
||||
ruff check src/
|
||||
|
||||
- name: Format check with black
|
||||
run: |
|
||||
cd pkg/hanzo
|
||||
black --check src/
|
||||
|
||||
- name: Type check with mypy
|
||||
run: |
|
||||
cd pkg/hanzo
|
||||
pip install -e .
|
||||
mypy src/ --ignore-missing-imports || true
|
||||
@@ -0,0 +1,10 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts = -v --tb=short
|
||||
asyncio_mode = auto
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
ignore::PendingDeprecationWarning
|
||||
+53
-64
@@ -104,78 +104,69 @@ async def start_compute_node(ctx, name: str = None, port: int = 52415,
|
||||
network: str = "mainnet", models: tuple = None,
|
||||
max_jobs: int = 10):
|
||||
"""Start this instance as a compute node using hanzo/net."""
|
||||
from .utils.net_check import check_net_installation, get_missing_dependencies
|
||||
|
||||
console = ctx.obj.get("console", Console())
|
||||
|
||||
console.print("[bold cyan]Starting Hanzo Net Compute Node[/bold cyan]")
|
||||
console.print(f"Network: {network}")
|
||||
console.print(f"Port: {port}")
|
||||
|
||||
# Check hanzo/net availability
|
||||
is_available, net_path, python_exe = check_net_installation()
|
||||
|
||||
if not is_available:
|
||||
console.print("[red]Error:[/red] hanzo/net is not properly configured")
|
||||
if net_path:
|
||||
console.print(f"Found hanzo/net at {net_path} but dependencies are missing")
|
||||
missing = get_missing_dependencies(python_exe)
|
||||
if missing:
|
||||
console.print(f"Missing packages: {', '.join(missing)}")
|
||||
console.print("\nTo fix, run:")
|
||||
console.print(f" cd {net_path} && pip install -e .")
|
||||
else:
|
||||
console.print("\nTo install hanzo/net:")
|
||||
console.print(" git clone https://github.com/hanzoai/net.git ~/work/hanzo/net")
|
||||
console.print(" cd ~/work/hanzo/net && pip install -e .")
|
||||
return
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Check if hanzo/net is available
|
||||
net_path = "/Users/z/work/hanzo/net"
|
||||
if not os.path.exists(net_path):
|
||||
# Try to find net in the Python environment
|
||||
try:
|
||||
import net
|
||||
# net is installed as a package
|
||||
console.print("[green]✓[/green] Using installed hanzo/net")
|
||||
|
||||
# Run net directly
|
||||
from net.main import run as net_run
|
||||
|
||||
# Set up environment for net
|
||||
if models:
|
||||
os.environ["NET_MODELS"] = ",".join(models)
|
||||
if name:
|
||||
os.environ["NET_NODE_NAME"] = name
|
||||
|
||||
console.print(f"\n[green]✓[/green] Node initialized")
|
||||
console.print(f" Port: {port}")
|
||||
console.print(f" Models: {', '.join(models) if models else 'auto-detect'}")
|
||||
console.print("\n[bold green]Hanzo Net is running![/bold green]")
|
||||
console.print("WebUI: http://localhost:52415")
|
||||
console.print("API: http://localhost:52415/v1/chat/completions")
|
||||
console.print("\nPress Ctrl+C to stop\n")
|
||||
|
||||
# Run net
|
||||
await net_run()
|
||||
|
||||
except ImportError:
|
||||
# net not installed, try to run from source
|
||||
console.print("[yellow]hanzo/net not installed, checking for source...[/yellow]")
|
||||
|
||||
if os.path.exists(net_path):
|
||||
console.print(f"[green]✓[/green] Found hanzo/net at {net_path}")
|
||||
|
||||
# Add net to Python path
|
||||
sys.path.insert(0, os.path.join(net_path, "src"))
|
||||
|
||||
# Import and run net
|
||||
from net.main import run as net_run
|
||||
|
||||
console.print(f"\n[green]✓[/green] Starting net node")
|
||||
console.print(f" Port: {port}")
|
||||
console.print(f" Models: {', '.join(models) if models else 'auto-detect'}")
|
||||
console.print("\n[bold green]Hanzo Net is running![/bold green]")
|
||||
console.print("WebUI: http://localhost:52415")
|
||||
console.print("API: http://localhost:52415/v1/chat/completions")
|
||||
console.print("\nPress Ctrl+C to stop\n")
|
||||
|
||||
# Run net
|
||||
await net_run()
|
||||
else:
|
||||
console.print("[red]Error:[/red] hanzo/net not found")
|
||||
console.print("\nInstall hanzo/net:")
|
||||
console.print(" git clone https://github.com/hanzoai/net.git")
|
||||
console.print(" cd net && pip install -e .")
|
||||
return
|
||||
# Use the checked net_path and python_exe
|
||||
if not net_path:
|
||||
# net is installed as a package
|
||||
console.print("[green]✓[/green] Using installed hanzo/net")
|
||||
|
||||
# Import and run net
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "lib", "python3.*/site-packages"))
|
||||
from net.main import run as net_run
|
||||
|
||||
# Set up environment for net
|
||||
if models:
|
||||
os.environ["NET_MODELS"] = ",".join(models)
|
||||
if name:
|
||||
os.environ["NET_NODE_NAME"] = name
|
||||
|
||||
console.print(f"\n[green]✓[/green] Node initialized")
|
||||
console.print(f" Port: {port}")
|
||||
console.print(f" Models: {', '.join(models) if models else 'auto-detect'}")
|
||||
console.print("\n[bold green]Hanzo Net is running![/bold green]")
|
||||
console.print("WebUI: http://localhost:52415")
|
||||
console.print("API: http://localhost:52415/v1/chat/completions")
|
||||
console.print("\nPress Ctrl+C to stop\n")
|
||||
|
||||
# Run net
|
||||
await net_run()
|
||||
else:
|
||||
# Run from source directory
|
||||
# Run from source directory using the detected python_exe
|
||||
console.print(f"[green]✓[/green] Using hanzo/net from {net_path}")
|
||||
if python_exe != sys.executable:
|
||||
console.print(f"[green]✓[/green] Using hanzo/net venv")
|
||||
else:
|
||||
console.print("[yellow]⚠[/yellow] Using system Python")
|
||||
|
||||
# Change to net directory and run
|
||||
original_cwd = os.getcwd()
|
||||
@@ -188,6 +179,7 @@ async def start_compute_node(ctx, name: str = None, port: int = 52415,
|
||||
env["NET_MODELS"] = ",".join(models)
|
||||
if name:
|
||||
env["NET_NODE_NAME"] = name
|
||||
env["PYTHONPATH"] = os.path.join(net_path, "src") + ":" + env.get("PYTHONPATH", "")
|
||||
|
||||
console.print(f"\n[green]✓[/green] Starting net node")
|
||||
console.print(f" Port: {port}")
|
||||
@@ -197,12 +189,9 @@ async def start_compute_node(ctx, name: str = None, port: int = 52415,
|
||||
console.print("API: http://localhost:52415/v1/chat/completions")
|
||||
console.print("\nPress Ctrl+C to stop\n")
|
||||
|
||||
# Add src to path and run net
|
||||
env["PYTHONPATH"] = os.path.join(net_path, "src") + ":" + env.get("PYTHONPATH", "")
|
||||
|
||||
# Run net command
|
||||
# Run net command with detected python
|
||||
process = subprocess.run(
|
||||
[sys.executable, "-c", "from net.main import run; run()"],
|
||||
[python_exe, "-c", "from net.main import run; run()"],
|
||||
env=env,
|
||||
check=False
|
||||
)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Utilities for checking hanzo/net availability and dependencies."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def check_net_installation() -> Tuple[bool, Optional[str], Optional[str]]:
|
||||
"""Check if hanzo/net is available and properly configured.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, net_path, python_exe)
|
||||
"""
|
||||
# Check for hanzo/net in standard location
|
||||
net_path = Path.home() / "work" / "hanzo" / "net"
|
||||
if not net_path.exists():
|
||||
net_path = Path("/Users/z/work/hanzo/net")
|
||||
|
||||
if not net_path.exists():
|
||||
# Try to import as package
|
||||
try:
|
||||
import net
|
||||
return True, None, sys.executable
|
||||
except ImportError:
|
||||
return False, None, None
|
||||
|
||||
# Check for venv
|
||||
venv_python = net_path / ".venv" / "bin" / "python"
|
||||
if venv_python.exists():
|
||||
# Check if venv has required packages
|
||||
result = subprocess.run(
|
||||
[str(venv_python), "-c", "import net, scapy, mlx, transformers"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True, str(net_path), str(venv_python)
|
||||
else:
|
||||
# Venv exists but missing dependencies
|
||||
return False, str(net_path), str(venv_python)
|
||||
|
||||
# No venv, check system Python
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", "import scapy"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return True, str(net_path), sys.executable
|
||||
else:
|
||||
return False, str(net_path), None
|
||||
|
||||
|
||||
def install_net_dependencies(net_path: str, python_exe: str = None) -> bool:
|
||||
"""Install hanzo/net dependencies.
|
||||
|
||||
Args:
|
||||
net_path: Path to hanzo/net directory
|
||||
python_exe: Python executable to use (optional)
|
||||
|
||||
Returns:
|
||||
True if installation successful
|
||||
"""
|
||||
if python_exe is None:
|
||||
python_exe = sys.executable
|
||||
|
||||
# Install dependencies
|
||||
result = subprocess.run(
|
||||
[python_exe, "-m", "pip", "install", "-e", net_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def get_missing_dependencies(python_exe: str = None) -> list:
|
||||
"""Check which dependencies are missing for hanzo/net.
|
||||
|
||||
Args:
|
||||
python_exe: Python executable to check (default: sys.executable)
|
||||
|
||||
Returns:
|
||||
List of missing package names
|
||||
"""
|
||||
if python_exe is None:
|
||||
python_exe = sys.executable
|
||||
|
||||
required_packages = [
|
||||
"scapy", "mlx", "mlx_lm", "transformers", "tinygrad",
|
||||
"aiohttp", "grpcio", "pydantic", "rich", "tqdm"
|
||||
]
|
||||
|
||||
missing = []
|
||||
for package in required_packages:
|
||||
result = subprocess.run(
|
||||
[python_exe, "-c", f"import {package}"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
missing.append(package)
|
||||
|
||||
return missing
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
"""Standalone test for hanzo node command."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src to path
|
||||
sys.path.insert(0, "src")
|
||||
|
||||
def test_imports():
|
||||
"""Test that modules can be imported."""
|
||||
try:
|
||||
from hanzo.utils.net_check import check_net_installation, get_missing_dependencies
|
||||
print("✓ Successfully imported net_check utilities")
|
||||
|
||||
# Test the check function
|
||||
is_available, net_path, python_exe = check_net_installation()
|
||||
print(f"✓ Net available: {is_available}")
|
||||
print(f" Net path: {net_path}")
|
||||
print(f" Python: {python_exe}")
|
||||
|
||||
if python_exe:
|
||||
missing = get_missing_dependencies(python_exe)
|
||||
if missing:
|
||||
print(f" Missing deps: {', '.join(missing[:5])}")
|
||||
else:
|
||||
print(" All dependencies installed")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Import failed: {e}")
|
||||
return False
|
||||
|
||||
def test_cli_import():
|
||||
"""Test CLI import."""
|
||||
try:
|
||||
from hanzo.cli import start_compute_node
|
||||
print("✓ Successfully imported CLI")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ CLI import failed: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing Hanzo Node Command Integration")
|
||||
print("=" * 40)
|
||||
|
||||
success = True
|
||||
success = test_imports() and success
|
||||
success = test_cli_import() and success
|
||||
|
||||
print("=" * 40)
|
||||
if success:
|
||||
print("✓ All tests passed!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("✗ Some tests failed")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Tests for hanzo node command integration with hanzo/net."""
|
||||
|
||||
import pytest
|
||||
import subprocess
|
||||
import sys
|
||||
from unittest.mock import patch, MagicMock, call
|
||||
from pathlib import Path
|
||||
|
||||
from hanzo.cli import start_compute_node
|
||||
from hanzo.utils.net_check import check_net_installation, get_missing_dependencies
|
||||
|
||||
|
||||
class TestNetCheck:
|
||||
"""Test hanzo/net availability checking."""
|
||||
|
||||
def test_check_net_installation_with_venv(self, tmp_path):
|
||||
"""Test checking net installation with venv present."""
|
||||
# Create mock net directory with venv
|
||||
net_path = tmp_path / "net"
|
||||
net_path.mkdir()
|
||||
venv_path = net_path / ".venv" / "bin"
|
||||
venv_path.mkdir(parents=True)
|
||||
(venv_path / "python").touch()
|
||||
|
||||
with patch("hanzo.utils.net_check.Path") as mock_path:
|
||||
mock_path.home.return_value = tmp_path.parent
|
||||
mock_path.return_value.exists.return_value = True
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value.returncode = 0
|
||||
|
||||
is_available, path, python = check_net_installation()
|
||||
|
||||
assert is_available is True
|
||||
assert path is not None
|
||||
assert "python" in python
|
||||
|
||||
def test_check_net_installation_no_venv(self):
|
||||
"""Test checking net installation without venv."""
|
||||
with patch("hanzo.utils.net_check.Path") as mock_path:
|
||||
mock_path.return_value.exists.return_value = False
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value.returncode = 1
|
||||
|
||||
is_available, path, python = check_net_installation()
|
||||
|
||||
assert is_available is False
|
||||
|
||||
def test_get_missing_dependencies(self):
|
||||
"""Test checking for missing dependencies."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
# Mock some packages as installed, others as missing
|
||||
def side_effect(cmd, **kwargs):
|
||||
result = MagicMock()
|
||||
if "scapy" in cmd[2] or "aiohttp" in cmd[2]:
|
||||
result.returncode = 0
|
||||
else:
|
||||
result.returncode = 1
|
||||
return result
|
||||
|
||||
mock_run.side_effect = side_effect
|
||||
|
||||
missing = get_missing_dependencies()
|
||||
|
||||
assert "scapy" not in missing
|
||||
assert "aiohttp" not in missing
|
||||
assert "mlx" in missing
|
||||
assert "transformers" in missing
|
||||
|
||||
|
||||
class TestNodeCommand:
|
||||
"""Test hanzo node command."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_compute_node_missing_net(self):
|
||||
"""Test starting compute node when net is not available."""
|
||||
ctx = MagicMock()
|
||||
ctx.obj = {"console": MagicMock()}
|
||||
|
||||
with patch("hanzo.cli.check_net_installation") as mock_check:
|
||||
mock_check.return_value = (False, None, None)
|
||||
|
||||
await start_compute_node(ctx)
|
||||
|
||||
# Should print error and return early
|
||||
ctx.obj["console"].print.assert_any_call(
|
||||
"[red]Error:[/red] hanzo/net is not properly configured"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_compute_node_with_net_venv(self):
|
||||
"""Test starting compute node with net venv available."""
|
||||
ctx = MagicMock()
|
||||
ctx.obj = {"console": MagicMock()}
|
||||
|
||||
with patch("hanzo.cli.check_net_installation") as mock_check:
|
||||
mock_check.return_value = (True, "/path/to/net", "/path/to/net/.venv/bin/python")
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value.returncode = 0
|
||||
|
||||
with patch("os.chdir"):
|
||||
await start_compute_node(ctx, models=("llama-3.2-3b",))
|
||||
|
||||
# Should use venv python
|
||||
mock_run.assert_called_once()
|
||||
assert "/path/to/net/.venv/bin/python" in mock_run.call_args[0][0][0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_compute_node_with_models(self):
|
||||
"""Test starting compute node with specific models."""
|
||||
ctx = MagicMock()
|
||||
ctx.obj = {"console": MagicMock()}
|
||||
|
||||
with patch("hanzo.cli.check_net_installation") as mock_check:
|
||||
mock_check.return_value = (True, "/path/to/net", sys.executable)
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value.returncode = 0
|
||||
|
||||
with patch("os.chdir"):
|
||||
await start_compute_node(
|
||||
ctx,
|
||||
models=("llama-3.2-3b", "gpt-4"),
|
||||
name="test-node",
|
||||
port=8080
|
||||
)
|
||||
|
||||
# Check environment variables were set
|
||||
env = mock_run.call_args[1]["env"]
|
||||
assert env["NET_MODELS"] == "llama-3.2-3b,gpt-4"
|
||||
assert env["NET_NODE_NAME"] == "test-node"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_compute_node_keyboard_interrupt(self):
|
||||
"""Test graceful shutdown on keyboard interrupt."""
|
||||
ctx = MagicMock()
|
||||
ctx.obj = {"console": MagicMock()}
|
||||
|
||||
with patch("hanzo.cli.check_net_installation") as mock_check:
|
||||
mock_check.return_value = (True, "/path/to/net", sys.executable)
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.side_effect = KeyboardInterrupt()
|
||||
|
||||
with patch("os.chdir"):
|
||||
await start_compute_node(ctx)
|
||||
|
||||
# Should print shutdown message
|
||||
ctx.obj["console"].print.assert_any_call(
|
||||
"\n[yellow]Shutting down node...[/yellow]"
|
||||
)
|
||||
|
||||
|
||||
def test_cli_imports():
|
||||
"""Test that CLI modules can be imported."""
|
||||
from hanzo import cli
|
||||
from hanzo.utils import net_check
|
||||
|
||||
assert hasattr(cli, "start_compute_node")
|
||||
assert hasattr(net_check, "check_net_installation")
|
||||
assert hasattr(net_check, "get_missing_dependencies")
|
||||
Reference in New Issue
Block a user