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