mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
Run ruff check --fix and ruff format across the repo to fix ~495 auto-fixable lint errors (mostly I001 import sorting) and reformat 225 files to match the py312 target. Co-authored-by: Hanzo Dev <[email protected]>
239 lines
6.1 KiB
Python
239 lines
6.1 KiB
Python
"""Async process operations.
|
|
|
|
Provides non-blocking subprocess execution.
|
|
All operations are fully async and safe for use in event loops.
|
|
"""
|
|
|
|
import shutil
|
|
import asyncio
|
|
from typing import Dict, List, Tuple, Union, Optional
|
|
|
|
|
|
async def run_command(
|
|
*args: str,
|
|
cwd: Optional[str] = None,
|
|
env: Optional[Dict[str, str]] = None,
|
|
timeout: Optional[float] = None,
|
|
capture: bool = True,
|
|
) -> Tuple[str, str, int]:
|
|
"""Run command asynchronously.
|
|
|
|
Args:
|
|
*args: Command and arguments
|
|
cwd: Working directory (default: current)
|
|
env: Environment variables to add
|
|
timeout: Timeout in seconds (default: None = no timeout)
|
|
capture: Capture stdout/stderr (default: True)
|
|
|
|
Returns:
|
|
Tuple of (stdout, stderr, exit_code)
|
|
|
|
Raises:
|
|
asyncio.TimeoutError: If timeout exceeded
|
|
FileNotFoundError: If command not found
|
|
|
|
Examples:
|
|
stdout, stderr, code = await run_command("ls", "-la")
|
|
stdout, stderr, code = await run_command("git", "status", cwd="/repo")
|
|
"""
|
|
import os
|
|
|
|
# Merge environment
|
|
full_env = None
|
|
if env:
|
|
full_env = os.environ.copy()
|
|
full_env.update(env)
|
|
|
|
# Create subprocess
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*args,
|
|
stdout=asyncio.subprocess.PIPE if capture else asyncio.subprocess.DEVNULL,
|
|
stderr=asyncio.subprocess.PIPE if capture else asyncio.subprocess.DEVNULL,
|
|
cwd=cwd,
|
|
env=full_env,
|
|
)
|
|
|
|
try:
|
|
if timeout:
|
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
else:
|
|
stdout, stderr = await proc.communicate()
|
|
|
|
stdout_str = stdout.decode("utf-8", errors="replace") if stdout else ""
|
|
stderr_str = stderr.decode("utf-8", errors="replace") if stderr else ""
|
|
|
|
return stdout_str, stderr_str, proc.returncode or 0
|
|
|
|
except asyncio.TimeoutError:
|
|
proc.kill()
|
|
await proc.wait()
|
|
raise
|
|
|
|
|
|
async def run_shell(
|
|
command: str,
|
|
shell: Optional[str] = None,
|
|
cwd: Optional[str] = None,
|
|
env: Optional[Dict[str, str]] = None,
|
|
timeout: Optional[float] = None,
|
|
capture: bool = True,
|
|
) -> Tuple[str, str, int]:
|
|
"""Run shell command asynchronously.
|
|
|
|
Args:
|
|
command: Shell command string
|
|
shell: Shell to use (default: auto-detect zsh > bash > sh)
|
|
cwd: Working directory (default: current)
|
|
env: Environment variables to add
|
|
timeout: Timeout in seconds (default: None = no timeout)
|
|
capture: Capture stdout/stderr (default: True)
|
|
|
|
Returns:
|
|
Tuple of (stdout, stderr, exit_code)
|
|
|
|
Raises:
|
|
asyncio.TimeoutError: If timeout exceeded
|
|
FileNotFoundError: If shell not found
|
|
|
|
Examples:
|
|
stdout, stderr, code = await run_shell("ls -la && pwd")
|
|
stdout, stderr, code = await run_shell("echo $PATH", shell="bash")
|
|
"""
|
|
import os
|
|
|
|
# Auto-detect shell
|
|
if not shell:
|
|
shell = _detect_shell()
|
|
|
|
# Merge environment
|
|
full_env = None
|
|
if env:
|
|
full_env = os.environ.copy()
|
|
full_env.update(env)
|
|
|
|
# Create subprocess with shell
|
|
proc = await asyncio.create_subprocess_exec(
|
|
shell,
|
|
"-c",
|
|
command,
|
|
stdout=asyncio.subprocess.PIPE if capture else asyncio.subprocess.DEVNULL,
|
|
stderr=asyncio.subprocess.PIPE if capture else asyncio.subprocess.DEVNULL,
|
|
cwd=cwd,
|
|
env=full_env,
|
|
)
|
|
|
|
try:
|
|
if timeout:
|
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
else:
|
|
stdout, stderr = await proc.communicate()
|
|
|
|
stdout_str = stdout.decode("utf-8", errors="replace") if stdout else ""
|
|
stderr_str = stderr.decode("utf-8", errors="replace") if stderr else ""
|
|
|
|
return stdout_str, stderr_str, proc.returncode or 0
|
|
|
|
except asyncio.TimeoutError:
|
|
proc.kill()
|
|
await proc.wait()
|
|
raise
|
|
|
|
|
|
async def check_command(command: str) -> bool:
|
|
"""Check if command is available.
|
|
|
|
Args:
|
|
command: Command name to check
|
|
|
|
Returns:
|
|
True if command is available
|
|
"""
|
|
loop = asyncio.get_running_loop()
|
|
result = await loop.run_in_executor(None, shutil.which, command)
|
|
return result is not None
|
|
|
|
|
|
def _detect_shell() -> str:
|
|
"""Detect the best available shell.
|
|
|
|
Returns:
|
|
Path to shell (zsh > bash > fish > sh)
|
|
"""
|
|
import os
|
|
|
|
# Check environment variable override
|
|
force_shell = os.environ.get("HANZO_SHELL")
|
|
if force_shell:
|
|
return force_shell
|
|
|
|
# Shell priority
|
|
shells = ["zsh", "bash", "fish", "sh"]
|
|
search_paths = [
|
|
"/opt/homebrew/bin",
|
|
"/usr/local/bin",
|
|
"/bin",
|
|
"/usr/bin",
|
|
]
|
|
|
|
for shell in shells:
|
|
for prefix in search_paths:
|
|
full_path = f"{prefix}/{shell}"
|
|
if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
|
|
return full_path
|
|
found = shutil.which(shell)
|
|
if found:
|
|
return found
|
|
|
|
return "sh"
|
|
|
|
|
|
async def which(command: str) -> Optional[str]:
|
|
"""Find command path asynchronously.
|
|
|
|
Args:
|
|
command: Command name to find
|
|
|
|
Returns:
|
|
Full path to command, or None if not found
|
|
"""
|
|
loop = asyncio.get_running_loop()
|
|
return await loop.run_in_executor(None, shutil.which, command)
|
|
|
|
|
|
async def start_background(
|
|
*args: str,
|
|
cwd: Optional[str] = None,
|
|
env: Optional[Dict[str, str]] = None,
|
|
) -> asyncio.subprocess.Process:
|
|
"""Start a background process.
|
|
|
|
Args:
|
|
*args: Command and arguments
|
|
cwd: Working directory (default: current)
|
|
env: Environment variables to add
|
|
|
|
Returns:
|
|
asyncio.subprocess.Process handle
|
|
|
|
Examples:
|
|
proc = await start_background("python", "server.py")
|
|
# Later...
|
|
proc.terminate()
|
|
await proc.wait()
|
|
"""
|
|
import os
|
|
|
|
# Merge environment
|
|
full_env = None
|
|
if env:
|
|
full_env = os.environ.copy()
|
|
full_env.update(env)
|
|
|
|
return await asyncio.create_subprocess_exec(
|
|
*args,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
cwd=cwd,
|
|
env=full_env,
|
|
)
|