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:
2025-08-06 21:18:39 -05:00
parent b8b4d723e8
commit b90e7752eb
6 changed files with 462 additions and 64 deletions
+71
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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
)
+107
View File
@@ -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
+58
View File
@@ -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)
+163
View File
@@ -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")