"""Deployment commands for the WayyDB CLI. Supports: - Local: start uvicorn directly or via Docker - HuggingFace Spaces: push to HF Docker space - Docker: build and run container """ from __future__ import annotations import os import shutil import subprocess import sys from pathlib import Path from typing import Optional import typer from wayy_db.cli.config import load_config, save_config from wayy_db.cli.output import console, print_error, print_info, print_success deploy_app = typer.Typer( name="deploy", help="Deploy WayyDB service", no_args_is_help=True, ) def _find_project_root() -> Path: """Walk up from cwd looking for pyproject.toml with wayy-db.""" cwd = Path.cwd() for parent in [cwd, *cwd.parents]: toml = parent / "pyproject.toml" if toml.exists() and "wayy-db" in toml.read_text(): return parent raise FileNotFoundError( "Cannot find wayyDB project root (no pyproject.toml with wayy-db found). " "Run this command from within the wayyDB repo." ) def _run(cmd: list[str], cwd: Optional[Path] = None, check: bool = True) -> subprocess.CompletedProcess[str]: """Run a subprocess with live output.""" console.print(f"[dim]$ {' '.join(cmd)}[/dim]") return subprocess.run(cmd, cwd=cwd, check=check, text=True) # --- Local serve --- @deploy_app.command("local") def deploy_local( port: int = typer.Option(8080, "--port", "-p", help="Port to serve on"), host: str = typer.Option("0.0.0.0", "--host", help="Host to bind to"), data_path: str = typer.Option("./data/wayydb", "--data-path", "-d", help="Data directory"), workers: int = typer.Option(1, "--workers", "-w", help="Number of uvicorn workers"), ) -> None: """Start WayyDB server locally with uvicorn.""" os.makedirs(data_path, exist_ok=True) os.environ["WAYY_DATA_PATH"] = str(Path(data_path).resolve()) os.environ["PORT"] = str(port) os.environ["CORS_ORIGINS"] = "*" print_info("Data path", os.environ["WAYY_DATA_PATH"]) print_info("Serving on", f"http://{host}:{port}") console.print("[dim]Press Ctrl+C to stop[/dim]\n") try: _find_project_root() # Running from source — use api.main:app api_module = "api.main:app" except FileNotFoundError: # Installed package — api module should be importable api_module = "api.main:app" cmd = [ sys.executable, "-m", "uvicorn", api_module, "--host", host, "--port", str(port), "--workers", str(workers), ] try: _run(cmd) except KeyboardInterrupt: console.print("\n[dim]Server stopped.[/dim]") except subprocess.CalledProcessError: print_error("Failed to start server. Is uvicorn installed? (pip install wayy-db[api])") raise typer.Exit(1) # --- Docker --- @deploy_app.command("docker") def deploy_docker( port: int = typer.Option(8080, "--port", "-p", help="Host port to expose"), tag: str = typer.Option("wayydb:latest", "--tag", "-t", help="Docker image tag"), data_volume: str = typer.Option("wayydb-data", "--volume", "-v", help="Docker volume for data persistence"), build: bool = typer.Option(True, "--build/--no-build", help="Build image before running"), detach: bool = typer.Option(True, "--detach/--foreground", help="Run in background"), ) -> None: """Build and run WayyDB in Docker.""" if not shutil.which("docker"): print_error("Docker not found. Install Docker: https://docs.docker.com/get-docker/") raise typer.Exit(1) try: root = _find_project_root() except FileNotFoundError as e: print_error(str(e)) raise typer.Exit(1) if build: console.print("[bold]Building Docker image...[/bold]") _run(["docker", "build", "-t", tag, "."], cwd=root) print_success(f"Built {tag}") # Create volume if needed _run(["docker", "volume", "create", data_volume], check=False) # Stop existing container if running _run(["docker", "rm", "-f", "wayydb"], check=False) cmd = [ "docker", "run", "--name", "wayydb", "-p", f"{port}:8080", "-v", f"{data_volume}:/data/wayydb", "-e", "CORS_ORIGINS=*", ] if detach: cmd.append("-d") cmd.append(tag) _run(cmd) if detach: print_success(f"WayyDB running at http://localhost:{port}") print_info("Container", "wayydb") print_info("Volume", data_volume) console.print("[dim]Stop with: docker stop wayydb[/dim]") else: console.print("\n[dim]Container stopped.[/dim]") # --- HuggingFace Spaces --- @deploy_app.command("hf") def deploy_hf( repo: str = typer.Option("", "--repo", "-r", help="HF Space repo (user/name). Uses git remote 'hf' if not set."), token: Optional[str] = typer.Option(None, "--token", help="HuggingFace token (or set HF_TOKEN env var)"), ) -> None: """Deploy WayyDB to HuggingFace Spaces (Docker). Pushes the current repo state to a HuggingFace Space configured as a Docker space. The Space must already exist. Create one at: https://huggingface.co/new-space?sdk=docker """ if not shutil.which("git"): print_error("git not found") raise typer.Exit(1) try: root = _find_project_root() except FileNotFoundError as e: print_error(str(e)) raise typer.Exit(1) # Check if hf remote exists result = subprocess.run( ["git", "remote", "get-url", "hf"], capture_output=True, text=True, cwd=root ) hf_remote_exists = result.returncode == 0 existing_url = result.stdout.strip() if hf_remote_exists else "" if repo: hf_token = token or os.environ.get("HF_TOKEN", "") if hf_token: remote_url = f"https://user:{hf_token}@huggingface.co/spaces/{repo}" else: remote_url = f"https://huggingface.co/spaces/{repo}" if hf_remote_exists: _run(["git", "remote", "set-url", "hf", remote_url], cwd=root) else: _run(["git", "remote", "add", "hf", remote_url], cwd=root) elif not hf_remote_exists: print_error( "No 'hf' git remote found. Either:\n" " 1. Run: wayy deploy hf --repo /\n" " 2. Add manually: git remote add hf https://huggingface.co/spaces//" ) raise typer.Exit(1) console.print("[bold]Pushing to HuggingFace Spaces...[/bold]") # HF Spaces rejects pushes containing large files in history (even deleted ones). # Create a clean orphan commit with only the current tree to avoid this. try: # Create a temporary orphan branch with just the current working tree _run(["git", "checkout", "--orphan", "_hf_deploy"], cwd=root) _run(["git", "add", "-A"], cwd=root) _run( ["git", "commit", "-m", "Deploy wayyDB to HuggingFace Spaces", "--allow-empty"], cwd=root, ) _run(["git", "push", "hf", "_hf_deploy:main", "--force"], cwd=root) except subprocess.CalledProcessError: # Clean up temp branch before erroring subprocess.run(["git", "checkout", "main"], cwd=root, capture_output=True) subprocess.run(["git", "branch", "-D", "_hf_deploy"], cwd=root, capture_output=True) print_error("Push failed. Check your HF token and Space configuration.") raise typer.Exit(1) finally: # Always return to main branch and clean up subprocess.run(["git", "checkout", "main"], cwd=root, capture_output=True) subprocess.run(["git", "branch", "-D", "_hf_deploy"], cwd=root, capture_output=True) # Extract space URL from remote result = subprocess.run( ["git", "remote", "get-url", "hf"], capture_output=True, text=True, cwd=root ) remote_url = result.stdout.strip() # Parse space name from URL space_name = "" if "huggingface.co/spaces/" in remote_url: space_name = remote_url.split("huggingface.co/spaces/")[-1].rstrip(".git") elif repo: space_name = repo if space_name: space_url = f"https://huggingface.co/spaces/{space_name}" # HF Spaces with Docker get a direct URL space_direct = f"https://{space_name.replace('/', '-')}.hf.space" print_success(f"Deployed to HuggingFace Spaces") print_info("Space", space_url) print_info("API", space_direct) console.print(f"\n[dim]Connect with: wayy connect {space_direct}[/dim]") else: print_success("Pushed to HuggingFace Spaces") # --- Status / logs --- @deploy_app.command("stop") def deploy_stop( name: str = typer.Option("wayydb", "--name", "-n", help="Container name"), ) -> None: """Stop a running WayyDB Docker container.""" if not shutil.which("docker"): print_error("Docker not found") raise typer.Exit(1) _run(["docker", "stop", name], check=False) _run(["docker", "rm", name], check=False) print_success(f"Stopped {name}") @deploy_app.command("logs") def deploy_logs( name: str = typer.Option("wayydb", "--name", "-n", help="Container name"), follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"), tail: int = typer.Option(100, "--tail", help="Number of lines to show"), ) -> None: """View logs from a running WayyDB Docker container.""" if not shutil.which("docker"): print_error("Docker not found") raise typer.Exit(1) cmd = ["docker", "logs", "--tail", str(tail)] if follow: cmd.append("-f") cmd.append(name) try: _run(cmd, check=False) except KeyboardInterrupt: pass