Init
This commit is contained in:
parent
9146896837
commit
8c6ed3d642
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
Loading…
Reference in New Issue
Block a user