mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
295 lines
8.6 KiB
Python
Executable File
295 lines
8.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Check package versions and publish to PyPI if newer version is available.
|
|
This script is designed to run in CI on every push to main.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import json
|
|
import subprocess
|
|
import urllib.error
|
|
import urllib.request
|
|
from typing import Tuple, Optional
|
|
from pathlib import Path
|
|
|
|
# Colors for output
|
|
RED = "\033[0;31m"
|
|
GREEN = "\033[0;32m"
|
|
YELLOW = "\033[1;33m"
|
|
BLUE = "\033[0;34m"
|
|
NC = "\033[0m" # No Color
|
|
|
|
|
|
def print_info(msg: str):
|
|
print(f"{GREEN}[INFO]{NC} {msg}")
|
|
|
|
|
|
def print_warn(msg: str):
|
|
print(f"{YELLOW}[WARN]{NC} {msg}")
|
|
|
|
|
|
def print_error(msg: str):
|
|
print(f"{RED}[ERROR]{NC} {msg}")
|
|
|
|
|
|
def print_action(msg: str):
|
|
print(f"{BLUE}[ACTION]{NC} {msg}")
|
|
|
|
|
|
def get_local_version(package_dir: Path) -> Optional[str]:
|
|
"""Extract version from pyproject.toml."""
|
|
pyproject_path = package_dir / "pyproject.toml"
|
|
if not pyproject_path.exists():
|
|
return None
|
|
|
|
with open(pyproject_path, "r") as f:
|
|
content = f.read()
|
|
|
|
# Match version = "x.y.z" or version = 'x.y.z'
|
|
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
|
if match:
|
|
return match.group(1)
|
|
return None
|
|
|
|
|
|
def get_pypi_version(package_name: str) -> Optional[str]:
|
|
"""Get the latest version from PyPI."""
|
|
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=30) as response: # noqa: S310
|
|
data = json.loads(response.read())
|
|
return data.get("info", {}).get("version")
|
|
except urllib.error.HTTPError as e:
|
|
if e.code == 404:
|
|
# Package doesn't exist on PyPI yet
|
|
return None
|
|
raise
|
|
except Exception as e:
|
|
print_warn(f"Could not fetch PyPI version for {package_name}: {e}")
|
|
return None
|
|
|
|
|
|
def parse_version(version: str) -> Tuple[int, ...]:
|
|
"""Parse version string to tuple for comparison."""
|
|
# Remove any pre-release or build metadata
|
|
version = re.split(r"[-+]", version)[0]
|
|
return tuple(int(x) for x in version.split("."))
|
|
|
|
|
|
def is_newer_version(local_version: str, pypi_version: Optional[str]) -> bool:
|
|
"""Check if local version is newer than PyPI version."""
|
|
if pypi_version is None:
|
|
# Package doesn't exist on PyPI
|
|
return True
|
|
|
|
try:
|
|
local_tuple = parse_version(local_version)
|
|
pypi_tuple = parse_version(pypi_version)
|
|
return local_tuple > pypi_tuple
|
|
except (ValueError, AttributeError):
|
|
# If we can't parse versions, compare as strings
|
|
return local_version != pypi_version
|
|
|
|
|
|
def build_package(package_dir: Path) -> bool:
|
|
"""Build the Python package."""
|
|
print_info(f"Building package in {package_dir}")
|
|
|
|
# Clean previous builds
|
|
for dir_name in ["dist", "build"]:
|
|
dir_path = package_dir / dir_name
|
|
if dir_path.exists():
|
|
subprocess.run(["rm", "-rf", str(dir_path)], check=True)
|
|
|
|
# Remove .egg-info directories
|
|
for egg_info in package_dir.glob("*.egg-info"):
|
|
subprocess.run(["rm", "-rf", str(egg_info)], check=True)
|
|
|
|
# Build the package
|
|
result = subprocess.run(["python", "-m", "build"], cwd=package_dir, capture_output=True, text=True)
|
|
|
|
if result.returncode != 0:
|
|
print_error(f"Build failed: {result.stderr}")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def publish_package(package_dir: Path, package_name: str) -> bool:
|
|
"""Publish package to PyPI."""
|
|
print_action(f"Publishing {package_name} to PyPI")
|
|
|
|
# Check for PyPI token
|
|
pypi_token = os.environ.get("PYPI_TOKEN") or os.environ.get("HANZO_PYPI_TOKEN")
|
|
if not pypi_token:
|
|
print_error("PYPI_TOKEN or HANZO_PYPI_TOKEN environment variable not set")
|
|
return False
|
|
|
|
# Upload to PyPI
|
|
result = subprocess.run(
|
|
["python", "-m", "twine", "upload", "dist/*", "--skip-existing"],
|
|
cwd=package_dir,
|
|
env={**os.environ, "TWINE_USERNAME": "__token__", "TWINE_PASSWORD": pypi_token},
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
print_error(f"Upload failed: {result.stderr}")
|
|
return False
|
|
|
|
print_info(f"✅ Successfully published {package_name}")
|
|
return True
|
|
|
|
|
|
def check_and_publish_package(package_name: str, package_dir: Path) -> bool:
|
|
"""Check version and publish if newer."""
|
|
print_info(f"Checking {package_name}...")
|
|
|
|
# Get local version
|
|
local_version = get_local_version(package_dir)
|
|
if not local_version:
|
|
print_warn(f"Could not find version for {package_name}")
|
|
return False
|
|
|
|
print_info(f" Local version: {local_version}")
|
|
|
|
# Get PyPI version
|
|
pypi_version = get_pypi_version(package_name)
|
|
if pypi_version:
|
|
print_info(f" PyPI version: {pypi_version}")
|
|
else:
|
|
print_info(f" PyPI version: Not published yet")
|
|
|
|
# Check if we need to publish
|
|
if is_newer_version(local_version, pypi_version):
|
|
print_action(f"📦 New version detected for {package_name}: {local_version}")
|
|
|
|
# Build the package
|
|
if not build_package(package_dir):
|
|
return False
|
|
|
|
# Publish to PyPI
|
|
return publish_package(package_dir, package_name)
|
|
else:
|
|
print_info(f" ✓ {package_name} is up to date")
|
|
return True
|
|
|
|
|
|
def main():
|
|
"""Main function to check and publish all packages."""
|
|
# Define packages in dependency order
|
|
packages = [
|
|
"hanzo-network",
|
|
"hanzo-memory",
|
|
"hanzo-agents",
|
|
"hanzo-aci",
|
|
"hanzo-mcp",
|
|
"hanzo-repl",
|
|
"hanzo",
|
|
]
|
|
|
|
# Get repository root
|
|
script_dir = Path(__file__).parent
|
|
repo_root = script_dir.parent
|
|
pkg_dir = repo_root / "pkg"
|
|
|
|
# Install required tools
|
|
print_info("Installing build tools...")
|
|
try:
|
|
# Try with pip first
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"pip",
|
|
"install",
|
|
"--quiet",
|
|
"--upgrade",
|
|
"pip",
|
|
"build",
|
|
"twine",
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
# Fall back to installing without pip upgrade
|
|
try:
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", "--quiet", "build", "twine"],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
except Exception:
|
|
print_warn("Could not install build tools. Make sure pip, build, and twine are available.")
|
|
print_info("You can install them with: pip install build twine")
|
|
|
|
# Track results
|
|
published = []
|
|
failed = []
|
|
skipped = []
|
|
|
|
# Check and publish each package
|
|
for package_name in packages:
|
|
package_dir = pkg_dir / package_name
|
|
|
|
if not package_dir.exists():
|
|
print_warn(f"Package directory {package_dir} does not exist")
|
|
skipped.append(package_name)
|
|
continue
|
|
|
|
try:
|
|
local_version = get_local_version(package_dir)
|
|
pypi_version = get_pypi_version(package_name)
|
|
|
|
if is_newer_version(local_version, pypi_version):
|
|
if check_and_publish_package(package_name, package_dir):
|
|
published.append(f"{package_name} ({local_version})")
|
|
else:
|
|
failed.append(package_name)
|
|
else:
|
|
skipped.append(f"{package_name} (already at {local_version})")
|
|
except Exception as e:
|
|
print_error(f"Error processing {package_name}: {e}")
|
|
failed.append(package_name)
|
|
|
|
# Print summary
|
|
print("\n" + "=" * 60)
|
|
print("📊 SUMMARY")
|
|
print("=" * 60)
|
|
|
|
if published:
|
|
print(f"\n{GREEN}✅ Published ({len(published)}):{NC}")
|
|
for pkg in published:
|
|
print(f" • {pkg}")
|
|
|
|
if skipped:
|
|
print(f"\n{BLUE}⏭️ Skipped ({len(skipped)}):{NC}")
|
|
for pkg in skipped:
|
|
print(f" • {pkg}")
|
|
|
|
if failed:
|
|
print(f"\n{RED}❌ Failed ({len(failed)}):{NC}")
|
|
for pkg in failed:
|
|
print(f" • {pkg}")
|
|
sys.exit(1)
|
|
|
|
# Set GitHub Actions output if running in CI
|
|
if os.environ.get("GITHUB_ACTIONS"):
|
|
if published:
|
|
print(
|
|
f"::notice title=Published Packages::Published {len(published)} packages to PyPI: {', '.join(published)}"
|
|
)
|
|
else:
|
|
print("::notice title=No Updates::All packages are up to date on PyPI")
|
|
|
|
print(f"\n{GREEN}✨ All packages processed successfully!{NC}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|