Init
This commit is contained in:
		
							
								
								
									
										23
									
								
								.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.env
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# JupyterHub Notebook Viewer Configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Core Settings
 | 
				
			||||||
 | 
					JUPYTERHUB_SHARED_DIR=/shared
 | 
				
			||||||
 | 
					APP_TITLE=JupyterHub Notebook Viewer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Flask Settings
 | 
				
			||||||
 | 
					FLASK_HOST=0.0.0.0
 | 
				
			||||||
 | 
					FLASK_PORT=5000
 | 
				
			||||||
 | 
					FLASK_DEBUG=True
 | 
				
			||||||
 | 
					FLASK_SECRET_KEY=your-secret-key-change-in-production
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# File Handling
 | 
				
			||||||
 | 
					MAX_FILE_SIZE=16777216
 | 
				
			||||||
 | 
					NOTEBOOKS_PER_PAGE=50
 | 
				
			||||||
 | 
					ALLOWED_EXTENSIONS=.ipynb,.py,.md
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Feature Toggles
 | 
				
			||||||
 | 
					ENABLE_DOWNLOAD=True
 | 
				
			||||||
 | 
					ENABLE_API=True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# UI Settings
 | 
				
			||||||
 | 
					THEME=dark
 | 
				
			||||||
							
								
								
									
										24
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					# JupyterHub Notebook Viewer Configuration
 | 
				
			||||||
 | 
					# Copy this file to .env and modify as needed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Core Settings
 | 
				
			||||||
 | 
					JUPYTERHUB_SHARED_DIR=/shared
 | 
				
			||||||
 | 
					APP_TITLE=JupyterHub Notebook Viewer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Flask Settings
 | 
				
			||||||
 | 
					FLASK_HOST=0.0.0.0
 | 
				
			||||||
 | 
					FLASK_PORT=5000
 | 
				
			||||||
 | 
					FLASK_DEBUG=True
 | 
				
			||||||
 | 
					FLASK_SECRET_KEY=your-secret-key-change-in-production
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# File Handling
 | 
				
			||||||
 | 
					MAX_FILE_SIZE=16777216  # 16MB in bytes
 | 
				
			||||||
 | 
					NOTEBOOKS_PER_PAGE=50
 | 
				
			||||||
 | 
					ALLOWED_EXTENSIONS=.ipynb,.py,.md
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Feature Toggles
 | 
				
			||||||
 | 
					ENABLE_DOWNLOAD=True
 | 
				
			||||||
 | 
					ENABLE_API=True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# UI Settings
 | 
				
			||||||
 | 
					THEME=dark  # dark or light
 | 
				
			||||||
							
								
								
									
										0
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										163
									
								
								Justfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								Justfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
				
			|||||||
 | 
					# JupyterHub Notebook Viewer - Development Commands
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Default recipe
 | 
				
			||||||
 | 
					default:
 | 
				
			||||||
 | 
					    @just --list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Set up development environment
 | 
				
			||||||
 | 
					setup:
 | 
				
			||||||
 | 
					    @echo "Setting up development environment..."
 | 
				
			||||||
 | 
					    mkdir -p shared notebooks templates
 | 
				
			||||||
 | 
					    [ ! -f .env ] && cp .env.example .env || true
 | 
				
			||||||
 | 
					    @echo "✓ Development environment ready"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start development server
 | 
				
			||||||
 | 
					dev:
 | 
				
			||||||
 | 
					    @echo "Starting development server..."
 | 
				
			||||||
 | 
					    python app.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start development server with auto-reload (using nix shell)
 | 
				
			||||||
 | 
					dev-nix:
 | 
				
			||||||
 | 
					    nix develop --command dev-server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Run tests
 | 
				
			||||||
 | 
					test:
 | 
				
			||||||
 | 
					    @echo "Running tests..."
 | 
				
			||||||
 | 
					    python -m pytest tests/ -v || python -c "import app; print('Basic import test passed')"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Run tests in nix environment
 | 
				
			||||||
 | 
					test-nix:
 | 
				
			||||||
 | 
					    nix develop --command run-tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Build Docker image
 | 
				
			||||||
 | 
					docker-build:
 | 
				
			||||||
 | 
					    docker build -t jupyterhub-notebook-viewer .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start with Docker Compose
 | 
				
			||||||
 | 
					docker-up:
 | 
				
			||||||
 | 
					    docker-compose up
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start with Docker Compose in background
 | 
				
			||||||
 | 
					docker-up-daemon:
 | 
				
			||||||
 | 
					    docker-compose up -d
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start with Docker Compose and rebuild
 | 
				
			||||||
 | 
					docker-rebuild:
 | 
				
			||||||
 | 
					    docker-compose up --build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start with nginx proxy
 | 
				
			||||||
 | 
					docker-proxy:
 | 
				
			||||||
 | 
					    docker-compose --profile with-proxy up
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Stop Docker services
 | 
				
			||||||
 | 
					docker-down:
 | 
				
			||||||
 | 
					    docker-compose down
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# View Docker logs
 | 
				
			||||||
 | 
					docker-logs:
 | 
				
			||||||
 | 
					    docker-compose logs -f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Clean Docker resources
 | 
				
			||||||
 | 
					docker-clean:
 | 
				
			||||||
 | 
					    docker-compose down -v
 | 
				
			||||||
 | 
					    docker system prune -f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install Python dependencies
 | 
				
			||||||
 | 
					install:
 | 
				
			||||||
 | 
					    pip install -r requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Format code
 | 
				
			||||||
 | 
					format:
 | 
				
			||||||
 | 
					    black app.py
 | 
				
			||||||
 | 
					    isort app.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Lint code
 | 
				
			||||||
 | 
					lint:
 | 
				
			||||||
 | 
					    flake8 app.py
 | 
				
			||||||
 | 
					    pylint app.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Generate requirements.txt
 | 
				
			||||||
 | 
					freeze:
 | 
				
			||||||
 | 
					    pip freeze > requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create sample notebook
 | 
				
			||||||
 | 
					sample-notebook:
 | 
				
			||||||
 | 
					    @mkdir -p shared
 | 
				
			||||||
 | 
					    @cat > shared/sample.ipynb << 'EOF'
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					     "cells": [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					       "cell_type": "markdown",
 | 
				
			||||||
 | 
					       "metadata": {},
 | 
				
			||||||
 | 
					       "source": [
 | 
				
			||||||
 | 
					        "# Sample Notebook\n\nThis is a sample Jupyter notebook."
 | 
				
			||||||
 | 
					       ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					       "cell_type": "code",
 | 
				
			||||||
 | 
					       "execution_count": null,
 | 
				
			||||||
 | 
					       "metadata": {},
 | 
				
			||||||
 | 
					       "outputs": [],
 | 
				
			||||||
 | 
					       "source": [
 | 
				
			||||||
 | 
					        "print('Hello World!')\nimport numpy as np\nprint(f'NumPy version: {np.__version__}')"
 | 
				
			||||||
 | 
					       ]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					     ],
 | 
				
			||||||
 | 
					     "metadata": {
 | 
				
			||||||
 | 
					      "kernelspec": {
 | 
				
			||||||
 | 
					       "display_name": "Python 3",
 | 
				
			||||||
 | 
					       "language": "python",
 | 
				
			||||||
 | 
					       "name": "python3"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					     },
 | 
				
			||||||
 | 
					     "nbformat": 4,
 | 
				
			||||||
 | 
					     "nbformat_minor": 4
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    EOF
 | 
				
			||||||
 | 
					    @echo "✓ Sample notebook created in shared/sample.ipynb"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Check environment configuration
 | 
				
			||||||
 | 
					check-env:
 | 
				
			||||||
 | 
					    @echo "Environment Configuration:"
 | 
				
			||||||
 | 
					    @echo "========================="
 | 
				
			||||||
 | 
					    @grep -E '^[A-Z_]' .env 2>/dev/null || echo "No .env file found"
 | 
				
			||||||
 | 
					    @echo ""
 | 
				
			||||||
 | 
					    @echo "Current Python: $(which python)"
 | 
				
			||||||
 | 
					    @echo "Flask available: $(python -c 'import flask; print(flask.__version__)' 2>/dev/null || echo 'Not installed')"
 | 
				
			||||||
 | 
					    @echo "Shared directory: ${JUPYTERHUB_SHARED_DIR:-./shared}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Show application logs
 | 
				
			||||||
 | 
					logs:
 | 
				
			||||||
 | 
					    tail -f app.log 2>/dev/null || echo "No log file found"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Clean temporary files
 | 
				
			||||||
 | 
					clean:
 | 
				
			||||||
 | 
					    rm -rf __pycache__/
 | 
				
			||||||
 | 
					    rm -rf .pytest_cache/
 | 
				
			||||||
 | 
					    rm -rf *.pyc
 | 
				
			||||||
 | 
					    rm -rf .coverage
 | 
				
			||||||
 | 
					    rm -rf htmlcov/
 | 
				
			||||||
 | 
					    rm -rf dist/
 | 
				
			||||||
 | 
					    rm -rf build/
 | 
				
			||||||
 | 
					    rm -rf *.egg-info/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Security scan
 | 
				
			||||||
 | 
					security:
 | 
				
			||||||
 | 
					    safety check -r requirements.txt || echo "Safety not installed, skipping security scan"
 | 
				
			||||||
 | 
					    bandit -r . -f json || echo "Bandit not installed, skipping security scan"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Performance test
 | 
				
			||||||
 | 
					perf:
 | 
				
			||||||
 | 
					    @echo "Running performance test..."
 | 
				
			||||||
 | 
					    ab -n 100 -c 10 http://localhost:5000/ || echo "Apache Bench not available"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Health check
 | 
				
			||||||
 | 
					health:
 | 
				
			||||||
 | 
					    curl -f http://localhost:5000/ || echo "Application not responding"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Generate documentation
 | 
				
			||||||
 | 
					docs:
 | 
				
			||||||
 | 
					    @echo "Generating documentation..."
 | 
				
			||||||
 | 
					    mkdir -p docs
 | 
				
			||||||
 | 
					    @echo "# JupyterHub Notebook Viewer\n\nGenerated documentation\n" > docs/README.md
 | 
				
			||||||
 | 
					    @echo "✓ Documentation generated in docs/"
 | 
				
			||||||
							
								
								
									
										236
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										236
									
								
								README.md
									
									
									
									
									
								
							@@ -0,0 +1,236 @@
 | 
				
			|||||||
 | 
					JupyterHub Notebook Viewer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A Flask-based web application for viewing and browsing Jupyter notebooks from a JupyterHub shared directory. Features a responsive web interface with directory navigation, notebook preview, and download capabilities.
 | 
				
			||||||
 | 
					Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    📁 Directory Navigation - Browse through nested directories with breadcrumb navigation
 | 
				
			||||||
 | 
					    📄 Notebook Viewing - Convert and display Jupyter notebooks as HTML in the browser
 | 
				
			||||||
 | 
					    ⬇️ Download Support - Direct download of notebook files
 | 
				
			||||||
 | 
					    🎨 Responsive Design - Bootstrap-based UI that works on desktop and mobile
 | 
				
			||||||
 | 
					    🔒 Security - Path traversal protection and configurable access controls
 | 
				
			||||||
 | 
					    🛠️ Configurable - All settings configurable via environment variables
 | 
				
			||||||
 | 
					    🐳 Docker Support - Ready-to-use Docker containers and compose files
 | 
				
			||||||
 | 
					    ❄️ Nix Development - Complete Nix flake for reproducible development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Quick Start
 | 
				
			||||||
 | 
					Using Nix (Recommended)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Enter development environment
 | 
				
			||||||
 | 
					nix develop
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Set up the environment (first time only)
 | 
				
			||||||
 | 
					setup-dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start development server
 | 
				
			||||||
 | 
					dev-server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Using Docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy and configure environment
 | 
				
			||||||
 | 
					cp .env.example .env
 | 
				
			||||||
 | 
					# Edit .env with your settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start with Docker Compose
 | 
				
			||||||
 | 
					docker-compose up
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Manual Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install dependencies
 | 
				
			||||||
 | 
					pip install -r requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Configure environment
 | 
				
			||||||
 | 
					cp .env.example .env
 | 
				
			||||||
 | 
					# Edit .env with your settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start the application
 | 
				
			||||||
 | 
					python app.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All configuration is done via environment variables, typically in a .env file:
 | 
				
			||||||
 | 
					Core Settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    JUPYTERHUB_SHARED_DIR - Path to the shared notebook directory (default: /shared)
 | 
				
			||||||
 | 
					    APP_TITLE - Application title shown in the interface
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Flask Settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FLASK_HOST - Host to bind to (default: 0.0.0.0)
 | 
				
			||||||
 | 
					    FLASK_PORT - Port to run on (default: 5000)
 | 
				
			||||||
 | 
					    FLASK_DEBUG - Enable debug mode (default: True)
 | 
				
			||||||
 | 
					    FLASK_SECRET_KEY - Secret key for sessions (change in production!)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					File Handling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    MAX_FILE_SIZE - Maximum file size in bytes (default: 16777216 = 16MB)
 | 
				
			||||||
 | 
					    NOTEBOOKS_PER_PAGE - Maximum notebooks to show per directory (default: 50)
 | 
				
			||||||
 | 
					    ALLOWED_EXTENSIONS - Comma-separated list of allowed file extensions (default: .ipynb,.py,.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Feature Toggles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ENABLE_DOWNLOAD - Enable file downloads (default: True)
 | 
				
			||||||
 | 
					    ENABLE_API - Enable JSON API endpoints (default: True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					UI Settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    THEME - UI theme, dark or light (default: dark)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Development
 | 
				
			||||||
 | 
					With Nix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The project includes a complete Nix flake for reproducible development:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Enter development shell
 | 
				
			||||||
 | 
					nix develop
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Available commands in the shell:
 | 
				
			||||||
 | 
					setup-dev     # Set up development environment
 | 
				
			||||||
 | 
					dev-server    # Start development server with auto-reload
 | 
				
			||||||
 | 
					run-tests     # Run basic tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					With Just
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you have just installed:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# See all available commands
 | 
				
			||||||
 | 
					just
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Common commands
 | 
				
			||||||
 | 
					just setup           # Set up development environment
 | 
				
			||||||
 | 
					just dev             # Start development server
 | 
				
			||||||
 | 
					just docker-up       # Start with Docker
 | 
				
			||||||
 | 
					just test            # Run tests
 | 
				
			||||||
 | 
					just clean           # Clean temporary files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Manual Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install dependencies
 | 
				
			||||||
 | 
					pip install -r requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Set up environment
 | 
				
			||||||
 | 
					cp .env.example .env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create sample content
 | 
				
			||||||
 | 
					mkdir -p shared
 | 
				
			||||||
 | 
					# Add some .ipynb files to shared/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start development server
 | 
				
			||||||
 | 
					python app.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Docker Deployment
 | 
				
			||||||
 | 
					Basic Deployment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Build and start
 | 
				
			||||||
 | 
					docker-compose up --build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Run in background
 | 
				
			||||||
 | 
					docker-compose up -d
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# View logs
 | 
				
			||||||
 | 
					docker-compose logs -f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					With Reverse Proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The compose file includes an optional nginx reverse proxy:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start with proxy
 | 
				
			||||||
 | 
					docker-compose --profile with-proxy up
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Production Considerations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For production deployment:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Change the secret key: Set FLASK_SECRET_KEY to a secure random value
 | 
				
			||||||
 | 
					    Disable debug mode: Set FLASK_DEBUG=False
 | 
				
			||||||
 | 
					    Configure volumes: Mount your actual shared directory
 | 
				
			||||||
 | 
					    Set up SSL: Configure HTTPS in your reverse proxy
 | 
				
			||||||
 | 
					    Resource limits: Set appropriate CPU and memory limits in Docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					API Endpoints
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When ENABLE_API=True, the following JSON API endpoints are available:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    GET /api/notebooks?dir=<path> - List notebooks and directories
 | 
				
			||||||
 | 
					    GET / - Main web interface
 | 
				
			||||||
 | 
					    GET /view/<path> - View notebook as HTML
 | 
				
			||||||
 | 
					    GET /download/<path> - Download notebook file (if enabled)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Security Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Path Traversal Protection - Prevents access outside the configured directory
 | 
				
			||||||
 | 
					    File Type Validation - Only allows configured file extensions
 | 
				
			||||||
 | 
					    Configurable Downloads - Downloads can be disabled entirely
 | 
				
			||||||
 | 
					    Non-root Docker User - Container runs as non-privileged user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Troubleshooting
 | 
				
			||||||
 | 
					Common Issues
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    "No notebooks found"
 | 
				
			||||||
 | 
					        Check that JUPYTERHUB_SHARED_DIR points to the correct directory
 | 
				
			||||||
 | 
					        Ensure the directory contains .ipynb files
 | 
				
			||||||
 | 
					        Check file permissions
 | 
				
			||||||
 | 
					    Notebook won't display
 | 
				
			||||||
 | 
					        Verify the notebook file is valid JSON
 | 
				
			||||||
 | 
					        Check that nbconvert dependencies are installed
 | 
				
			||||||
 | 
					        Look for errors in the application logs
 | 
				
			||||||
 | 
					    Permission denied
 | 
				
			||||||
 | 
					        Ensure the application has read access to the shared directory
 | 
				
			||||||
 | 
					        Check Docker volume mounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Logs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Application logs are printed to stdout. In Docker:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docker-compose logs -f notebook-viewer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Contributing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Fork the repository
 | 
				
			||||||
 | 
					    Enter the development environment: nix develop
 | 
				
			||||||
 | 
					    Make your changes
 | 
				
			||||||
 | 
					    Run tests: run-tests
 | 
				
			||||||
 | 
					    Submit a pull request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MIT License - see LICENSE file for details.
 | 
				
			||||||
 | 
					Architecture
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					┌─────────────────────────────────────────────────────────────┐
 | 
				
			||||||
 | 
					│                     Web Browser                             │
 | 
				
			||||||
 | 
					└─────────────────────┬───────────────────────────────────────┘
 | 
				
			||||||
 | 
					                      │ HTTP
 | 
				
			||||||
 | 
					┌─────────────────────▼───────────────────────────────────────┐
 | 
				
			||||||
 | 
					│                  Flask App                                  │
 | 
				
			||||||
 | 
					│  ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐   │
 | 
				
			||||||
 | 
					│  │   Routes    │ │  Templates   │ │    Static Assets    │   │
 | 
				
			||||||
 | 
					│  └─────────────┘ └──────────────┘ └─────────────────────┘   │
 | 
				
			||||||
 | 
					└─────────────────────┬───────────────────────────────────────┘
 | 
				
			||||||
 | 
					                      │ File System
 | 
				
			||||||
 | 
					┌─────────────────────▼───────────────────────────────────────┐
 | 
				
			||||||
 | 
					│              JupyterHub Shared Directory                    │
 | 
				
			||||||
 | 
					│  ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐   │
 | 
				
			||||||
 | 
					│  │ Notebooks   │ │ Directories  │ │   Other Files       │   │
 | 
				
			||||||
 | 
					│  │ (.ipynb)    │ │              │ │   (.py, .md)        │   │
 | 
				
			||||||
 | 
					│  └─────────────┘ └──────────────┘ └─────────────────────┘   │
 | 
				
			||||||
 | 
					└─────────────────────────────────────────────────────────────┘
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										440
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										440
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,440 @@
 | 
				
			|||||||
 | 
					import os
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from flask import Flask, render_template, request, jsonify, send_file, abort
 | 
				
			||||||
 | 
					import nbformat
 | 
				
			||||||
 | 
					from nbconvert import HTMLExporter
 | 
				
			||||||
 | 
					import mimetypes
 | 
				
			||||||
 | 
					from dotenv import load_dotenv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Load environment variables from .env file
 | 
				
			||||||
 | 
					load_dotenv()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app = Flask(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Configuration from environment variables
 | 
				
			||||||
 | 
					app.config.update({
 | 
				
			||||||
 | 
					    'SHARED_DIRECTORY': os.environ.get('JUPYTERHUB_SHARED_DIR', '/shared'),
 | 
				
			||||||
 | 
					    'HOST': os.environ.get('FLASK_HOST', '0.0.0.0'),
 | 
				
			||||||
 | 
					    'PORT': int(os.environ.get('FLASK_PORT', 5000)),
 | 
				
			||||||
 | 
					    'DEBUG': os.environ.get('FLASK_DEBUG', 'True').lower() == 'true',
 | 
				
			||||||
 | 
					    'SECRET_KEY': os.environ.get('FLASK_SECRET_KEY', 'dev-key-change-in-production'),
 | 
				
			||||||
 | 
					    'MAX_CONTENT_LENGTH': int(os.environ.get('MAX_FILE_SIZE', 16 * 1024 * 1024)),  # 16MB default
 | 
				
			||||||
 | 
					    'NOTEBOOKS_PER_PAGE': int(os.environ.get('NOTEBOOKS_PER_PAGE', 50)),
 | 
				
			||||||
 | 
					    'ALLOWED_EXTENSIONS': os.environ.get('ALLOWED_EXTENSIONS', '.ipynb').split(','),
 | 
				
			||||||
 | 
					    'ENABLE_DOWNLOAD': os.environ.get('ENABLE_DOWNLOAD', 'True').lower() == 'true',
 | 
				
			||||||
 | 
					    'ENABLE_API': os.environ.get('ENABLE_API', 'True').lower() == 'true',
 | 
				
			||||||
 | 
					    'APP_TITLE': os.environ.get('APP_TITLE', 'JupyterHub Notebook Viewer'),
 | 
				
			||||||
 | 
					    'THEME': os.environ.get('THEME', 'dark'),  # dark or light
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_notebook_files(directory):
 | 
				
			||||||
 | 
					    """Recursively get all notebook files from directory"""
 | 
				
			||||||
 | 
					    notebooks = []
 | 
				
			||||||
 | 
					    allowed_extensions = app.config['ALLOWED_EXTENSIONS']
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        for root, dirs, files in os.walk(directory):
 | 
				
			||||||
 | 
					            for file in files:
 | 
				
			||||||
 | 
					                if any(file.endswith(ext) for ext in allowed_extensions):
 | 
				
			||||||
 | 
					                    full_path = os.path.join(root, file)
 | 
				
			||||||
 | 
					                    relative_path = os.path.relpath(full_path, directory)
 | 
				
			||||||
 | 
					                    notebooks.append({
 | 
				
			||||||
 | 
					                        'name': file,
 | 
				
			||||||
 | 
					                        'path': relative_path,
 | 
				
			||||||
 | 
					                        'full_path': full_path,
 | 
				
			||||||
 | 
					                        'size': os.path.getsize(full_path),
 | 
				
			||||||
 | 
					                        'modified': os.path.getmtime(full_path)
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					    except (OSError, PermissionError) as e:
 | 
				
			||||||
 | 
					        app.logger.error(f"Error accessing directory {directory}: {e}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Limit results based on configuration
 | 
				
			||||||
 | 
					    notebooks = sorted(notebooks, key=lambda x: x['modified'], reverse=True)
 | 
				
			||||||
 | 
					    return notebooks[:app.config['NOTEBOOKS_PER_PAGE']]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_directory_structure(directory):
 | 
				
			||||||
 | 
					    """Get directory structure for navigation"""
 | 
				
			||||||
 | 
					    structure = []
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        for item in os.listdir(directory):
 | 
				
			||||||
 | 
					            item_path = os.path.join(directory, item)
 | 
				
			||||||
 | 
					            if os.path.isdir(item_path):
 | 
				
			||||||
 | 
					                structure.append({
 | 
				
			||||||
 | 
					                    'name': item,
 | 
				
			||||||
 | 
					                    'type': 'directory',
 | 
				
			||||||
 | 
					                    'path': os.path.relpath(item_path, app.config['SHARED_DIRECTORY'])
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					    except (OSError, PermissionError):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return sorted(structure, key=lambda x: x['name'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def convert_notebook_to_html(notebook_path):
 | 
				
			||||||
 | 
					    """Convert Jupyter notebook to HTML"""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        with open(notebook_path, 'r', encoding='utf-8') as f:
 | 
				
			||||||
 | 
					            notebook = nbformat.read(f, as_version=4)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        html_exporter = HTMLExporter()
 | 
				
			||||||
 | 
					        html_exporter.template_name = 'classic'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        (body, resources) = html_exporter.from_notebook_node(notebook)
 | 
				
			||||||
 | 
					        return body
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        return f"<div class='alert alert-danger'>Error converting notebook: {str(e)}</div>"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/')
 | 
				
			||||||
 | 
					def index():
 | 
				
			||||||
 | 
					    """Main page showing all notebooks"""
 | 
				
			||||||
 | 
					    current_dir = request.args.get('dir', '')
 | 
				
			||||||
 | 
					    full_current_dir = os.path.join(app.config['SHARED_DIRECTORY'], current_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if not os.path.exists(full_current_dir):
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    notebooks = get_notebook_files(full_current_dir)
 | 
				
			||||||
 | 
					    directories = get_directory_structure(full_current_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Breadcrumb navigation
 | 
				
			||||||
 | 
					    breadcrumbs = []
 | 
				
			||||||
 | 
					    if current_dir:
 | 
				
			||||||
 | 
					        parts = current_dir.split(os.sep)
 | 
				
			||||||
 | 
					        for i, part in enumerate(parts):
 | 
				
			||||||
 | 
					            breadcrumbs.append({
 | 
				
			||||||
 | 
					                'name': part,
 | 
				
			||||||
 | 
					                'path': os.sep.join(parts[:i+1])
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return render_template('index.html', 
 | 
				
			||||||
 | 
					                         notebooks=notebooks, 
 | 
				
			||||||
 | 
					                         directories=directories,
 | 
				
			||||||
 | 
					                         current_dir=current_dir,
 | 
				
			||||||
 | 
					                         breadcrumbs=breadcrumbs,
 | 
				
			||||||
 | 
					                         config=app.config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/view/<path:notebook_path>')
 | 
				
			||||||
 | 
					def view_notebook(notebook_path):
 | 
				
			||||||
 | 
					    """View a specific notebook"""
 | 
				
			||||||
 | 
					    full_path = os.path.join(app.config['SHARED_DIRECTORY'], notebook_path)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if not os.path.exists(full_path):
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Check if file has allowed extension
 | 
				
			||||||
 | 
					    if not any(full_path.endswith(ext) for ext in app.config['ALLOWED_EXTENSIONS']):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Security check - ensure path is within shared directory
 | 
				
			||||||
 | 
					    if not os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    html_content = convert_notebook_to_html(full_path)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return render_template('notebook.html', 
 | 
				
			||||||
 | 
					                         html_content=html_content,
 | 
				
			||||||
 | 
					                         notebook_name=os.path.basename(notebook_path),
 | 
				
			||||||
 | 
					                         notebook_path=notebook_path,
 | 
				
			||||||
 | 
					                         config=app.config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/download/<path:notebook_path>')
 | 
				
			||||||
 | 
					def download_notebook(notebook_path):
 | 
				
			||||||
 | 
					    """Download a notebook file"""
 | 
				
			||||||
 | 
					    if not app.config['ENABLE_DOWNLOAD']:
 | 
				
			||||||
 | 
					        abort(403, "Downloads are disabled")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    full_path = os.path.join(app.config['SHARED_DIRECTORY'], notebook_path)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if not os.path.exists(full_path):
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Check if file has allowed extension
 | 
				
			||||||
 | 
					    if not any(full_path.endswith(ext) for ext in app.config['ALLOWED_EXTENSIONS']):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Security check
 | 
				
			||||||
 | 
					    if not os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return send_file(full_path, as_attachment=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/api/notebooks')
 | 
				
			||||||
 | 
					def api_notebooks():
 | 
				
			||||||
 | 
					    """API endpoint to get notebooks as JSON"""
 | 
				
			||||||
 | 
					    if not app.config['ENABLE_API']:
 | 
				
			||||||
 | 
					        abort(403, "API is disabled")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    current_dir = request.args.get('dir', '')
 | 
				
			||||||
 | 
					    full_current_dir = os.path.join(app.config['SHARED_DIRECTORY'], current_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    notebooks = get_notebook_files(full_current_dir)
 | 
				
			||||||
 | 
					    directories = get_directory_structure(full_current_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return jsonify({
 | 
				
			||||||
 | 
					        'notebooks': notebooks,
 | 
				
			||||||
 | 
					        'directories': directories,
 | 
				
			||||||
 | 
					        'current_dir': current_dir,
 | 
				
			||||||
 | 
					        'config': {
 | 
				
			||||||
 | 
					            'app_title': app.config['APP_TITLE'],
 | 
				
			||||||
 | 
					            'enable_download': app.config['ENABLE_DOWNLOAD'],
 | 
				
			||||||
 | 
					            'notebooks_per_page': app.config['NOTEBOOKS_PER_PAGE']
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.errorhandler(404)
 | 
				
			||||||
 | 
					def not_found(error):
 | 
				
			||||||
 | 
					    return render_template('error.html', 
 | 
				
			||||||
 | 
					                         error_code=404, 
 | 
				
			||||||
 | 
					                         error_message="Notebook or directory not found"), 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.errorhandler(403)
 | 
				
			||||||
 | 
					def forbidden(error):
 | 
				
			||||||
 | 
					    return render_template('error.html', 
 | 
				
			||||||
 | 
					                         error_code=403, 
 | 
				
			||||||
 | 
					                         error_message="Access forbidden"), 403
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.errorhandler(500)
 | 
				
			||||||
 | 
					def server_error(error):
 | 
				
			||||||
 | 
					    return render_template('error.html', 
 | 
				
			||||||
 | 
					                         error_code=500, 
 | 
				
			||||||
 | 
					                         error_message="Internal server error"), 500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    # Create templates directory if it doesn't exist
 | 
				
			||||||
 | 
					    os.makedirs('templates', exist_ok=True)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Determine theme classes
 | 
				
			||||||
 | 
					    navbar_class = "navbar-dark bg-dark" if app.config['THEME'] == 'dark' else "navbar-light bg-light"
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Basic templates
 | 
				
			||||||
 | 
					    index_template = f'''<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					    <title>{{{{ config.APP_TITLE }}}}</title>
 | 
				
			||||||
 | 
					    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					    <nav class="navbar {navbar_class}">
 | 
				
			||||||
 | 
					        <div class="container">
 | 
				
			||||||
 | 
					            <a class="navbar-brand" href="{{{{ url_for('index') }}}}">
 | 
				
			||||||
 | 
					                <i class="fas fa-book"></i> {{{{ config.APP_TITLE }}}}
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="container mt-4">
 | 
				
			||||||
 | 
					        <!-- Breadcrumb Navigation -->
 | 
				
			||||||
 | 
					        {{% if breadcrumbs %}}
 | 
				
			||||||
 | 
					        <nav aria-label="breadcrumb">
 | 
				
			||||||
 | 
					            <ol class="breadcrumb">
 | 
				
			||||||
 | 
					                <li class="breadcrumb-item"><a href="{{{{ url_for('index') }}}}">Home</a></li>
 | 
				
			||||||
 | 
					                {{% for crumb in breadcrumbs %}}
 | 
				
			||||||
 | 
					                <li class="breadcrumb-item">
 | 
				
			||||||
 | 
					                    <a href="{{{{ url_for('index', dir=crumb.path) }}}}">{{{{ crumb.name }}}}</a>
 | 
				
			||||||
 | 
					                </li>
 | 
				
			||||||
 | 
					                {{% endfor %}}
 | 
				
			||||||
 | 
					            </ol>
 | 
				
			||||||
 | 
					        </nav>
 | 
				
			||||||
 | 
					        {{% endif %}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <h1 class="mb-4">
 | 
				
			||||||
 | 
					            {{% if current_dir %}}
 | 
				
			||||||
 | 
					                Notebooks in {{{{ current_dir }}}}
 | 
				
			||||||
 | 
					            {{% else %}}
 | 
				
			||||||
 | 
					                All Notebooks
 | 
				
			||||||
 | 
					            {{% endif %}}
 | 
				
			||||||
 | 
					        </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- Directories -->
 | 
				
			||||||
 | 
					        {{% if directories %}}
 | 
				
			||||||
 | 
					        <div class="row mb-4">
 | 
				
			||||||
 | 
					            <div class="col-12">
 | 
				
			||||||
 | 
					                <h3><i class="fas fa-folder"></i> Directories</h3>
 | 
				
			||||||
 | 
					                {{% for dir in directories %}}
 | 
				
			||||||
 | 
					                <div class="card mb-2">
 | 
				
			||||||
 | 
					                    <div class="card-body">
 | 
				
			||||||
 | 
					                        <h5 class="card-title">
 | 
				
			||||||
 | 
					                            <a href="{{{{ url_for('index', dir=dir.path) }}}}" class="text-decoration-none">
 | 
				
			||||||
 | 
					                                <i class="fas fa-folder text-warning"></i> {{{{ dir.name }}}}
 | 
				
			||||||
 | 
					                            </a>
 | 
				
			||||||
 | 
					                        </h5>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                {{% endfor %}}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        {{% endif %}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- Notebooks -->
 | 
				
			||||||
 | 
					        <div class="row">
 | 
				
			||||||
 | 
					            <div class="col-12">
 | 
				
			||||||
 | 
					                <h3><i class="fas fa-file-code"></i> Notebooks</h3>
 | 
				
			||||||
 | 
					                {{% if notebooks %}}
 | 
				
			||||||
 | 
					                    {{% for notebook in notebooks %}}
 | 
				
			||||||
 | 
					                    <div class="card mb-3">
 | 
				
			||||||
 | 
					                        <div class="card-body">
 | 
				
			||||||
 | 
					                            <div class="row align-items-center">
 | 
				
			||||||
 | 
					                                <div class="col-md-8">
 | 
				
			||||||
 | 
					                                    <h5 class="card-title">
 | 
				
			||||||
 | 
					                                        <i class="fas fa-file-code text-primary"></i> {{{{ notebook.name }}}}
 | 
				
			||||||
 | 
					                                    </h5>
 | 
				
			||||||
 | 
					                                    <p class="card-text text-muted">
 | 
				
			||||||
 | 
					                                        <small>
 | 
				
			||||||
 | 
					                                            Size: {{{{ "%.1f"|format(notebook.size/1024) }}}} KB | 
 | 
				
			||||||
 | 
					                                            Modified: {{{{ notebook.modified|int|datetime }}}}
 | 
				
			||||||
 | 
					                                        </small>
 | 
				
			||||||
 | 
					                                    </p>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                                <div class="col-md-4 text-end">
 | 
				
			||||||
 | 
					                                    <a href="{{{{ url_for('view_notebook', notebook_path=notebook.path) }}}}" 
 | 
				
			||||||
 | 
					                                       class="btn btn-primary btn-sm me-2">
 | 
				
			||||||
 | 
					                                        <i class="fas fa-eye"></i> View
 | 
				
			||||||
 | 
					                                    </a>
 | 
				
			||||||
 | 
					                                    {{% if config.ENABLE_DOWNLOAD %}}
 | 
				
			||||||
 | 
					                                    <a href="{{{{ url_for('download_notebook', notebook_path=notebook.path) }}}}" 
 | 
				
			||||||
 | 
					                                       class="btn btn-secondary btn-sm">
 | 
				
			||||||
 | 
					                                        <i class="fas fa-download"></i> Download
 | 
				
			||||||
 | 
					                                    </a>
 | 
				
			||||||
 | 
					                                    {{% endif %}}
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    {{% endfor %}}
 | 
				
			||||||
 | 
					                    {{% if notebooks|length == config.NOTEBOOKS_PER_PAGE %}}
 | 
				
			||||||
 | 
					                    <div class="alert alert-info">
 | 
				
			||||||
 | 
					                        <i class="fas fa-info-circle"></i> Showing first {{{{ config.NOTEBOOKS_PER_PAGE }}}} notebooks. Configure NOTEBOOKS_PER_PAGE to show more.
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    {{% endif %}}
 | 
				
			||||||
 | 
					                {{% else %}}
 | 
				
			||||||
 | 
					                    <div class="alert alert-info">
 | 
				
			||||||
 | 
					                        <i class="fas fa-info-circle"></i> No notebooks found in this directory.
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                {{% endif %}}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>'''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    notebook_template = f'''<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					    <title>{{{{ notebook_name }}}} - {{{{ config.APP_TITLE }}}}</title>
 | 
				
			||||||
 | 
					    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					        .notebook-content {{
 | 
				
			||||||
 | 
					            max-width: 100%;
 | 
				
			||||||
 | 
					            overflow-x: auto;
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        .notebook-content img {{
 | 
				
			||||||
 | 
					            max-width: 100%;
 | 
				
			||||||
 | 
					            height: auto;
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					    <nav class="navbar {navbar_class}">
 | 
				
			||||||
 | 
					        <div class="container">
 | 
				
			||||||
 | 
					            <a class="navbar-brand" href="{{{{ url_for('index') }}}}">
 | 
				
			||||||
 | 
					                <i class="fas fa-book"></i> {{{{ config.APP_TITLE }}}}
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                {{% if config.ENABLE_DOWNLOAD %}}
 | 
				
			||||||
 | 
					                <a href="{{{{ url_for('download_notebook', notebook_path=notebook_path) }}}}" 
 | 
				
			||||||
 | 
					                   class="btn btn-outline-light btn-sm">
 | 
				
			||||||
 | 
					                    <i class="fas fa-download"></i> Download
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					                {{% endif %}}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="container-fluid mt-4">
 | 
				
			||||||
 | 
					        <div class="row">
 | 
				
			||||||
 | 
					            <div class="col-12">
 | 
				
			||||||
 | 
					                <h2 class="mb-3">
 | 
				
			||||||
 | 
					                    <i class="fas fa-file-code text-primary"></i> {{{{ notebook_name }}}}
 | 
				
			||||||
 | 
					                </h2>
 | 
				
			||||||
 | 
					                <div class="notebook-content">
 | 
				
			||||||
 | 
					                    {{{{ html_content|safe }}}}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>'''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    error_template = f'''<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					    <title>Error {{{{ error_code }}}} - {{{{ config.APP_TITLE if config else 'JupyterHub Notebook Viewer' }}}}</title>
 | 
				
			||||||
 | 
					    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					    <nav class="navbar {navbar_class}">
 | 
				
			||||||
 | 
					        <div class="container">
 | 
				
			||||||
 | 
					            <a class="navbar-brand" href="{{{{ url_for('index') }}}}">
 | 
				
			||||||
 | 
					                <i class="fas fa-book"></i> {{{{ config.APP_TITLE if config else 'JupyterHub Notebook Viewer' }}}}
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="container mt-5">
 | 
				
			||||||
 | 
					        <div class="row justify-content-center">
 | 
				
			||||||
 | 
					            <div class="col-md-6 text-center">
 | 
				
			||||||
 | 
					                <div class="alert alert-danger">
 | 
				
			||||||
 | 
					                    <h1><i class="fas fa-exclamation-triangle"></i></h1>
 | 
				
			||||||
 | 
					                    <h2>Error {{{{ error_code }}}}</h2>
 | 
				
			||||||
 | 
					                    <p>{{{{ error_message }}}}</p>
 | 
				
			||||||
 | 
					                    <a href="{{{{ url_for('index') }}}}" class="btn btn-primary">
 | 
				
			||||||
 | 
					                        <i class="fas fa-home"></i> Go Home
 | 
				
			||||||
 | 
					                    </a>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>'''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Write templates
 | 
				
			||||||
 | 
					    with open('templates/index.html', 'w') as f:
 | 
				
			||||||
 | 
					        f.write(index_template)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    with open('templates/notebook.html', 'w') as f:
 | 
				
			||||||
 | 
					        f.write(notebook_template)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    with open('templates/error.html', 'w') as f:
 | 
				
			||||||
 | 
					        f.write(error_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Add datetime filter
 | 
				
			||||||
 | 
					    @app.template_filter('datetime')
 | 
				
			||||||
 | 
					    def datetime_filter(timestamp):
 | 
				
			||||||
 | 
					        from datetime import datetime
 | 
				
			||||||
 | 
					        return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    print(f"Starting {app.config['APP_TITLE']}")
 | 
				
			||||||
 | 
					    print(f"Shared directory: {app.config['SHARED_DIRECTORY']}")
 | 
				
			||||||
 | 
					    print(f"Server running on http://{app.config['HOST']}:{app.config['PORT']}")
 | 
				
			||||||
 | 
					    print(f"Debug mode: {app.config['DEBUG']}")
 | 
				
			||||||
 | 
					    print(f"Downloads enabled: {app.config['ENABLE_DOWNLOAD']}")
 | 
				
			||||||
 | 
					    print(f"API enabled: {app.config['ENABLE_API']}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    app.run(
 | 
				
			||||||
 | 
					        debug=app.config['DEBUG'], 
 | 
				
			||||||
 | 
					        host=app.config['HOST'], 
 | 
				
			||||||
 | 
					        port=app.config['PORT']
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
							
								
								
									
										36
									
								
								compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								compose.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					version: '3.8'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					services:
 | 
				
			||||||
 | 
					  notebook-viewer:
 | 
				
			||||||
 | 
					    build: .
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - "${FLASK_PORT:-5000}:5000"
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - "${JUPYTERHUB_SHARED_DIR:-./shared}:/shared:ro"
 | 
				
			||||||
 | 
					      - "./notebooks:/notebooks:ro"  # Additional notebooks directory
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - JUPYTERHUB_SHARED_DIR=/shared
 | 
				
			||||||
 | 
					      - FLASK_HOST=0.0.0.0
 | 
				
			||||||
 | 
					      - FLASK_PORT=5000
 | 
				
			||||||
 | 
					      - FLASK_DEBUG=${FLASK_DEBUG:-False}
 | 
				
			||||||
 | 
					      - FLASK_SECRET_KEY=${FLASK_SECRET_KEY:-change-me-in-production}
 | 
				
			||||||
 | 
					      - MAX_FILE_SIZE=${MAX_FILE_SIZE:-16777216}
 | 
				
			||||||
 | 
					      - NOTEBOOKS_PER_PAGE=${NOTEBOOKS_PER_PAGE:-50}
 | 
				
			||||||
 | 
					      - ALLOWED_EXTENSIONS=${ALLOWED_EXTENSIONS:-.ipynb,.py,.md}
 | 
				
			||||||
 | 
					      - ENABLE_DOWNLOAD=${ENABLE_DOWNLOAD:-True}
 | 
				
			||||||
 | 
					      - ENABLE_API=${ENABLE_API:-True}
 | 
				
			||||||
 | 
					      - APP_TITLE=${APP_TITLE:-JupyterHub Notebook Viewer}
 | 
				
			||||||
 | 
					      - THEME=${THEME:-dark}
 | 
				
			||||||
 | 
					    env_file:
 | 
				
			||||||
 | 
					      - .env
 | 
				
			||||||
 | 
					    restart: unless-stopped
 | 
				
			||||||
 | 
					    healthcheck:
 | 
				
			||||||
 | 
					      test: ["CMD", "curl", "-f", "http://localhost:5000/"]
 | 
				
			||||||
 | 
					      interval: 30s
 | 
				
			||||||
 | 
					      timeout: 10s
 | 
				
			||||||
 | 
					      retries: 3
 | 
				
			||||||
 | 
					      start_period: 40s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					volumes:
 | 
				
			||||||
 | 
					  shared:
 | 
				
			||||||
 | 
					    driver: local
 | 
				
			||||||
							
								
								
									
										313
									
								
								flake.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								flake.nix
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,313 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  description = "JupyterHub Notebook Viewer Development Environment";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  inputs = {
 | 
				
			||||||
 | 
					    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
 | 
				
			||||||
 | 
					    flake-utils.url = "github:numtide/flake-utils";
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  outputs = { self, nixpkgs, flake-utils }:
 | 
				
			||||||
 | 
					    flake-utils.lib.eachDefaultSystem (system:
 | 
				
			||||||
 | 
					      let
 | 
				
			||||||
 | 
					        pkgs = nixpkgs.legacyPackages.${system};
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Python with required packages
 | 
				
			||||||
 | 
					        python = pkgs.python311.withPackages (ps: with ps; [
 | 
				
			||||||
 | 
					          flask
 | 
				
			||||||
 | 
					          python-dotenv
 | 
				
			||||||
 | 
					          nbformat
 | 
				
			||||||
 | 
					          nbconvert
 | 
				
			||||||
 | 
					          werkzeug
 | 
				
			||||||
 | 
					          jinja2
 | 
				
			||||||
 | 
					          markupsafe
 | 
				
			||||||
 | 
					          itsdangerous
 | 
				
			||||||
 | 
					          click
 | 
				
			||||||
 | 
					          blinker
 | 
				
			||||||
 | 
					          jupyter-core
 | 
				
			||||||
 | 
					          traitlets
 | 
				
			||||||
 | 
					          jupyter-client
 | 
				
			||||||
 | 
					          ipython
 | 
				
			||||||
 | 
					          bleach
 | 
				
			||||||
 | 
					          defusedxml
 | 
				
			||||||
 | 
					          mistune
 | 
				
			||||||
 | 
					          pandocfilters
 | 
				
			||||||
 | 
					          beautifulsoup4
 | 
				
			||||||
 | 
					          webencodings
 | 
				
			||||||
 | 
					          fastjsonschema
 | 
				
			||||||
 | 
					          jsonschema
 | 
				
			||||||
 | 
					          pyrsistent
 | 
				
			||||||
 | 
					          pyzmq
 | 
				
			||||||
 | 
					          tornado
 | 
				
			||||||
 | 
					          nest-asyncio
 | 
				
			||||||
 | 
					          psutil
 | 
				
			||||||
 | 
					          packaging
 | 
				
			||||||
 | 
					          platformdirs
 | 
				
			||||||
 | 
					          debugpy
 | 
				
			||||||
 | 
					          matplotlib-inline
 | 
				
			||||||
 | 
					          pygments
 | 
				
			||||||
 | 
					          cffi
 | 
				
			||||||
 | 
					          argon2-cffi
 | 
				
			||||||
 | 
					          jupyterlab-pygments
 | 
				
			||||||
 | 
					          nbclient
 | 
				
			||||||
 | 
					          tinycss2
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Development tools
 | 
				
			||||||
 | 
					        devTools = with pkgs; [
 | 
				
			||||||
 | 
					          # Core development
 | 
				
			||||||
 | 
					          python
 | 
				
			||||||
 | 
					          nodejs_20
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # System tools
 | 
				
			||||||
 | 
					          curl
 | 
				
			||||||
 | 
					          jq
 | 
				
			||||||
 | 
					          tree
 | 
				
			||||||
 | 
					          htop
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Container tools
 | 
				
			||||||
 | 
					          docker
 | 
				
			||||||
 | 
					          docker-compose
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Documentation tools
 | 
				
			||||||
 | 
					          pandoc
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # LaTeX for nbconvert
 | 
				
			||||||
 | 
					          texlive.combined.scheme-medium
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Version control
 | 
				
			||||||
 | 
					          git
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Text editors
 | 
				
			||||||
 | 
					          vim
 | 
				
			||||||
 | 
					          nano
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Network tools
 | 
				
			||||||
 | 
					          netcat
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Process management
 | 
				
			||||||
 | 
					          supervisor
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Shell script for easy setup
 | 
				
			||||||
 | 
					        setupScript = pkgs.writeShellScriptBin "setup-dev" ''
 | 
				
			||||||
 | 
					          echo "Setting up JupyterHub Notebook Viewer development environment..."
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Create directories
 | 
				
			||||||
 | 
					          mkdir -p shared notebooks templates
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Copy example .env if it doesn't exist
 | 
				
			||||||
 | 
					          if [ ! -f .env ]; then
 | 
				
			||||||
 | 
					            cp .env.example .env 2>/dev/null || echo "No .env.example found, using defaults"
 | 
				
			||||||
 | 
					          fi
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Set up git hooks if in git repo
 | 
				
			||||||
 | 
					          if [ -d .git ]; then
 | 
				
			||||||
 | 
					            echo "Setting up git hooks..."
 | 
				
			||||||
 | 
					            cp .githooks/* .git/hooks/ 2>/dev/null || true
 | 
				
			||||||
 | 
					          fi
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Create sample notebook if shared directory is empty
 | 
				
			||||||
 | 
					          if [ ! "$(ls -A shared)" ]; then
 | 
				
			||||||
 | 
					            echo "Creating sample notebook..."
 | 
				
			||||||
 | 
					            cat > shared/sample.ipynb << 'EOF'
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					           "cells": [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					             "cell_type": "markdown",
 | 
				
			||||||
 | 
					             "metadata": {},
 | 
				
			||||||
 | 
					             "source": [
 | 
				
			||||||
 | 
					              "# Sample Notebook\n",
 | 
				
			||||||
 | 
					              "\n",
 | 
				
			||||||
 | 
					              "This is a sample Jupyter notebook for testing the viewer."
 | 
				
			||||||
 | 
					             ]
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					             "cell_type": "code",
 | 
				
			||||||
 | 
					             "execution_count": null,
 | 
				
			||||||
 | 
					             "metadata": {},
 | 
				
			||||||
 | 
					             "outputs": [],
 | 
				
			||||||
 | 
					             "source": [
 | 
				
			||||||
 | 
					              "print('Hello from JupyterHub Notebook Viewer!')\n",
 | 
				
			||||||
 | 
					              "import numpy as np\n",
 | 
				
			||||||
 | 
					              "import matplotlib.pyplot as plt\n",
 | 
				
			||||||
 | 
					              "\n",
 | 
				
			||||||
 | 
					              "x = np.linspace(0, 10, 100)\n",
 | 
				
			||||||
 | 
					              "y = np.sin(x)\n",
 | 
				
			||||||
 | 
					              "\n",
 | 
				
			||||||
 | 
					              "plt.figure(figsize=(10, 6))\n",
 | 
				
			||||||
 | 
					              "plt.plot(x, y)\n",
 | 
				
			||||||
 | 
					              "plt.title('Sample Plot')\n",
 | 
				
			||||||
 | 
					              "plt.xlabel('X')\n",
 | 
				
			||||||
 | 
					              "plt.ylabel('sin(x)')\n",
 | 
				
			||||||
 | 
					              "plt.grid(True)\n",
 | 
				
			||||||
 | 
					              "plt.show()"
 | 
				
			||||||
 | 
					             ]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					           ],
 | 
				
			||||||
 | 
					           "metadata": {
 | 
				
			||||||
 | 
					            "kernelspec": {
 | 
				
			||||||
 | 
					             "display_name": "Python 3",
 | 
				
			||||||
 | 
					             "language": "python",
 | 
				
			||||||
 | 
					             "name": "python3"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "language_info": {
 | 
				
			||||||
 | 
					             "name": "python",
 | 
				
			||||||
 | 
					             "version": "3.11.0"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					           },
 | 
				
			||||||
 | 
					           "nbformat": 4,
 | 
				
			||||||
 | 
					           "nbformat_minor": 4
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          EOF
 | 
				
			||||||
 | 
					          fi
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          echo "Development environment setup complete!"
 | 
				
			||||||
 | 
					          echo "Run 'python app.py' to start the development server"
 | 
				
			||||||
 | 
					        '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Script for running tests
 | 
				
			||||||
 | 
					        testScript = pkgs.writeShellScriptBin "run-tests" ''
 | 
				
			||||||
 | 
					          echo "Running tests for JupyterHub Notebook Viewer..."
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Basic import test
 | 
				
			||||||
 | 
					          python -c "
 | 
				
			||||||
 | 
					          import app
 | 
				
			||||||
 | 
					          print('✓ App imports successfully')
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Test configuration loading
 | 
				
			||||||
 | 
					          from dotenv import load_dotenv
 | 
				
			||||||
 | 
					          load_dotenv()
 | 
				
			||||||
 | 
					          print('✓ Environment variables loaded')
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Test nbconvert
 | 
				
			||||||
 | 
					          import nbformat
 | 
				
			||||||
 | 
					          import nbconvert
 | 
				
			||||||
 | 
					          print('✓ Jupyter dependencies working')
 | 
				
			||||||
 | 
					          "
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Test Flask app creation
 | 
				
			||||||
 | 
					          python -c "
 | 
				
			||||||
 | 
					          import app
 | 
				
			||||||
 | 
					          test_app = app.app.test_client()
 | 
				
			||||||
 | 
					          response = test_app.get('/')
 | 
				
			||||||
 | 
					          if response.status_code in [200, 404]:  # 404 is OK if no shared dir
 | 
				
			||||||
 | 
					            print('✓ Flask app responds correctly')
 | 
				
			||||||
 | 
					          else:
 | 
				
			||||||
 | 
					            print('✗ Flask app error:', response.status_code)
 | 
				
			||||||
 | 
					            exit(1)
 | 
				
			||||||
 | 
					          "
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          echo "All tests passed!"
 | 
				
			||||||
 | 
					        '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Development server with auto-reload
 | 
				
			||||||
 | 
					        devServerScript = pkgs.writeShellScriptBin "dev-server" ''
 | 
				
			||||||
 | 
					          echo "Starting development server with auto-reload..."
 | 
				
			||||||
 | 
					          export FLASK_DEBUG=True
 | 
				
			||||||
 | 
					          export FLASK_ENV=development
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Load .env file
 | 
				
			||||||
 | 
					          if [ -f .env ]; then
 | 
				
			||||||
 | 
					            export $(cat .env | grep -v '^#' | xargs)
 | 
				
			||||||
 | 
					          fi
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Start the server
 | 
				
			||||||
 | 
					          python app.py
 | 
				
			||||||
 | 
					        '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      in
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        # Development shell
 | 
				
			||||||
 | 
					        devShells.default = pkgs.mkShell {
 | 
				
			||||||
 | 
					          buildInputs = devTools ++ [
 | 
				
			||||||
 | 
					            setupScript
 | 
				
			||||||
 | 
					            testScript
 | 
				
			||||||
 | 
					            devServerScript
 | 
				
			||||||
 | 
					          ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          shellHook = ''
 | 
				
			||||||
 | 
					            echo "🚀 JupyterHub Notebook Viewer Development Environment"
 | 
				
			||||||
 | 
					            echo "=================================================="
 | 
				
			||||||
 | 
					            echo ""
 | 
				
			||||||
 | 
					            echo "Available commands:"
 | 
				
			||||||
 | 
					            echo "  setup-dev     - Set up development environment"
 | 
				
			||||||
 | 
					            echo "  dev-server    - Start development server with auto-reload"
 | 
				
			||||||
 | 
					            echo "  run-tests     - Run basic tests"
 | 
				
			||||||
 | 
					            echo "  python app.py - Start the application"
 | 
				
			||||||
 | 
					            echo ""
 | 
				
			||||||
 | 
					            echo "Docker commands:"
 | 
				
			||||||
 | 
					            echo "  docker-compose up              - Start with Docker"
 | 
				
			||||||
 | 
					            echo "  docker-compose up --build      - Rebuild and start"
 | 
				
			||||||
 | 
					            echo "  docker-compose up -d           - Start in background"
 | 
				
			||||||
 | 
					            echo "  docker-compose --profile with-proxy up - Start with nginx proxy"
 | 
				
			||||||
 | 
					            echo ""
 | 
				
			||||||
 | 
					            echo "Configuration:"
 | 
				
			||||||
 | 
					            echo "  Edit .env file to configure the application"
 | 
				
			||||||
 | 
					            echo "  JUPYTERHUB_SHARED_DIR: $(echo ''${JUPYTERHUB_SHARED_DIR:-/shared})"
 | 
				
			||||||
 | 
					            echo ""
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Auto-setup on first run
 | 
				
			||||||
 | 
					            if [ ! -f .env ] && [ ! -d shared ]; then
 | 
				
			||||||
 | 
					              echo "First run detected, running setup..."
 | 
				
			||||||
 | 
					              setup-dev
 | 
				
			||||||
 | 
					            fi
 | 
				
			||||||
 | 
					          '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          # Environment variables for development
 | 
				
			||||||
 | 
					          FLASK_DEBUG = "True";
 | 
				
			||||||
 | 
					          FLASK_ENV = "development";
 | 
				
			||||||
 | 
					          PYTHONPATH = ".";
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Package for the application
 | 
				
			||||||
 | 
					        packages.default = pkgs.stdenv.mkDerivation {
 | 
				
			||||||
 | 
					          pname = "jupyterhub-notebook-viewer";
 | 
				
			||||||
 | 
					          version = "1.0.0";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          src = ./.;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          buildInputs = [ python ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          installPhase = ''
 | 
				
			||||||
 | 
					            mkdir -p $out/bin $out/share/jupyterhub-notebook-viewer
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Copy application files
 | 
				
			||||||
 | 
					            cp app.py $out/share/jupyterhub-notebook-viewer/
 | 
				
			||||||
 | 
					            cp -r templates $out/share/jupyterhub-notebook-viewer/
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Create wrapper script
 | 
				
			||||||
 | 
					            cat > $out/bin/jupyterhub-notebook-viewer << EOF
 | 
				
			||||||
 | 
					            #!${pkgs.bash}/bin/bash
 | 
				
			||||||
 | 
					            cd $out/share/jupyterhub-notebook-viewer
 | 
				
			||||||
 | 
					            exec ${python}/bin/python app.py "\$@"
 | 
				
			||||||
 | 
					            EOF
 | 
				
			||||||
 | 
					            chmod +x $out/bin/jupyterhub-notebook-viewer
 | 
				
			||||||
 | 
					          '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          meta = with pkgs.lib; {
 | 
				
			||||||
 | 
					            description = "Web viewer for JupyterHub shared notebooks";
 | 
				
			||||||
 | 
					            license = licenses.mit;
 | 
				
			||||||
 | 
					            platforms = platforms.unix;
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Docker image build
 | 
				
			||||||
 | 
					        packages.docker = pkgs.dockerTools.buildImage {
 | 
				
			||||||
 | 
					          name = "jupyterhub-notebook-viewer";
 | 
				
			||||||
 | 
					          tag = "latest";
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          contents = [ python pkgs.pandoc ];
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          config = {
 | 
				
			||||||
 | 
					            Cmd = [ "${python}/bin/python" "/app/app.py" ];
 | 
				
			||||||
 | 
					            WorkingDir = "/app";
 | 
				
			||||||
 | 
					            ExposedPorts = {
 | 
				
			||||||
 | 
					              "5000/tcp" = {};
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Formatter for nix files
 | 
				
			||||||
 | 
					        formatter = pkgs.nixpkgs-fmt;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										0
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
		Reference in New Issue
	
	Block a user