Files
TinyTorch/tito/commands/book.py
Vijay Janapa Reddi e053dc9a71 Improve Jupyter Book styling and configuration
- Replace ugly gray background with clean white theme
- Add proper logo styling and configuration
- Update book chapters from module READMEs
- Add educational-ml-docs-architect agent
- Clean up custom CSS for better readability
- Configure logo.png in correct location
- Update tito book command with proper chapters
2025-09-18 09:48:01 -04:00

407 lines
15 KiB
Python

"""
Book command for TinyTorch CLI: builds and manages the Jupyter Book.
"""
import os
import subprocess
from argparse import ArgumentParser, Namespace
from pathlib import Path
from rich.panel import Panel
from .base import BaseCommand
NOTEBOOKS_DIR = "modules/source"
class BookCommand(BaseCommand):
@property
def name(self) -> str:
return "book"
@property
def description(self) -> str:
return "Build and manage the TinyTorch Jupyter Book"
def add_arguments(self, parser: ArgumentParser) -> None:
subparsers = parser.add_subparsers(
dest='book_command',
help='Book management commands',
metavar='COMMAND'
)
# Build command
build_parser = subparsers.add_parser(
'build',
help='Build the Jupyter Book locally'
)
# Publish command
publish_parser = subparsers.add_parser(
'publish',
help='Generate content, commit, and publish to GitHub'
)
publish_parser.add_argument(
'--message',
type=str,
default='📚 Update book content',
help='Commit message (default: "📚 Update book content")'
)
publish_parser.add_argument(
'--branch',
type=str,
default='main',
help='Branch to push to (default: main)'
)
# Clean command
clean_parser = subparsers.add_parser(
'clean',
help='Clean built book files'
)
# Serve command
serve_parser = subparsers.add_parser(
'serve',
help='Build and serve the Jupyter Book locally'
)
serve_parser.add_argument(
'--port',
type=int,
default=8001,
help='Port to serve on (default: 8001)'
)
serve_parser.add_argument(
'--no-build',
action='store_true',
help='Skip building and serve existing files'
)
def run(self, args: Namespace) -> int:
console = self.console
# Check if we're in the right directory
if not Path("book").exists():
console.print(Panel(
"[red]❌ book/ directory not found. Run this command from the TinyTorch root directory.[/red]",
title="Error",
border_style="red"
))
return 1
# Handle subcommands
if not hasattr(args, 'book_command') or not args.book_command:
console.print(Panel(
"[bold cyan]📚 TinyTorch Book Management[/bold cyan]\n\n"
"[bold]Available Commands:[/bold]\n"
" [bold green]build[/bold green] - Build the complete Jupyter Book\n"
" [bold green]serve[/bold green] - Build and serve the Jupyter Book locally\n"
" [bold green]publish[/bold green] - Generate content, commit, and publish to GitHub\n"
" [bold green]clean[/bold green] - Clean built book files\n\n"
"[bold]Quick Start:[/bold]\n"
" [dim]tito book publish[/dim] - Generate, commit, and publish to GitHub\n"
" [dim]tito book clean[/dim] - Clean built book files",
title="Book Commands",
border_style="bright_blue"
))
return 0
if args.book_command == 'build':
return self._build_book(args)
elif args.book_command == 'serve':
return self._serve_book(args)
elif args.book_command == 'publish':
return self._publish_book(args)
elif args.book_command == 'clean':
return self._clean_book()
else:
console.print(f"[red]Unknown book command: {args.book_command}[/red]")
return 1
def _generate_overview(self) -> int:
"""Generate overview pages from modules."""
console = self.console
console.print("🔄 Generating overview pages from modules...")
try:
os.chdir("book")
result = subprocess.run(
["python3", "convert_readmes.py"],
capture_output=True,
text=True
)
if result.returncode == 0:
console.print("✅ Overview pages generated successfully")
# Show summary from the output
for line in result.stdout.split('\n'):
if "✅ Created" in line or "🎉 Converted" in line:
console.print(f" {line.strip()}")
return 0
else:
console.print(f"[red]❌ Failed to generate overview pages: {result.stderr}[/red]")
return 1
except FileNotFoundError:
console.print("[red]❌ Python3 not found or convert_readmes.py missing[/red]")
return 1
except Exception as e:
console.print(f"[red]❌ Error generating overview pages: {e}[/red]")
return 1
finally:
os.chdir("..")
def _generate_all(self) -> int:
"""Convert READMEs to Jupyter Book chapters."""
console = self.console
console.print("🔄 Converting module READMEs to Jupyter Book chapters...")
# Step 1: Convert READMEs to chapters
console.print("📝 Step 1: Converting READMEs to chapters...")
try:
os.chdir("book")
result = subprocess.run([
"python3", "convert_readmes.py"
], capture_output=True, text=True)
if result.returncode == 0:
console.print(" ✅ All READMEs converted to chapters")
# Show summary from the output
for line in result.stdout.split('\n'):
if "✅ Created" in line or "🎉 Converted" in line:
console.print(f" {line.strip()}")
else:
console.print(f" ❌ Failed to convert READMEs: {result.stderr}")
return 1
except Exception as e:
console.print(f"[red]❌ Error converting READMEs: {e}[/red]")
return 1
finally:
os.chdir("..")
console.print("✅ Chapters generated successfully")
return 0
def _build_book(self, args: Namespace) -> int:
"""Build the Jupyter Book locally."""
console = self.console
# First generate all content (notebooks + overview pages)
console.print("📄 Step 1: Generating all content...")
if self._generate_all() != 0:
return 1
# Then build the book
console.print("📚 Step 2: Building Jupyter Book...")
try:
os.chdir("book")
result = subprocess.run(
["jupyter-book", "build", "."],
capture_output=True,
text=True
)
if result.returncode == 0:
console.print("✅ Book built successfully!")
# Extract and show the file path
if "file://" in result.stdout:
for line in result.stdout.split('\n'):
if "file://" in line:
console.print(f"🌐 View at: {line.strip()}")
break
console.print("📁 HTML files available in: book/_build/html/")
return 0
else:
console.print(f"[red]❌ Failed to build book[/red]")
if result.stderr:
console.print(f"Error details: {result.stderr}")
return 1
except FileNotFoundError:
console.print("[red]❌ jupyter-book not found. Install with: pip install jupyter-book[/red]")
return 1
except Exception as e:
console.print(f"[red]❌ Error building book: {e}[/red]")
return 1
finally:
os.chdir("..")
def _serve_book(self, args: Namespace) -> int:
"""Build and serve the Jupyter Book locally."""
console = self.console
# Build the book first unless --no-build is specified
if not args.no_build:
console.print("📚 Step 1: Building the book...")
if self._build_book(args) != 0:
return 1
console.print()
# Start the HTTP server
console.print("🌐 Step 2: Starting development server...")
console.print(f"📖 Open your browser to: [bold blue]http://localhost:{args.port}[/bold blue]")
console.print("🛑 Press [bold]Ctrl+C[/bold] to stop the server")
console.print()
book_dir = Path("book/_build/html")
if not book_dir.exists():
console.print("[red]❌ Built book not found. Run with --no-build=False to build first.[/red]")
return 1
try:
# Use Python's built-in HTTP server
subprocess.run([
"python3", "-m", "http.server", str(args.port),
"--directory", str(book_dir)
])
except KeyboardInterrupt:
console.print("\n🛑 Development server stopped")
except FileNotFoundError:
console.print("[red]❌ Python3 not found in PATH[/red]")
return 1
except Exception as e:
console.print(f"[red]❌ Error starting server: {e}[/red]")
return 1
return 0
def _clean_book(self) -> int:
"""Clean built book files."""
console = self.console
console.print("🧹 Cleaning book build files...")
try:
os.chdir("book")
result = subprocess.run(
["jupyter-book", "clean", "."],
capture_output=True,
text=True
)
if result.returncode == 0:
console.print("✅ Book files cleaned successfully")
return 0
else:
console.print(f"[red]❌ Failed to clean book files: {result.stderr}[/red]")
return 1
except FileNotFoundError:
console.print("[red]❌ jupyter-book not found[/red]")
return 1
except Exception as e:
console.print(f"[red]❌ Error cleaning book: {e}[/red]")
return 1
finally:
os.chdir("..")
def _publish_book(self, args: Namespace) -> int:
"""Generate content, commit, and publish to GitHub."""
console = self.console
console.print("🚀 Starting book publishing workflow...")
# Step 1: Generate all content
console.print("📝 Step 1: Generating all content...")
if self._generate_all() != 0:
console.print("[red]❌ Failed to generate content. Aborting publish.[/red]")
return 1
# Step 2: Check git status
console.print("🔍 Step 2: Checking git status...")
try:
result = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True,
text=True,
cwd="."
)
if result.returncode != 0:
console.print("[red]❌ Git not available or not a git repository[/red]")
return 1
changes = result.stdout.strip()
if not changes:
console.print("✅ No changes to publish")
return 0
except Exception as e:
console.print(f"[red]❌ Error checking git status: {e}[/red]")
return 1
# Step 3: Add and commit changes
console.print("📦 Step 3: Committing changes...")
try:
# Add all changes
subprocess.run(["git", "add", "."], check=True, cwd=".")
# Commit with message
subprocess.run([
"git", "commit", "-m", args.message
], check=True, cwd=".")
console.print(f"✅ Committed with message: {args.message}")
except subprocess.CalledProcessError as e:
console.print(f"[red]❌ Failed to commit changes: {e}[/red]")
return 1
except Exception as e:
console.print(f"[red]❌ Error during commit: {e}[/red]")
return 1
# Step 4: Push to GitHub
console.print(f"⬆️ Step 4: Pushing to {args.branch} branch...")
try:
result = subprocess.run([
"git", "push", "origin", args.branch
], capture_output=True, text=True, cwd=".")
if result.returncode == 0:
console.print(f"✅ Successfully pushed to {args.branch}")
else:
console.print(f"[red]❌ Failed to push: {result.stderr}[/red]")
return 1
except Exception as e:
console.print(f"[red]❌ Error during push: {e}[/red]")
return 1
# Step 5: Show deployment info
console.print("🌐 Step 5: Deployment initiated...")
console.print("✅ GitHub Actions will now:")
console.print(" 📚 Build the Jupyter Book")
console.print(" 🚀 Deploy to GitHub Pages")
console.print(" 🔗 Update live website")
# Try to get repository info for deployment URL
try:
result = subprocess.run([
"git", "remote", "get-url", "origin"
], capture_output=True, text=True, cwd=".")
if result.returncode == 0:
remote_url = result.stdout.strip()
if "github.com" in remote_url:
# Extract owner/repo from git URL
if remote_url.endswith(".git"):
remote_url = remote_url[:-4]
if remote_url.startswith("git@github.com:"):
repo_path = remote_url.replace("git@github.com:", "")
elif remote_url.startswith("https://github.com/"):
repo_path = remote_url.replace("https://github.com/", "")
else:
repo_path = None
if repo_path:
console.print(f"\n🔗 Monitor deployment: https://github.com/{repo_path}/actions")
console.print(f"📖 Live website: https://{repo_path.split('/')[0]}.github.io/{repo_path.split('/')[1]}/")
except Exception:
# Don't fail the whole command if we can't get repo info
pass
console.print("\n🎉 Publishing workflow complete!")
console.print("💡 Check GitHub Actions for deployment status")
return 0