├── nbviewer ├── tests │ ├── __init__.py │ ├── templates │ │ └── index.html │ ├── notebook.ipynb │ ├── test_index.py │ ├── test_config.py │ ├── test_json.py │ ├── test_format_slides.py │ ├── test_security.py │ ├── test_app.py │ ├── base.py │ └── test_utils.py ├── providers │ ├── gist │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_gist.py │ │ └── __init__.py │ ├── github │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_handlers.py │ │ │ ├── test_client.py │ │ │ └── test_github.py │ │ ├── __init__.py │ │ └── client.py │ ├── local │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_localfile.py │ │ └── __init__.py │ ├── url │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_content.py │ │ │ └── test_url.py │ │ ├── __init__.py │ │ └── handlers.py │ ├── dropbox │ │ ├── __init__.py │ │ └── handlers.py │ ├── huggingface │ │ ├── __init__.py │ │ └── handlers.py │ └── __init__.py ├── static │ ├── .bowerrc │ ├── less │ │ ├── custom.less │ │ ├── dropdown-ref.less │ │ ├── variables.less │ │ ├── styles.less │ │ ├── media.less │ │ ├── notebook.less │ │ ├── home.less │ │ ├── layout.less │ │ ├── bootstrap.less │ │ └── slides.less │ ├── favicon.ico │ ├── img │ │ ├── bird.png │ │ ├── icon-css3.png │ │ ├── github-16px.png │ │ ├── icon-binder.png │ │ ├── icon-github.png │ │ ├── icon-html5.png │ │ ├── less-small.png │ │ ├── th_github.jpeg │ │ ├── icon-twitter.png │ │ ├── example-nb │ │ │ ├── bokeh.png │ │ │ ├── gaza.png │ │ │ ├── sympy.png │ │ │ ├── fitting.png │ │ │ ├── iruby-nb.png │ │ │ ├── plotly.png │ │ │ ├── r_magic.png │ │ │ ├── covariance.png │ │ │ ├── lightning.png │ │ │ ├── data-cleanup.png │ │ │ ├── ipython-thumb.png │ │ │ ├── mining-slice.png │ │ │ ├── nose_testing.png │ │ │ ├── numpy_tests.png │ │ │ ├── python-signal.png │ │ │ ├── XKCD-Matplotlib.png │ │ │ ├── bayesian-chap1.png │ │ │ ├── ijulia-preview.png │ │ │ ├── jaynes-cummings.png │ │ │ ├── ip-examples-list.png │ │ │ ├── pandas_timeseries.png │ │ │ ├── python_for_visres.png │ │ │ ├── exploring_r_formula.png │ │ │ ├── working_with_pandas.png │ │ │ ├── pde_solver_with_numpy.png │ │ │ ├── probabilistic-bayesian.png │ │ │ └── readme.md │ │ ├── grid-18px-masked.png │ │ ├── less-logo-large.png │ │ ├── Python-logo-notext.png │ │ ├── icon-binder-color.png │ │ ├── glyphicons-halflings.png │ │ ├── responsive-illustrations.png │ │ ├── glyphicons-halflings-white.png │ │ ├── glyphicons │ │ │ ├── glyphicons_009_magic.png │ │ │ ├── glyphicons_042_group.png │ │ │ ├── glyphicons_079_podium.png │ │ │ ├── glyphicons_163_iphone.png │ │ │ ├── glyphicons_082_roundabout.png │ │ │ ├── glyphicons_266_book_open.png │ │ │ ├── glyphicons_214_resize_small.png │ │ │ └── glyphicons_155_show_thumbnails.png │ │ └── nav_logo.svg │ ├── ico │ │ ├── ipynb_icon_16x16.ico │ │ ├── ipynb_icon_16x16.png │ │ ├── apple-touch-icon-57-precomposed.png │ │ ├── apple-touch-icon-72-precomposed.png │ │ ├── apple-touch-icon-114-precomposed.png │ │ └── apple-touch-icon-144-precomposed.png │ ├── robots.txt │ ├── bower.json │ └── css │ │ └── theme │ │ ├── cdp_1.css │ │ └── css_linalg.css ├── templates │ ├── formats │ │ ├── script.html │ │ ├── html.html │ │ └── slides.html │ ├── 404.html │ ├── userview.html │ ├── 500.html │ ├── 502.html │ ├── 400.html │ ├── slow_notebook.html │ ├── unknown_filetype.html │ ├── popular.html │ ├── error.html │ ├── usergists.html │ ├── dirview.html │ ├── tabular.html │ ├── index.html │ ├── treelist.html │ ├── notebook.html │ └── layout.html ├── __main__.py ├── __init__.py ├── frontpage.schema.json ├── index.py ├── log.py ├── render.py ├── ratelimit.py ├── formats.py ├── frontpage.json ├── client.py ├── handlers.py └── cache.py ├── .gitattributes ├── statuspage ├── README.md ├── Dockerfile └── statuspage.py ├── docker-compose.yml ├── hooks └── post_push ├── requirements-dev.txt ├── requirements.in ├── helm-chart ├── nbviewer │ ├── Chart.lock │ ├── Chart.yaml │ ├── templates │ │ ├── pdb.yaml │ │ ├── secret.yaml │ │ ├── service.yaml │ │ ├── _helpers.tpl │ │ ├── statuspage.yaml │ │ └── deployment.yaml │ └── values.yaml └── minikube.yaml ├── .dockerignore ├── .gitignore ├── .flake8 ├── .pre-commit-config.yaml ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── test.yaml │ └── image.yaml ├── package.json ├── pyproject.toml ├── Dockerfile ├── .travis.yml ├── LICENSE.txt ├── setup.py ├── CONTRIBUTING.md ├── tasks.py └── requirements.txt /nbviewer/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nbviewer/providers/gist/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nbviewer/providers/github/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nbviewer/providers/local/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nbviewer/providers/url/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | nbviewer/_version.py export-subst 2 | -------------------------------------------------------------------------------- /nbviewer/tests/templates/index.html: -------------------------------------------------------------------------------- 1 | IT WORKED 2 | -------------------------------------------------------------------------------- /nbviewer/static/.bowerrc: -------------------------------------------------------------------------------- 1 | {"directory": "components"} 2 | -------------------------------------------------------------------------------- /nbviewer/templates/formats/script.html: -------------------------------------------------------------------------------- 1 | {{ body | safe }} 2 | -------------------------------------------------------------------------------- /nbviewer/__main__.py: -------------------------------------------------------------------------------- 1 | from nbviewer.app import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /nbviewer/templates/formats/html.html: -------------------------------------------------------------------------------- 1 | {% extends "notebook.html" %} 2 | -------------------------------------------------------------------------------- /statuspage/README.md: -------------------------------------------------------------------------------- 1 | publish github rate limit remaining to statuspage.io 2 | -------------------------------------------------------------------------------- /nbviewer/static/less/custom.less: -------------------------------------------------------------------------------- 1 | // Please use this to custom your stylesheet 2 | -------------------------------------------------------------------------------- /nbviewer/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/favicon.ico -------------------------------------------------------------------------------- /nbviewer/static/img/bird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/bird.png -------------------------------------------------------------------------------- /nbviewer/providers/dropbox/__init__.py: -------------------------------------------------------------------------------- 1 | from .handlers import uri_rewrites 2 | 3 | __all__ = ["uri_rewrites"] 4 | -------------------------------------------------------------------------------- /nbviewer/providers/huggingface/__init__.py: -------------------------------------------------------------------------------- 1 | from .handlers import uri_rewrites 2 | 3 | __all__ = ["uri_rewrites"] 4 | -------------------------------------------------------------------------------- /nbviewer/static/img/icon-css3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/icon-css3.png -------------------------------------------------------------------------------- /nbviewer/static/img/github-16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/github-16px.png -------------------------------------------------------------------------------- /nbviewer/static/img/icon-binder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/icon-binder.png -------------------------------------------------------------------------------- /nbviewer/static/img/icon-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/icon-github.png -------------------------------------------------------------------------------- /nbviewer/static/img/icon-html5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/icon-html5.png -------------------------------------------------------------------------------- /nbviewer/static/img/less-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/less-small.png -------------------------------------------------------------------------------- /nbviewer/static/img/th_github.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/th_github.jpeg -------------------------------------------------------------------------------- /nbviewer/static/img/icon-twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/icon-twitter.png -------------------------------------------------------------------------------- /nbviewer/static/ico/ipynb_icon_16x16.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/ico/ipynb_icon_16x16.ico -------------------------------------------------------------------------------- /nbviewer/static/ico/ipynb_icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/ico/ipynb_icon_16x16.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/bokeh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/bokeh.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/gaza.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/gaza.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/sympy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/sympy.png -------------------------------------------------------------------------------- /nbviewer/static/img/grid-18px-masked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/grid-18px-masked.png -------------------------------------------------------------------------------- /nbviewer/static/img/less-logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/less-logo-large.png -------------------------------------------------------------------------------- /nbviewer/static/img/Python-logo-notext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/Python-logo-notext.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/fitting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/fitting.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/iruby-nb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/iruby-nb.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/plotly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/plotly.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/r_magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/r_magic.png -------------------------------------------------------------------------------- /nbviewer/static/img/icon-binder-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/icon-binder-color.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/covariance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/covariance.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/lightning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/lightning.png -------------------------------------------------------------------------------- /nbviewer/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | nbviewer: 2 | build: . 3 | links: 4 | - nbcache 5 | ports: 6 | - 8080:8080 7 | nbcache: 8 | image: memcached 9 | -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/data-cleanup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/data-cleanup.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/ipython-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/ipython-thumb.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/mining-slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/mining-slice.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/nose_testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/nose_testing.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/numpy_tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/numpy_tests.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/python-signal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/python-signal.png -------------------------------------------------------------------------------- /nbviewer/static/img/responsive-illustrations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/responsive-illustrations.png -------------------------------------------------------------------------------- /hooks/post_push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SHA=${SOURCE_COMMIT::8} 3 | docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$SHA 4 | docker push $DOCKER_REPO:$SHA 5 | -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/XKCD-Matplotlib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/XKCD-Matplotlib.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/bayesian-chap1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/bayesian-chap1.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/ijulia-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/ijulia-preview.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/jaynes-cummings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/jaynes-cummings.png -------------------------------------------------------------------------------- /nbviewer/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /nbviewer/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Crawl-Delay: 10 3 | 4 | User-agent: dotbot 5 | Disallow: / 6 | User-agent: AhrefsBot 7 | Disallow: / 8 | -------------------------------------------------------------------------------- /statuspage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | RUN pip install requests 3 | ADD statuspage.py statuspage.py 4 | CMD ["python", "statuspage.py"] 5 | -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/ip-examples-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/ip-examples-list.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/pandas_timeseries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/pandas_timeseries.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/python_for_visres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/python_for_visres.png -------------------------------------------------------------------------------- /nbviewer/static/ico/apple-touch-icon-57-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/ico/apple-touch-icon-57-precomposed.png -------------------------------------------------------------------------------- /nbviewer/static/ico/apple-touch-icon-72-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/ico/apple-touch-icon-72-precomposed.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/exploring_r_formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/exploring_r_formula.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/working_with_pandas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/working_with_pandas.png -------------------------------------------------------------------------------- /nbviewer/static/img/glyphicons/glyphicons_009_magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/glyphicons/glyphicons_009_magic.png -------------------------------------------------------------------------------- /nbviewer/static/img/glyphicons/glyphicons_042_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/glyphicons/glyphicons_042_group.png -------------------------------------------------------------------------------- /nbviewer/static/ico/apple-touch-icon-114-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/ico/apple-touch-icon-114-precomposed.png -------------------------------------------------------------------------------- /nbviewer/static/ico/apple-touch-icon-144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/ico/apple-touch-icon-144-precomposed.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/pde_solver_with_numpy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/pde_solver_with_numpy.png -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/probabilistic-bayesian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/example-nb/probabilistic-bayesian.png -------------------------------------------------------------------------------- /nbviewer/static/img/glyphicons/glyphicons_079_podium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/glyphicons/glyphicons_079_podium.png -------------------------------------------------------------------------------- /nbviewer/static/img/glyphicons/glyphicons_163_iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/glyphicons/glyphicons_163_iphone.png -------------------------------------------------------------------------------- /nbviewer/providers/gist/__init__.py: -------------------------------------------------------------------------------- 1 | from .handlers import default_handlers 2 | from .handlers import uri_rewrites 3 | 4 | __all__ = ["default_handlers", "uri_rewrites"] 5 | -------------------------------------------------------------------------------- /nbviewer/providers/url/__init__.py: -------------------------------------------------------------------------------- 1 | from .handlers import default_handlers 2 | from .handlers import uri_rewrites 3 | 4 | __all__ = ["default_handlers", "uri_rewrites"] 5 | -------------------------------------------------------------------------------- /nbviewer/static/img/glyphicons/glyphicons_082_roundabout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/glyphicons/glyphicons_082_roundabout.png -------------------------------------------------------------------------------- /nbviewer/static/img/glyphicons/glyphicons_266_book_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/glyphicons/glyphicons_266_book_open.png -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | invoke>=0.13.0 3 | mock>=1.3.0 # python34 and older versions of mock do not play well together. 4 | pre-commit 5 | pytest 6 | requests 7 | -------------------------------------------------------------------------------- /nbviewer/providers/github/__init__.py: -------------------------------------------------------------------------------- 1 | from .handlers import default_handlers 2 | from .handlers import uri_rewrites 3 | 4 | __all__ = ["default_handlers", "uri_rewrites"] 5 | -------------------------------------------------------------------------------- /nbviewer/static/img/glyphicons/glyphicons_214_resize_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/glyphicons/glyphicons_214_resize_small.png -------------------------------------------------------------------------------- /nbviewer/providers/local/__init__.py: -------------------------------------------------------------------------------- 1 | from .handlers import default_handlers 2 | from .handlers import LocalFileHandler 3 | 4 | __all__ = ["default_handlers", "LocalFileHandler"] 5 | -------------------------------------------------------------------------------- /nbviewer/static/img/glyphicons/glyphicons_155_show_thumbnails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/nbviewer/HEAD/nbviewer/static/img/glyphicons/glyphicons_155_show_thumbnails.png -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | elasticsearch 2 | ipython>=8 3 | jupyter_client 4 | jupyter_server>=2 5 | markdown>=3.3 6 | nbconvert>=6.5.4 7 | nbformat>=5.0 8 | pycurl 9 | pylibmc 10 | statsd 11 | tornado>=6.0 12 | -------------------------------------------------------------------------------- /nbviewer/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | try: 4 | __version__ = importlib.metadata.version(__name__) 5 | except importlib.metadata.PackageNotFoundError: 6 | __version__ = "0.0.0.dev-unknown" 7 | -------------------------------------------------------------------------------- /helm-chart/nbviewer/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: memcached 3 | repository: oci://registry-1.docker.io/cloudpirates 4 | version: 0.1.2 5 | digest: sha256:69c816eb2858dbd348e8b8b455613e309d7939d89edcbefb9aa99f91c903f429 6 | generated: "2025-09-22T10:14:27.753704-07:00" 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .DS_Store 3 | .eggs 4 | .github 5 | .gitignore 6 | .ipynb_checkpoints/ 7 | .travis.yml 8 | .vscode 9 | *.pyc 10 | build/ 11 | dist/ 12 | nbviewer.egg-info/ 13 | nbviewer/static/build/ 14 | nbviewer/static/components/ 15 | node_modules/ 16 | notebook-*/ 17 | -------------------------------------------------------------------------------- /helm-chart/minikube.yaml: -------------------------------------------------------------------------------- 1 | service: 2 | type: NodePort 3 | ports: 4 | nodePort: 32567 5 | 6 | resources: 7 | requests: 8 | memory: null 9 | cpu: null 10 | 11 | nbviewer: 12 | extraArgs: 13 | - "--logging=debug" 14 | 15 | memcached: 16 | replicaCount: 1 17 | pdbMinAvailable: 0 18 | -------------------------------------------------------------------------------- /nbviewer/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "error.html" %} 2 | {% block error_detail %} 3 |

You are requesting a page that does not exist!

4 | {% if 'HTTP 404' in message %} 5 |

The remote resource was not found.

6 | {% elif message %} 7 |

{{message}}

8 | {% endif %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /nbviewer/templates/userview.html: -------------------------------------------------------------------------------- 1 | {% extends "tabular.html" %} 2 | 3 | 4 | {% block entry scoped %} 5 | 6 | 7 | 8 | 9 | {{entry.name}} 10 | 11 | 12 | 13 | {% endblock entry %} 14 | -------------------------------------------------------------------------------- /nbviewer/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "error.html" %} 2 | {% block error_detail %} 3 |

Something went wrong.

4 |

If this is reproducible, please let us know.

5 | {% if message %} 6 |

The upstream error was {{message}}

7 | {% endif %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /nbviewer/templates/502.html: -------------------------------------------------------------------------------- 1 | {% extends "error.html" %} 2 | 3 | {% block h1_error %} 4 |

502 : Upstream Error

5 | {% endblock %} 6 | 7 | {% block error_detail %} 8 |

Something went wrong, but it's probably not my fault.

9 | {% if message %} 10 |

The upstream error was {{message}}

11 | {% endif %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /nbviewer/static/less/dropdown-ref.less: -------------------------------------------------------------------------------- 1 | .dropdown-ref { 2 | a { cursor: pointer; } 3 | 4 | ul { 5 | max-height: 300px; 6 | overflow-y: auto; 7 | padding: 0 10px; 8 | margin: 0; 9 | list-style: none; 10 | a { white-space: nowrap; } 11 | } 12 | 13 | .nav-header { 14 | padding: 8px; 15 | margin: 0; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .debug 3 | .DS_Store 4 | .pandoc 5 | *.egg-info 6 | *.pyc 7 | *.swp 8 | *.un~ 9 | */static/components 10 | \.ipynb_checkpoints 11 | bin 12 | build 13 | dist 14 | node_modules 15 | screenshots 16 | nbviewer/git_info.json 17 | # ignore downloaded notebook sources 18 | notebook-* 19 | .eggs/ 20 | MANIFEST 21 | package-lock.json 22 | .vscode/ 23 | *.tgz 24 | -------------------------------------------------------------------------------- /nbviewer/static/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "animate.css": "~3.2", 6 | "headroom.js": "0.7.0", 7 | "requirejs": "~2.1", 8 | "moment": "~2.8.4", 9 | "bootstrap": "components/bootstrap#~3.3", 10 | "font-awesome": "~4", 11 | "pygments": "~2.0.0", 12 | "reveal.js": "~3.1.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /helm-chart/nbviewer/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: nbviewer 3 | version: 0.0.1 4 | appVersion: 1.0.1 5 | description: Jupyter Notebook Viewer 6 | home: https://nbviewer.jupyter.org 7 | sources: 8 | - https://github.com/jupyter/nbviewer 9 | kubeVersion: ">=1.28.0-0" 10 | dependencies: 11 | - name: memcached 12 | version: "0.1.2" 13 | repository: oci://registry-1.docker.io/cloudpirates 14 | -------------------------------------------------------------------------------- /nbviewer/tests/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "code", 12 | "collapsed": false, 13 | "input": [], 14 | "language": "python", 15 | "metadata": {}, 16 | "outputs": [] 17 | } 18 | ], 19 | "metadata": {} 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /nbviewer/templates/400.html: -------------------------------------------------------------------------------- 1 | {% extends "error.html" %} 2 | {% block error_detail %} 3 | 4 |

We couldn't render your notebook

5 |

Perhaps it is not valid JSON, or not the right URL.

6 |

If this should be a working notebook, please let us know.

7 | {% if message %} 8 |

The error was: {{message}}

9 | {% endif %} 10 | 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # Ignore style and complexity 3 | # E: style errors 4 | # W: style warnings 5 | # F401: module imported but unused 6 | # F811: redefinition of unused `name` from line `N` 7 | # F841: local variable assigned but never used 8 | ignore = E, C, W, F401, F403, F811, F841, E402, I100, I101, D400 9 | exclude = 10 | helm-chart, 11 | hooks, 12 | setup.py, 13 | statuspage, 14 | versioneer.py 15 | -------------------------------------------------------------------------------- /helm-chart/nbviewer/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.pdb.enabled -}} 2 | apiVersion: policy/v1beta1 3 | kind: PodDisruptionBudget 4 | metadata: 5 | name: {{ template "nbviewer.fullname" . }} 6 | labels: 7 | {{- include "nbviewer.labels" . | nindent 4 }} 8 | spec: 9 | minAvailable: {{ .Values.pdb.minAvailable }} 10 | selector: 11 | matchLabels: 12 | {{- include "nbviewer.matchLabels" . | nindent 6 }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /nbviewer/templates/slow_notebook.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |

Working...

6 |

This notebook is taking a long time to render.

7 |

Please wait, the page will reload in a few seconds.

8 |
9 |
10 | 11 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /nbviewer/static/less/variables.less: -------------------------------------------------------------------------------- 1 | @jupyter-brand: rgb(228, 110, 46); 2 | 3 | // bootstrap 4 | @jumbotron-bg: transparent; 5 | 6 | @thumbnail-border-radius: 1px; 7 | 8 | @border-radius-base: 2px; 9 | 10 | @navbar-default-bg: #fff; 11 | @navbar-default-border: transparent; 12 | @navbar-height: 80px; 13 | 14 | @breadcrumb-bg: #fafafa; 15 | 16 | @blockquote-font-size: inherit; 17 | 18 | @footer-bg: #979797; 19 | 20 | // fontawesome 21 | @fa-font-path: "../components/font-awesome/fonts"; 22 | -------------------------------------------------------------------------------- /nbviewer/tests/test_index.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from .base import NBViewerTestCase 4 | 5 | 6 | class TestNBViewer(NBViewerTestCase): 7 | 8 | def test_get_index(self): 9 | r = requests.get(self.url()) 10 | r.raise_for_status() 11 | 12 | def test_static_files(self): 13 | r = requests.get(self.url("/static/img/nav_logo.svg")) 14 | r.raise_for_status() 15 | r = requests.get(self.url("/static/build/styles.css")) 16 | r.raise_for_status() 17 | -------------------------------------------------------------------------------- /nbviewer/templates/unknown_filetype.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | 4 |
5 |
6 |

666 : Diabolic URL!

7 |

I don't trust everyone...

8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /nbviewer/templates/popular.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | 4 | {% block body %} 5 | 6 | 7 | 8 | 9 | 10 | {% for entry in entries %} 11 | 12 | 15 | 16 | {% endfor %} 17 | 18 |
Most Popular
13 | ({{entry.count}}) from(entry.url) 14 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /helm-chart/nbviewer/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | kind: Secret 2 | apiVersion: v1 3 | metadata: 4 | name: {{ template "nbviewer.fullname" . }} 5 | labels: 6 | {{- include "nbviewer.labels" . | nindent 4 }} 7 | type: Opaque 8 | data: 9 | github-accessToken: {{ .Values.github.accessToken | b64enc | quote }} 10 | github-clientId: {{ .Values.github.clientId | b64enc | quote }} 11 | github-clientSecret: {{ .Values.github.clientSecret | b64enc | quote }} 12 | statuspage-apiKey: {{ .Values.statuspage.apiKey | b64enc | quote }} 13 | -------------------------------------------------------------------------------- /nbviewer/static/less/styles.less: -------------------------------------------------------------------------------- 1 | // @import (less) "../css/docs.css"; 2 | @import "bootstrap"; 3 | @import "../components/font-awesome/less/font-awesome"; 4 | 5 | @import (less) "../components/animate.css/source/_base.css"; 6 | @import (less) "../components/animate.css/source/sliding_entrances/slideInDown.css"; 7 | @import (less) "../components/animate.css/source/sliding_exits/slideOutUp.css"; 8 | 9 | @import "layout"; 10 | @import "home"; 11 | @import "dropdown-ref"; 12 | 13 | @import "media"; 14 | 15 | @import "variables"; 16 | 17 | @import "custom"; 18 | -------------------------------------------------------------------------------- /nbviewer/providers/url/tests/test_content.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests 3 | 4 | from ....tests.base import NBViewerTestCase 5 | 6 | 7 | class ForceUTF8TestCase(NBViewerTestCase): 8 | def test_utf8(self): 9 | """#507, bitbucket returns no content headers, but _is_ serving utf-8""" 10 | response = requests.get( 11 | self.url("/urls/bitbucket.org/sandiego206/asdasd/raw/master/Untitled.ipynb") 12 | ) 13 | self.assertEqual(response.status_code, 200) 14 | self.assertIn("ñ", response.content) 15 | -------------------------------------------------------------------------------- /nbviewer/static/img/example-nb/readme.md: -------------------------------------------------------------------------------- 1 | # Images for frontpage 2 | 3 | This contain images for the frontpage of nbviewer. 4 | 5 | at most thoses images will be show with a size of 360x225 (W x H), aspect ratio 6 | (W/H) of 1.6. As they might be show on retina display, consider using a double 7 | resolution. Resizing will be handled by browser, so to avoid artifact images 8 | shoudl be of size multiple of maximum size ie (720 width) by (450 height), 9 | preferentially in png, and passed to png crusher as the number of request for 10 | thoses images will be heigh. 11 | -------------------------------------------------------------------------------- /nbviewer/static/less/media.less: -------------------------------------------------------------------------------- 1 | // get fixed navbar on mobiles 2 | @media (max-width: 767px) /* @grid-float-breakpoint -1 */ 3 | { 4 | .navbar-fixed-top{ 5 | position: fixed; 6 | top: 0; 7 | margin-left: 0; 8 | margin-right: 0; 9 | } 10 | 11 | .masthead h1{ 12 | font-size: 64px; 13 | } 14 | 15 | .marketing .marketing-byline { 16 | white-space: inherit; 17 | } 18 | } 19 | 20 | 21 | @media (min-width: 768px){ 22 | .menu-text{ 23 | display:none 24 | } 25 | .dropdown-ref .dropdown-menu { 26 | width: 300px; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /nbviewer/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | 4 |
5 |
6 |
7 | {% block h1_error %} 8 |

{{status_code}} : {{status_message}}

9 | {% endblock h1_error %} 10 | {% block error_detail %} 11 | {% if message %} 12 |

{{message}}

13 | {% endif %} 14 | {% endblock %} 15 |
16 |
17 | 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /nbviewer/providers/dropbox/handlers.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | 8 | 9 | def uri_rewrites(rewrites=[]): 10 | return rewrites + [ 11 | ( 12 | r"^http(s?)://www.dropbox.com/(sh?)/(.+?)(\?dl=.)*$", 13 | "/url{0}/dl.dropbox.com/{1}/{2}", 14 | ) 15 | ] 16 | -------------------------------------------------------------------------------- /nbviewer/providers/huggingface/handlers.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | 8 | 9 | def uri_rewrites(rewrites=[]): 10 | return rewrites + [ 11 | ( 12 | r"^https://huggingface.co/(.+?)/blob/(.+?)$", 13 | "/urls/huggingface.co/{0}/resolve/{1}", 14 | ) 15 | ] 16 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black-pre-commit-mirror 3 | rev: "25.9.0" 4 | hooks: 5 | - id: black 6 | exclude: versioneer.py|nbviewer/_version.py 7 | - repo: https://github.com/PyCQA/flake8 8 | rev: "7.3.0" 9 | hooks: 10 | - id: flake8 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v6.0.0 13 | hooks: 14 | - id: end-of-file-fixer 15 | - id: check-json 16 | - id: check-yaml 17 | exclude: ^helm-chart/nbviewer/templates/ 18 | - id: check-case-conflict 19 | - id: check-executables-have-shebangs 20 | ci: 21 | autofix_prs: false 22 | autoupdate_schedule: quarterly 23 | -------------------------------------------------------------------------------- /nbviewer/static/less/notebook.less: -------------------------------------------------------------------------------- 1 | @import (less) "style/ipython.min.css"; 2 | @import (reference) "../components/bootstrap/less/bootstrap"; 3 | 4 | // Fixes regression of #391 when using bower-pygments 5 | .highlight { 6 | @import (less) "../components/pygments/css/default.css"; 7 | } 8 | 9 | // from inline 10 | .imgwrap { 11 | text-align: center; 12 | } 13 | 14 | nav span.fa { 15 | font-size: 20px; 16 | } 17 | 18 | .navbar-brand {height:inherit;} 19 | 20 | @media (max-width: 767px) { 21 | div.input, div.output_area { 22 | box-orient: vertical; 23 | } 24 | 25 | div.prompt { 26 | text-align:left; 27 | } 28 | 29 | .cell img { 30 | .img-responsive(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | # needed to update requirements.txt 11 | allow: 12 | - dependency-type: all 13 | schedule: 14 | interval: "monthly" 15 | open-pull-requests-limit: 3 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "monthly" 20 | open-pull-requests-limit: 3 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nbviewer-deps", 3 | "version": "0.1.0", 4 | "description": "Jupyter nbviewer build dependencies", 5 | "readme": "README.md", 6 | "scripts": { 7 | "watch-less": "./node_modules/.bin/watch 'invoke less' ./nbviewer/static/less" 8 | }, 9 | "devDependencies": { 10 | "autoprefixer": "^10.4.21", 11 | "bower": "~1.8.8", 12 | "less": "~4.4.1", 13 | "less-plugin-clean-css": "~1.6.0", 14 | "postcss": "^8.5.6", 15 | "postcss-cli": "^11.0.1", 16 | "watch": "~1.0.2" 17 | }, 18 | "overrides": { 19 | "exec-sh": "~0.3.4" 20 | }, 21 | "author": "Jupyter Developers", 22 | "license": "BSD-3-Clause", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/jupyter/nbviewer.git" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /nbviewer/templates/usergists.html: -------------------------------------------------------------------------------- 1 | {% extends "tabular.html" %} 2 | 3 | 4 | {% block header_row %} 5 | 6 | Name 7 | Notebooks 8 | Description 9 | 10 | {% endblock header_row %} 11 | 12 | 13 | {% block entry scoped %} 14 | 15 | 16 | 17 | {{entry.id}} 18 | 19 | 20 | 21 | {% for notebook in entry.notebooks %} 22 | 23 | 24 | {{notebook}} 25 | 26 | {% endfor %} 27 | 28 | 29 | 30 | {{entry.description}} 31 | 32 | 33 | 34 | {% endblock entry %} 35 | -------------------------------------------------------------------------------- /nbviewer/tests/test_config.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from .base import NBViewerTestCase 4 | 5 | tmpl_fixture = "nbviewer/tests/templates" 6 | 7 | 8 | class CustomTemplateStub(object): 9 | def test_used_custom_template(self): 10 | r = requests.get(self.url("/")) 11 | self.assertEqual(r.status_code, 200) 12 | self.assertIn("IT WORKED", r.content) 13 | self.assertNotIn("html", r.content) 14 | 15 | 16 | class TemplatePathCLITestCase(NBViewerTestCase, CustomTemplateStub): 17 | @classmethod 18 | def get_server_cmd(cls): 19 | return super().get_server_cmd() + ["--template-path={}".format(tmpl_fixture)] 20 | 21 | 22 | class TemplatePathEnvTestCase(NBViewerTestCase, CustomTemplateStub): 23 | 24 | environment_variables = {"NBVIEWER_TEMPLATE_PATH": tmpl_fixture} 25 | -------------------------------------------------------------------------------- /helm-chart/nbviewer/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "nbviewer.fullname" . }} 5 | labels: 6 | {{- include "nbviewer.labels" . | nindent 4 }} 7 | annotations: 8 | {{- if .Values.service.annotations }} 9 | {{- .Values.service.annotations | toYaml | nindent 4 }} 10 | {{- end }} 11 | spec: 12 | type: {{ .Values.service.type }} 13 | {{- if .Values.service.loadBalancerIP }} 14 | loadBalancerIP: {{ .Values.service.loadBalancerIP }} 15 | {{- end }} 16 | selector: 17 | {{- include "nbviewer.matchLabels" . | nindent 4 }} 18 | ports: 19 | - protocol: TCP 20 | port: 80 21 | targetPort: 5000 22 | {{- if .Values.service.ports.nodePort }} 23 | nodePort: {{ .Values.service.ports.nodePort }} 24 | {{- end }} 25 | -------------------------------------------------------------------------------- /nbviewer/static/less/home.less: -------------------------------------------------------------------------------- 1 | .masthead { 2 | margin: 0; 3 | padding: 0 0 40px; 4 | text-align: center; 5 | 6 | border-bottom: 1px solid #e5e5e5; 7 | 8 | h2 { 9 | margin-bottom: 25px; 10 | font-size: 30px; 11 | line-height: 36px; 12 | } 13 | h1 { 14 | margin-bottom: 9px; 15 | font-size: 81px; 16 | font-weight: bold; 17 | letter-spacing: -1px; 18 | line-height: 1; 19 | } 20 | } 21 | 22 | .marketing { 23 | .marketing-byline { 24 | font-size: 18px; 25 | font-weight: 300; 26 | line-height: 24px; 27 | color: #999; 28 | text-align: center; 29 | white-space: nowrap; 30 | } 31 | } 32 | 33 | .thumbnails { 34 | list-style: none; 35 | padding: 0; 36 | & > li { margin-bottom: 20px; } 37 | } 38 | 39 | h3.section-heading { 40 | text-align: center; 41 | } 42 | -------------------------------------------------------------------------------- /nbviewer/frontpage.schema.json: -------------------------------------------------------------------------------- 1 | {"type": "object", 2 | "properties": { 3 | "title": {"type": "string"}, 4 | "subtitle": {"type": "string"}, 5 | "text": {"type": "string"}, 6 | "show_input": {"type": "boolean"}, 7 | "sections" : { 8 | "type": "array", 9 | "items": { 10 | "type": "object", 11 | "properties": { 12 | "header": {"type": "string"}, 13 | "links": { 14 | "type": "array", 15 | "items": { 16 | "type": "object", 17 | "properties": { 18 | "text": {"type": "string"}, 19 | "target": {"type": "string"}, 20 | "img": {"type": "string"} 21 | }, 22 | "required": ["text", "target", "img"] 23 | } 24 | } 25 | }, 26 | "required": [ 27 | "header", 28 | "links" 29 | ] 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /nbviewer/static/css/theme/cdp_1.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Computer Modern"; 3 | src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf'); 4 | } 5 | div.cell{ 6 | width:800px; 7 | margin-left:auto; 8 | margin-right:auto; 9 | } 10 | h1 { 11 | font-family: "Charis SIL", Palatino, serif; 12 | } 13 | div.text_cell_render{ 14 | font-family: Computer Modern, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif; 15 | line-height: 145%; 16 | font-size: 120%; 17 | width:800px; 18 | margin-left:auto; 19 | margin-right:auto; 20 | } 21 | .CodeMirror{ 22 | font-family: "Source Code Pro", source-code-pro,Consolas, monospace; 23 | } 24 | .prompt{ 25 | display: None; 26 | } 27 | .text_cell_render h5 { 28 | font-weight: 300; 29 | font-size: 16pt; 30 | color: #4057A1; 31 | font-style: italic; 32 | margin-bottom: .5em; 33 | margin-top: 0.5em; 34 | display: block; 35 | } 36 | 37 | .warning{ 38 | color: rgb( 240, 20, 20 ) 39 | } 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for nbviewer 4 | 5 | --- 6 | 7 | Hi! Thanks for using the Jupyter Notebook Viewer (nbviewer) and thinking about ways to improve it. Please use the template below to tell us your idea. 8 | 9 | If your suggestion is more relevant to another Jupyter project (e.g., [Jupyter Notebook](http://github.com/jupyter/notebook), [JupyterLab](http://github.com/jupyterlab/jupyterlab), [JupyterHub](http://github.com/jupyterhub/jupyterhub), etc.), please open an issue using that project's issue tracker instead. 10 | 11 | If you need help using or installing Jupyter Notebook Viewer, please use the [jupyter/help](https://github.com/jupyter/help) issue tracker instead. 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61", "setuptools-scm", "invoke"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "nbviewer" 7 | dynamic = ["version", "dependencies", "readme"] 8 | license = "BSD-3-Clause" 9 | license-files = ["LICENSE.txt"] 10 | description= "Jupyter Notebook Viewer" 11 | authors = [{name = "Jupyter Development Team", email = "jupyter@googlegroups.com"}] 12 | requires-python = ">=3.10" 13 | classifiers = [ 14 | "Programming Language :: Python :: 3", 15 | "Framework :: Jupyter", 16 | ] 17 | 18 | [project.urls] 19 | Homepage = "https://nbviewer.org" 20 | Funding = "https://jupyter.org/about" 21 | Source = "https://github.com/jupyter/nbviewer" 22 | Tracker = "https://github.com/jupyter/nbviewer/issues" 23 | 24 | [tool.setuptools] 25 | zip-safe = false 26 | include-package-data = true 27 | 28 | [tool.setuptools.packages.find] 29 | where = [""] 30 | include = ["nbviewer"] 31 | 32 | [tool.setuptools.dynamic] 33 | readme = { file = "README.md", content-type = "text/markdown" } 34 | dependencies = { file = "requirements.in" } 35 | 36 | # setuptools_scm needs a section to be present 37 | [tool.setuptools_scm] 38 | # we don't actually use setuptools_scm for versions, 39 | # only the file-finder 40 | fallback_version = "0.0.0" 41 | -------------------------------------------------------------------------------- /nbviewer/templates/dirview.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | {{ link_breadcrumbs(breadcrumbs) }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | {% for entry in entries %} 18 | 19 | 28 | 31 | 32 | {% endfor %} 33 | 34 |
NameModified
11 | {% if len(breadcrumbs) > 1 %} 12 | .. 13 | {% endif %} 14 |
20 | {% if entry.url %} 21 | 22 | {% endif %} 23 | {{entry.name}} 24 | {% if entry.url %} 25 | 26 | {% endif %} 27 | 29 | {{ entry.modtime }} 30 |
35 | 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-24.04 11 | strategy: 12 | matrix: 13 | python: 14 | - "3.10" 15 | - "3.13" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v6 19 | 20 | - uses: actions/setup-python@v6 21 | with: 22 | python-version: ${{ matrix.python }} 23 | 24 | - name: install apt packages 25 | # pycurl build requirements 26 | run: | 27 | sudo apt-get update && sudo apt-get -y install \ 28 | libcurl4-gnutls-dev \ 29 | libgnutls28-dev \ 30 | libmemcached-dev 31 | 32 | - name: install 33 | run: | 34 | python3 -m pip install -r requirements.in -r requirements-dev.txt 35 | python3 -m pip install -v -e . 36 | - name: install mypy 37 | run: | 38 | python3 -m pip install mypy types-pycurl types-requests types-Markdown pyflakes 39 | - name: run mypy 40 | run: | 41 | mypy nbviewer 42 | - name: run pyflakes 43 | run: | 44 | pyflakes nbviewer 45 | 46 | - name: pip freeze 47 | run: | 48 | python3 -m pip freeze 49 | 50 | - name: run tests 51 | run: | 52 | pytest -v nbviewer/tests -s 53 | -------------------------------------------------------------------------------- /nbviewer/templates/formats/slides.html: -------------------------------------------------------------------------------- 1 | {% extends "notebook.html" %} 2 | 3 | 4 | {% block style_base %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 25 | 28 | 29 | 30 | {% endblock %} 31 | 32 | 33 | {% block menu_extra %} 34 |
{{ link_breadcrumbs(breadcrumbs) }}
35 | {% endblock %} 36 | 37 | 38 | {% block container %} 39 | {{ body|safe }} 40 | {% endblock container %} 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Define a builder image 2 | FROM python:3.12-bookworm AS builder 3 | 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | ENV LANG=C.UTF-8 6 | RUN apt-get update \ 7 | && apt-get install -yq --no-install-recommends \ 8 | ca-certificates \ 9 | libcurl4-gnutls-dev \ 10 | libgnutls28-dev \ 11 | libmemcached-dev \ 12 | git \ 13 | nodejs \ 14 | npm 15 | 16 | # Build requirements 17 | COPY ./requirements-dev.txt /srv/nbviewer/ 18 | RUN python3 -mpip install -r /srv/nbviewer/requirements-dev.txt setuptools 19 | 20 | WORKDIR /srv/nbviewer 21 | 22 | # Copy source tree in 23 | COPY . /srv/nbviewer 24 | RUN python3 setup.py build && \ 25 | python3 -mpip wheel -vv -r requirements.txt . -w /wheels 26 | 27 | # Now define the runtime image 28 | FROM python:3.12-slim-bookworm 29 | LABEL maintainer="Jupyter Project " 30 | 31 | ENV DEBIAN_FRONTEND=noninteractive 32 | ENV LANG=C.UTF-8 33 | 34 | RUN apt-get update \ 35 | && apt-get install -yq --no-install-recommends \ 36 | ca-certificates \ 37 | libcurl4 \ 38 | libmemcached11 \ 39 | git \ 40 | && apt-get clean && rm -rf /var/lib/apt/lists/* 41 | 42 | 43 | RUN --mount=type=cache,from=builder,source=/wheels,target=/wheels \ 44 | python3 -mpip install --no-cache /wheels/* 45 | 46 | # To change the number of threads use 47 | # docker run -d -e NBVIEWER_THREADS=4 -p 80:8080 nbviewer 48 | ENV NBVIEWER_THREADS=2 49 | WORKDIR /srv/nbviewer 50 | EXPOSE 8080 51 | USER nobody 52 | 53 | EXPOSE 9000 54 | CMD ["python", "-m", "nbviewer", "--port=8080"] 55 | -------------------------------------------------------------------------------- /nbviewer/tests/test_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Optional 4 | from unittest import TestCase 5 | 6 | from jsonschema import validate # type: ignore 7 | 8 | 9 | ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) 10 | 11 | 12 | class JSONTestCase(TestCase): 13 | json: Optional[str] = None 14 | schema: Optional[str] = None 15 | 16 | def test_json(self): 17 | if not self.json: 18 | return 19 | 20 | try: 21 | json.load(open(os.path.join(ROOT, self.json), "r")) 22 | except Exception as err: 23 | self.fail("%s failed to parse: %s" % (self.json, err)) 24 | 25 | def test_schema(self): 26 | if not self.schema: 27 | return 28 | 29 | try: 30 | data = json.load(open(os.path.join(ROOT, self.json), "r")) 31 | schema = json.load(open(os.path.join(ROOT, self.schema), "r")) 32 | validate(data, schema) 33 | except Exception as err: 34 | self.fail( 35 | "%s failed to validate against %s: %s" % (self.json, self.schema, err) 36 | ) 37 | 38 | 39 | class FrontpageJSONTestCase(JSONTestCase): 40 | json = "nbviewer/frontpage.json" 41 | schema = "nbviewer/frontpage.schema.json" 42 | 43 | 44 | class BowerJSONTestCase(JSONTestCase): 45 | json = "nbviewer/static/bower.json" 46 | 47 | 48 | class BowerRcJSONTestCase(JSONTestCase): 49 | json = "nbviewer/static/.bowerrc" 50 | 51 | 52 | class NpmJSONTestCase(JSONTestCase): 53 | json = "package.json" 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Tell us about a bug in nbviewer 4 | 5 | --- 6 | 7 | Hi! Thanks for using Jupyter Notebook Viewer (nbviewer) and taking the time to report a bug you've encountered. Please use the template below to tell us about the problem. 8 | 9 | If you've found a bug in a different Jupyter project (e.g., [Jupyter Notebook](http://github.com/jupyter/notebook), [JupyterLab](http://github.com/jupyterlab/jupyterlab), [JupyterHub](http://github.com/jupyterhub/jupyterhub), etc.), please open an issue using that project's issue tracker instead. 10 | 11 | If you need help using or installing Jupyter Notebook Viewer, please use the [jupyter/help](https://github.com/jupyter/help) issue tracker instead. 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Browser [e.g. chrome, safari] 32 | - Version [e.g. 22] 33 | 34 | **Smartphone (please complete the following information):** 35 | - Device: [e.g. iPhone6] 36 | - OS: [e.g. iOS8.1] 37 | - Browser [e.g. stock browser, safari] 38 | - Version [e.g. 22] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /helm-chart/nbviewer/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "nbviewer.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 6 | {{- end -}} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | */}} 12 | {{- define "nbviewer.fullname" -}} 13 | {{- if .Values.fullnameOverride -}} 14 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 15 | {{- else -}} 16 | {{- $name := default .Chart.Name .Values.nameOverride -}} 17 | {{- if contains $name .Release.Name -}} 18 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 19 | {{- else -}} 20 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 21 | {{- end -}} 22 | {{- end -}} 23 | {{- end -}} 24 | 25 | {{/* 26 | Common labels 27 | */}} 28 | {{- define "nbviewer.labels" -}} 29 | app.kubernetes.io/name: {{ include "nbviewer.name" . }} 30 | helm.sh/chart: {{ include "nbviewer.chart" . }} 31 | app.kubernetes.io/instance: {{ .Release.Name }} 32 | {{- if .Chart.AppVersion }} 33 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 34 | {{- end }} 35 | app.kubernetes.io/managed-by: {{ .Release.Service }} 36 | {{- end -}} 37 | 38 | {{- define "nbviewer.matchLabels" -}} 39 | app.kubernetes.io/name: {{ include "nbviewer.name" . }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- end -}} 42 | 43 | {{/* 44 | Create chart name and version as used by the chart label. 45 | */}} 46 | {{- define "nbviewer.chart" -}} 47 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 48 | {{- end -}} 49 | -------------------------------------------------------------------------------- /nbviewer/static/less/layout.less: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: @navbar-height; 3 | &.scrolled .navbar-fixed-top { 4 | box-shadow: 1px 1px 1px #999; 5 | } 6 | } 7 | 8 | .navbar-header .navbar-toggle { 9 | padding: 5px 8px; 10 | } 11 | 12 | .fa.menu-icon { 13 | line-height: 8px; 14 | margin-right: 5px; 15 | position:relative; 16 | top:5px; 17 | } 18 | 19 | .back-to-top { 20 | margin: @navbar-height * 0.5; 21 | } 22 | 23 | footer { 24 | padding: 15px 0; 25 | background-color: @footer-bg; 26 | color: #ddd; 27 | 28 | a { 29 | color: #fff; 30 | &:hover{ 31 | color: #fff; 32 | text-decoration: dotted; 33 | } 34 | } 35 | } 36 | 37 | pre, code { 38 | /* restore default mono font */ 39 | font-family: monospace; 40 | } 41 | 42 | td.page_links { 43 | text-align: center; 44 | } 45 | 46 | .navbar-fixed-top { 47 | .nav > li > a{ 48 | &:hover, &.active{ 49 | color: @jupyter-brand; 50 | } 51 | } 52 | .navbar-brand { 53 | padding: 15px 0 0 15px; 54 | } 55 | } 56 | 57 | .breadcrumb a, .table-nbviewer a, .nbviewer-error a{ 58 | color: grey; 59 | white-space: nowrap; 60 | &:hover{ 61 | color: @jupyter-brand; 62 | text-decoration: none; 63 | }; 64 | } 65 | 66 | .container-main { 67 | min-height: 100vh; 68 | } 69 | 70 | // binder icon 71 | .fa-icon-binder { 72 | width: 24px; 73 | height: 24px; 74 | margin-top: -8px; 75 | background-image: url(../img/icon-binder.png); 76 | background-repeat: no-repeat; 77 | background-position: center right; 78 | background-size: 24px; 79 | &:hover, &.active { 80 | &:extend(.fa-icon-binder); 81 | background-image: url(../img/icon-binder-color.png); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /helm-chart/nbviewer/values.yaml: -------------------------------------------------------------------------------- 1 | image: "jupyter/nbviewer" 2 | imagePullPolicy: null 3 | 4 | nodeSelector: null 5 | 6 | pdbMinAvailable: 2 7 | 8 | pdb: 9 | minAvailable: 1 10 | 11 | resources: 12 | requests: 13 | memory: 256M 14 | cpu: "1" 15 | 16 | annotations: {} 17 | 18 | extraContainers: null 19 | extraVolumes: null 20 | 21 | livenessProbe: 22 | enabled: true 23 | initialDelaySeconds: 5 24 | periodSeconds: 10 25 | 26 | readinessProbe: 27 | enabled: true 28 | initialDelaySeconds: 5 29 | periodSeconds: 10 30 | 31 | service: 32 | type: LoadBalancer 33 | ports: 34 | nodePort: null 35 | 36 | github: 37 | accessToken: "" 38 | clientId: "" 39 | clientSecret: "" 40 | 41 | nbviewer: 42 | baseUrl: "/" 43 | extraArgs: [] 44 | 45 | memcached: 46 | config: 47 | memoryLimit: 250 48 | replicaCount: 2 49 | # memory limits should be a bit bigger than config.memoryLimit 50 | resources: 51 | limits: 52 | cpu: 1 53 | memory: 300MB 54 | requests: 55 | cpu: 100m 56 | memory: 300MB 57 | affinity: 58 | podAntiAffinity: 59 | preferredDuringSchedulingIgnoredDuringExecution: 60 | - weight: 100 61 | podAffinityTerm: 62 | labelSelector: 63 | matchExpressions: 64 | - key: app.kubernetes.io/name 65 | operator: In 66 | values: 67 | - memcached 68 | topologyKey: kubernetes.io/hostname 69 | 70 | updateStrategy: 71 | rollingUpdate: 72 | maxSurge: 1 73 | maxUnavailable: 1 74 | 75 | statuspage: 76 | enabled: false 77 | image: jupyter/nbviewer-statuspage 78 | resources: null 79 | apiKey: "" 80 | pageId: "" 81 | metricId: "" 82 | -------------------------------------------------------------------------------- /nbviewer/providers/url/tests/test_url.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | import unittest 8 | 9 | import requests 10 | 11 | from ....tests.base import FormatHTMLMixin 12 | from ....tests.base import NBViewerTestCase 13 | 14 | 15 | class URLTestCase(NBViewerTestCase): 16 | def test_url(self): 17 | url = self.url("url/jdj.mit.edu/~stevenj/IJulia Preview.ipynb") 18 | r = requests.get(url) 19 | # Base class overrides assertIn to do unicode in unicode checking 20 | # We want to use the original unittest implementation 21 | unittest.TestCase.assertIn(self, r.status_code, (200, 202)) 22 | self.assertIn("Download Notebook", r.text) 23 | 24 | def test_urls_with_querystring(self): 25 | # This notebook is only available if the querystring is passed through. 26 | # Notebook URL: https://bug1348008.bmoattachments.org/attachment.cgi?id=8860059 27 | url = self.url( 28 | "urls/bug1348008.bmoattachments.org/attachment.cgi/%3Fid%3D8860059" 29 | ) 30 | r = requests.get(url) 31 | # Base class overrides assertIn to do unicode in unicode checking 32 | # We want to use the original unittest implementation 33 | unittest.TestCase.assertIn(self, r.status_code, (200, 202)) 34 | self.assertIn("Download Notebook", r.text) 35 | 36 | 37 | class FormatHTMLURLTestCase(URLTestCase, FormatHTMLMixin): 38 | pass 39 | -------------------------------------------------------------------------------- /nbviewer/templates/tabular.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 | 5 | 6 | {% block header_row %} 7 | 8 | 9 | 10 | {% endblock header_row %} 11 | 12 | 13 | {% block entries %} 14 | {% for entry in entries %} 15 | {% block entry scoped %} 16 | 17 | 23 | 24 | {% endblock entry %} 25 | {% endfor %} 26 | {% endblock entries %} 27 | 28 | 29 | {% block page_links %} 30 | {% if prev_url or next_url %} 31 | 32 | 51 | 52 | {% endif %} 53 | {% endblock page_links %} 54 | 55 |
Name
18 | 19 | 20 | {{entry.name}} 21 | 22 |
56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | node_js: 4 | - 6 5 | 6 | python: 7 | - 3.5 8 | - 3.6 9 | 10 | before_install: 11 | - sudo apt-get update 12 | - sudo apt-get install -qq libzmq3-dev pandoc libcurl4-gnutls-dev libmemcached-dev libgnutls28-dev 13 | - pip install --upgrade setuptools pip 14 | - pip install -r requirements-dev.txt 15 | 16 | install: 17 | - pip install --upgrade setuptools pip 18 | - pip install -r requirements.txt 19 | - pip install -e . 20 | 21 | # run tests 22 | script: 23 | - invoke test 24 | 25 | # list the jobs 26 | jobs: 27 | include: 28 | - name: autoformatting check 29 | python: 3.6 30 | # NOTE: It does not suffice to override to: null, [], or [""]. Travis will 31 | # fall back to the default if we do. 32 | before_install: echo "Do nothing before install." 33 | install: pip install pre-commit 34 | script: 35 | - pre-commit run --all-files 36 | after_success: echo "Do nothing after success." 37 | after_failure: 38 | - | 39 | echo "You can install pre-commit hooks to automatically run formatting" 40 | echo "on each commit with:" 41 | echo " pre-commit install" 42 | echo "or you can run by hand on staged files with" 43 | echo " pre-commit run" 44 | echo "or after-the-fact on already committed files with" 45 | echo " pre-commit run --all-files" 46 | 47 | env: 48 | global: 49 | - secure: Sv53YMdsVTin1hUPRqIuvdAOJ0UwklEowW49qpxY9wSgiAM79D+e1b5Yxrn+RTtS3WGlvK1aKHICc+2ajccEJkKFL8WDy2SnTnoWPadrEy4NAGLkNMGK+bAYMnLNoNRbSGVz5JpvNJ7JkeaEplhJ572OJOxa1X7ZF9165ZbOWng= 50 | - secure: ajFM7ch1/xYyEjusyTzd963GOOLg5/H0lxvQ7L6r+LBDDro79FxNPMcAkZxF7n24rkPO8I+AP3FfUwbQf4ShmGkAdsxSFMc2d7GDUowxiicPr5bMitygxlzl2ox2lWdpt4QldmEywbrCKKwt/cZkKxE8er9xBcwe7xw/2xUYOLk= 51 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ============================= 2 | The NbViewer licensing terms 3 | ============================= 4 | 5 | NbViewer is licensed under the terms of the Modified BSD License (also known as 6 | New or Revised BSD), as follows: 7 | 8 | Copyright (c) 2012-2013, IPython Development Team 9 | 10 | All rights reserved. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions are met: 14 | 15 | Redistributions of source code must retain the above copyright notice, this 16 | list of conditions and the following disclaimer. 17 | 18 | Redistributions in binary form must reproduce the above copyright notice, this 19 | list of conditions and the following disclaimer in the documentation and/or 20 | other materials provided with the distribution. 21 | 22 | Neither the name of the IPython Development Team nor the names of its 23 | contributors may be used to endorse or promote products derived from this 24 | software without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 27 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 28 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 29 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 30 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 31 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 33 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 34 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | -------------------------------------------------------------------------------- /nbviewer/providers/local/tests/test_localfile.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | import requests 8 | 9 | from ....tests.base import FormatHTMLMixin 10 | from ....tests.base import NBViewerTestCase 11 | 12 | 13 | class LocalFileDefaultTestCase(NBViewerTestCase): 14 | @classmethod 15 | def get_server_cmd(cls): 16 | return super().get_server_cmd() + ["--localfiles=."] 17 | 18 | def test_url(self): 19 | ## assumes being run from base of this repo 20 | url = self.url("localfile/nbviewer/tests/notebook.ipynb") 21 | r = requests.get(url) 22 | self.assertEqual(r.status_code, 200) 23 | 24 | 25 | class FormatHTMLLocalFileDefaultTestCase(LocalFileDefaultTestCase, FormatHTMLMixin): 26 | pass 27 | 28 | 29 | class LocalFileRelativePathTestCase(NBViewerTestCase): 30 | @classmethod 31 | def get_server_cmd(cls): 32 | return super().get_server_cmd() + ["--localfiles=nbviewer"] 33 | 34 | def test_url(self): 35 | ## assumes being run from base of this repo 36 | url = self.url("localfile/tests/notebook.ipynb") 37 | r = requests.get(url) 38 | self.assertEqual(r.status_code, 200) 39 | 40 | def test_404(self): 41 | ## assumes being run from base of this repo 42 | url = self.url("localfile/doesntexist") 43 | r = requests.get(url) 44 | self.assertEqual(r.status_code, 404) 45 | 46 | 47 | class FormatHTMLLocalFileRelativePathTestCase( 48 | LocalFileRelativePathTestCase, FormatHTMLMixin 49 | ): 50 | pass 51 | -------------------------------------------------------------------------------- /statuspage/statuspage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import os 4 | import sys 5 | import time 6 | from datetime import datetime 7 | 8 | import requests 9 | 10 | api_key = os.environ["STATUSPAGE_API_KEY"] 11 | page_id = os.environ["STATUSPAGE_PAGE_ID"] 12 | metric_id = os.environ["STATUSPAGE_METRIC_ID"] 13 | api_base = "api.statuspage.io" 14 | 15 | github_id = os.environ["GITHUB_OAUTH_KEY"] 16 | github_secret = os.environ["GITHUB_OAUTH_SECRET"] 17 | 18 | 19 | def get_rate_limit(): 20 | """Retrieve the current GitHub rate limit for our auth tokens""" 21 | r = requests.get( 22 | "https://api.github.com/rate_limit", auth=(github_id, github_secret) 23 | ) 24 | r.raise_for_status() 25 | resp = r.json() 26 | return resp["resources"]["core"] 27 | 28 | 29 | def post_data(limit, remaining, **ignore): 30 | """Send the percent-remaining GitHub rate limit to statuspage""" 31 | percent = 100 * remaining / limit 32 | now = int(datetime.utcnow().timestamp()) 33 | url = "https://api.statuspage.io/v1/pages/{page_id}/metrics/{metric_id}/data.json".format( 34 | page_id=page_id, metric_id=metric_id 35 | ) 36 | 37 | r = requests.post( 38 | url, 39 | headers={ 40 | "Content-Type": "application/x-www-form-urlencoded", 41 | "Authorization": "OAuth " + api_key, 42 | }, 43 | data={"data[timestamp]": now, "data[value]": percent}, 44 | ) 45 | r.raise_for_status() 46 | 47 | 48 | def get_and_post(): 49 | data = get_rate_limit() 50 | print(json.dumps(data)) 51 | post_data(limit=data["limit"], remaining=data["remaining"]) 52 | 53 | 54 | while True: 55 | try: 56 | get_and_post() 57 | except Exception as e: 58 | print("Error: %s" % e, file=sys.stderr) 59 | # post every two minutes 60 | time.sleep(120) 61 | -------------------------------------------------------------------------------- /.github/workflows/image.yaml: -------------------------------------------------------------------------------- 1 | name: docker 2 | 3 | on: 4 | push: 5 | branches: 6 | pull_request: 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v6 14 | 15 | - name: Set some variables 16 | id: vars 17 | run: | 18 | short_sha=$(git rev-parse --short HEAD) 19 | image="jupyter/nbviewer:$short_sha" 20 | echo "image=$image" >> $GITHUB_OUTPUT 21 | tags="$image" 22 | if "${{ github.ref_name }}" == "main" ]]; then 23 | tags="jupyter/nbviewer:latest $tags" 24 | fi 25 | echo "tags=$tags" >> $GITHUB_OUTPUT 26 | 27 | - name: Check outputs 28 | run: echo ${{ steps.vars.outputs.sha_short }} 29 | 30 | - name: Login to docker hub 31 | if: github.ref_name == 'main' 32 | uses: docker/login-action@v3 33 | with: 34 | username: ${{ secrets.DOCKER_USERNAME }} 35 | password: ${{ secrets.DOCKER_PASSWORD }} 36 | 37 | - name: Build 38 | uses: docker/build-push-action@v6 39 | with: 40 | push: false 41 | load: true 42 | context: . 43 | platforms: linux/amd64 44 | tags: ${{ steps.vars.outputs.tags }} 45 | 46 | - name: Test 47 | run: | 48 | docker run --rm -i ${{ steps.vars.outputs.image }} python3 -c "import nbviewer, pycurl, pylibmc" 49 | docker run --rm -i ${{ steps.vars.outputs.image }} python3 -m nbviewer --help-all 50 | 51 | - name: Push 52 | if: github.ref_name == 'main' 53 | uses: docker/build-push-action@v6 54 | with: 55 | context: . 56 | platforms: linux/amd64 57 | push: ${{ github.ref_name == 'main' }} 58 | tags: ${{ steps.vars.outputs.tags }} 59 | -------------------------------------------------------------------------------- /nbviewer/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 | {% if title or subtitle or text or show_input %} 5 |
6 | {% if title %}

{{ title }}

{% endif %} 7 | {% if subtitle %}

{{ subtitle }}

{% endif %} 8 | {% if text %} 9 | {{ text | markdown | safe }} 10 | {% endif %} 11 | {% if show_input %} 12 |
13 |
14 |
15 |
16 | 22 | 23 | 26 | 27 |
28 |
29 |
30 |
31 | {% endif %} 32 |
33 | {% endif %} 34 | 35 | {% for section in sections %} 36 |
37 |

{{section.header}}

38 |
39 |
    40 | {% for link in section.links %} 41 |
  • 42 | 43 | 44 | 45 | 46 |
  • 47 | {% endfor %} 48 |
49 |
50 |
51 | {% endfor %} 52 | 53 | {% endblock body %} 54 | -------------------------------------------------------------------------------- /nbviewer/index.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | """ 8 | Classes for Indexing Notebooks 9 | """ 10 | import uuid 11 | 12 | from tornado.log import app_log 13 | 14 | 15 | class Indexer(object): 16 | def index_notebook(self, notebook_url, notebook_contents): 17 | raise NotImplementedError("index_notebook not implemented") 18 | 19 | 20 | class NoSearch(Indexer): 21 | def __init__(self): 22 | pass 23 | 24 | def index_notebook(self, notebook_url, notebook_contents, *args, **kwargs): 25 | app_log.debug('Totally not indexing "{}"'.format(notebook_url)) 26 | 27 | 28 | class ElasticSearch(Indexer): 29 | def __init__(self, host="127.0.0.1", port=9200): 30 | from elasticsearch import Elasticsearch 31 | 32 | self.elasticsearch = Elasticsearch([{"host": host, "port": port}]) 33 | 34 | def index_notebook(self, notebook_url, notebook_contents, public=False): 35 | notebook_url = notebook_url.encode("utf-8") 36 | notebook_id = uuid.uuid5(uuid.NAMESPACE_URL, notebook_url) 37 | 38 | # Notebooks API Model 39 | # https://github.com/ipython/ipython/wiki/IPEP-16%3A-Notebook-multi-directory-dashboard-and-URL-mapping#notebooks-api 40 | body = {"content": notebook_contents, "public": public} 41 | 42 | resp = self.elasticsearch.index( 43 | index="notebooks", doc_type="ipynb", body=body, id=notebook_id.hex 44 | ) 45 | if resp["created"]: 46 | app_log.info( 47 | "Created new indexed notebook={}, public={}".format( 48 | notebook_url, public 49 | ) 50 | ) 51 | else: 52 | app_log.info( 53 | "Indexing old notebook={}, public={}".format(notebook_url, public) 54 | ) 55 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) 2013 The IPython Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | import os 8 | import shlex 9 | import sys 10 | from subprocess import check_call 11 | 12 | from setuptools import setup 13 | from setuptools.command.develop import develop 14 | from setuptools.command.build_py import build_py 15 | from setuptools.command.sdist import sdist 16 | 17 | 18 | def sh(cmd): 19 | """Run a command, echoing what command is to be run""" 20 | print("Running command %s" % " ".join(map(shlex.quote, cmd)), file=sys.stderr) 21 | check_call(cmd) 22 | 23 | 24 | def preflight(): 25 | print("Building LESS", file=sys.stderr) 26 | sh(["invoke", "git-info"]) 27 | sh(["npm", "install"]) 28 | sh(["invoke", "bower"]) 29 | sh(["invoke", "less"]) 30 | 31 | 32 | def invoke_first(cmd): 33 | class InvokeFirst(cmd): 34 | def run(self): 35 | preflight() 36 | return cmd.run(self) 37 | 38 | return InvokeFirst 39 | 40 | 41 | def walk_subpkg(name): 42 | data_files = [] 43 | package_dir = "nbviewer" 44 | for parent, dirs, files in os.walk(os.path.join(package_dir, name)): 45 | sub_dir = os.sep.join( 46 | parent.split(os.sep)[1:] 47 | ) # remove package_dir from the path 48 | for f in files: 49 | data_files.append(os.path.join(sub_dir, f)) 50 | return data_files 51 | 52 | 53 | pkg_data = { 54 | "nbviewer": ( 55 | ["frontpage.json"] 56 | + walk_subpkg("static") 57 | + walk_subpkg("templates") 58 | + walk_subpkg("providers") 59 | ) 60 | } 61 | 62 | cmdclass = {} 63 | # run invoke prior to develop/sdist 64 | cmdclass["develop"] = invoke_first(develop) 65 | cmdclass["build_py"] = invoke_first(build_py) 66 | cmdclass["sdist"] = invoke_first(sdist) 67 | 68 | 69 | setup( 70 | package_data=pkg_data, 71 | cmdclass=cmdclass, 72 | ) 73 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to NBViewer 2 | 3 | 4 | Welcome! As a [Jupyter](https://jupyter.org) project, 5 | you can follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html). 6 | 7 | Make sure to also follow [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md) 8 | for a friendly and welcoming collaborative environment. 9 | 10 | ## Setting up a development environment 11 | 12 | See the instructions for local development or local installation first. 13 | 14 | NBViewer has adopted automatic code formatting so you shouldn't 15 | need to worry too much about your code style. 16 | As long as your code is valid, 17 | the pre-commit hook should take care of how it should look. Here is how to set up pre-commit hooks for automatic code formatting, etc. 18 | 19 | ```bash 20 | pre-commit install 21 | ``` 22 | 23 | You can also invoke the pre-commit hook manually at any time with 24 | 25 | ```bash 26 | pre-commit run 27 | ``` 28 | 29 | which should run any autoformatting on your code 30 | and tell you about any errors it couldn't fix automatically. 31 | You may also install [black integration](https://github.com/ambv/black#editor-integration) 32 | into your text editor to format code automatically. 33 | 34 | If you have already committed files before setting up the pre-commit 35 | hook with `pre-commit install`, you can fix everything up using 36 | `pre-commit run --all-files`. You need to make the fixing commit 37 | yourself after that. 38 | 39 | #### Running the Tests 40 | 41 | It's a good idea to write tests to exercise any new features, 42 | or that trigger any bugs that you have fixed to catch regressions. `nose` is used to run the test suite. The tests currently make calls to 43 | external APIs such as GitHub, so it is best to use your Github API Token when 44 | running: 45 | 46 | ```shell 47 | $ cd 48 | $ pip install -r requirements-dev.txt 49 | $ GITHUB_API_TOKEN= python setup.py test 50 | ``` 51 | 52 | You can run the tests with: 53 | 54 | ```bash 55 | nosetests -v 56 | ``` 57 | 58 | in the repo directory. 59 | -------------------------------------------------------------------------------- /nbviewer/log.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | import json 8 | 9 | from tornado.log import access_log 10 | from tornado.web import StaticFileHandler 11 | 12 | 13 | def log_request(handler): 14 | """log a bit more information about each request than tornado's default 15 | 16 | - move static file get success to debug-level (reduces noise) 17 | - get proxied IP instead of proxy IP 18 | - log referer for redirect and failed requests 19 | - log user-agent for failed requests 20 | """ 21 | status = handler.get_status() 22 | request = handler.request 23 | if ( 24 | status == 304 25 | or (status < 300 and isinstance(handler, StaticFileHandler)) 26 | or (status < 300 and request.uri == "/") 27 | ): 28 | # static-file successes or any 304 FOUND are debug-level 29 | log_method = access_log.debug 30 | elif status < 400: 31 | log_method = access_log.info 32 | elif status < 500: 33 | log_method = access_log.warning 34 | else: 35 | log_method = access_log.error 36 | 37 | request_time = 1000.0 * handler.request.request_time() 38 | ns = dict( 39 | status=status, 40 | method=request.method, 41 | ip=request.remote_ip, 42 | uri=request.uri, 43 | request_time=request_time, 44 | ) 45 | msg = "{status} {method} {uri} ({ip}) {request_time:.2f}ms" 46 | if status >= 300: 47 | # log referers on redirects 48 | ns["referer"] = request.headers.get("Referer", "None") 49 | msg = msg + ' referer="{referer}"' 50 | if status >= 400: 51 | # log user agent for failed requests 52 | ns["agent"] = request.headers.get("User-Agent", "Unknown") 53 | msg = msg + ' user-agent="{agent}"' 54 | if status >= 500 and status not in {502, 503}: 55 | # log all headers if it caused an error 56 | log_method(json.dumps(dict(request.headers), indent=2)) 57 | log_method(msg.format(**ns)) 58 | -------------------------------------------------------------------------------- /nbviewer/render.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | from nbconvert.exporters import Exporter # type: ignore 8 | from tornado.log import app_log 9 | 10 | # ----------------------------------------------------------------------------- 11 | # 12 | # ----------------------------------------------------------------------------- 13 | 14 | 15 | class NbFormatError(Exception): 16 | pass 17 | 18 | 19 | exporters = {} 20 | 21 | 22 | def render_notebook(format, nb, url=None, forced_theme=None, config=None): 23 | exporter = format["exporter"] 24 | 25 | if not isinstance(exporter, Exporter): 26 | # allow exporter to be passed as a class, rather than instance 27 | # because Exporter instances cannot be passed across multiprocessing boundaries 28 | # instances are cached by class to avoid repeated instantiation of duplicates 29 | exporter_cls = exporter 30 | if exporter_cls not in exporters: 31 | app_log.info("instantiating %s" % exporter_cls.__name__) 32 | exporters[exporter_cls] = exporter_cls(config=config, log=app_log) 33 | exporter = exporters[exporter_cls] 34 | 35 | css_theme = nb.get("metadata", {}).get("_nbviewer", {}).get("css", None) 36 | 37 | if not css_theme or not css_theme.strip(): 38 | # whitespace 39 | css_theme = None 40 | 41 | if forced_theme: 42 | css_theme = forced_theme 43 | 44 | # get the notebook title, if any 45 | try: 46 | name = nb.metadata.name 47 | except AttributeError: 48 | name = "" 49 | 50 | if not name and url is not None: 51 | name = url.rsplit("/")[-1] 52 | 53 | if not name.endswith(".ipynb"): 54 | name = name + ".ipynb" 55 | 56 | html, resources = exporter.from_notebook_node(nb) 57 | 58 | if "postprocess" in format: 59 | html, resources = format["postprocess"](html, resources) 60 | 61 | config = {"download_name": name, "css_theme": css_theme} 62 | 63 | return html, config 64 | -------------------------------------------------------------------------------- /nbviewer/ratelimit.py: -------------------------------------------------------------------------------- 1 | """Object for tracking rate-limited requests""" 2 | 3 | # Copyright (c) Jupyter Development Team. 4 | # Distributed under the terms of the Modified BSD License. 5 | import hashlib 6 | 7 | from tornado.log import app_log 8 | from tornado.web import HTTPError 9 | 10 | 11 | class RateLimiter(object): 12 | """Rate limit checking object""" 13 | 14 | def __init__(self, limit, interval, cache): 15 | self.limit = limit 16 | self.interval = interval 17 | self.cache = cache 18 | 19 | def key_for_handler(self, handler): 20 | """Identify a visitor. 21 | 22 | Currently combine ip + user-agent. 23 | We don't need to be perfect. 24 | """ 25 | agent = handler.request.headers.get("User-Agent", "") 26 | return "rate-limit:{}:{}".format( 27 | handler.request.remote_ip, 28 | hashlib.md5(agent.encode("utf8", "replace")).hexdigest(), 29 | ) 30 | 31 | async def check(self, handler): 32 | """Check the rate limit for a handler. 33 | 34 | Identifies the source by ip and user-agent. 35 | 36 | If the rate limit is exceeded, raise HTTPError(429) 37 | """ 38 | if not self.limit: 39 | return 40 | key = self.key_for_handler(handler) 41 | added = await self.cache.add(key, 1, self.interval) 42 | if not added: 43 | # it's been seen before, use incr 44 | try: 45 | count = await self.cache.incr(key) 46 | except Exception: 47 | app_log.warning("Failed to increment rate limit for %()s", key) 48 | return 49 | 50 | app_log.debug( 51 | "Rate limit remaining for %r: %s/%s", 52 | key, 53 | self.limit - count, 54 | self.limit, 55 | ) 56 | 57 | if count and count >= self.limit: 58 | minutes = self.interval // 60 59 | raise HTTPError( 60 | 429, 61 | "Rate limit exceeded for {ip} ({limit} req / {minutes} min)." 62 | " Try again later.".format( 63 | ip=handler.request.remote_ip, limit=self.limit, minutes=minutes 64 | ), 65 | ) 66 | -------------------------------------------------------------------------------- /nbviewer/templates/treelist.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | 4 | {% macro ref_list(ref_type, refs) %} 5 |
6 | 7 | 16 |
17 | {% endmacro %} 18 | 19 | 20 | {% macro ref_dropdown(current, branches, tags) -%} 21 | 34 | {%- endmacro %} 35 | 36 | 37 | {% block body %} 38 |
39 | {% if branches|length + tags|length > 1 %} 40 | 43 | {% endif %} 44 | 45 | {{ link_breadcrumbs(breadcrumbs) }} 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 66 | 67 | {% for entry in entries %} 68 | 69 | 79 | 80 | {% endfor %} 81 | 82 |
Name
55 | {% if len(breadcrumbs) > 1 %} 56 | 57 | .. 58 | 59 | {% else %} 60 | 61 | {{user}}'s 62 | {{ tree_label | default('repositories')}} 63 | 64 | {% endif %} 65 |
70 | {% if entry.url %} 71 | 72 | {% endif %} 73 | 74 | {{entry.name}} 75 | {% if entry.url %} 76 | 77 | {% endif %} 78 |
83 | {% endblock %} 84 | -------------------------------------------------------------------------------- /nbviewer/static/less/bootstrap.less: -------------------------------------------------------------------------------- 1 | // Core variables and mixins 2 | @import "../components/bootstrap/less/variables.less"; 3 | @import "../components/bootstrap/less/mixins.less"; 4 | 5 | // Reset and dependencies 6 | @import "../components/bootstrap/less/normalize.less"; 7 | @import "../components/bootstrap/less/print.less"; 8 | // @import "../components/bootstrap/less/glyphicons.less"; 9 | 10 | // Core CSS 11 | @import "../components/bootstrap/less/scaffolding.less"; 12 | @import "../components/bootstrap/less/type.less"; 13 | @import "../components/bootstrap/less/code.less"; 14 | @import "../components/bootstrap/less/grid.less"; 15 | @import "../components/bootstrap/less/tables.less"; 16 | @import "../components/bootstrap/less/forms.less"; 17 | @import "../components/bootstrap/less/buttons.less"; 18 | 19 | // Components 20 | @import "../components/bootstrap/less/component-animations.less"; 21 | @import "../components/bootstrap/less/dropdowns.less"; 22 | @import "../components/bootstrap/less/button-groups.less"; 23 | @import "../components/bootstrap/less/input-groups.less"; 24 | @import "../components/bootstrap/less/navs.less"; 25 | @import "../components/bootstrap/less/navbar.less"; 26 | @import "../components/bootstrap/less/breadcrumbs.less"; 27 | @import "../components/bootstrap/less/pagination.less"; 28 | @import "../components/bootstrap/less/pager.less"; 29 | @import "../components/bootstrap/less/labels.less"; 30 | @import "../components/bootstrap/less/badges.less"; 31 | @import "../components/bootstrap/less/jumbotron.less"; 32 | @import "../components/bootstrap/less/thumbnails.less"; 33 | @import "../components/bootstrap/less/alerts.less"; 34 | @import "../components/bootstrap/less/progress-bars.less"; 35 | @import "../components/bootstrap/less/media.less"; 36 | @import "../components/bootstrap/less/list-group.less"; 37 | @import "../components/bootstrap/less/panels.less"; 38 | @import "../components/bootstrap/less/responsive-embed.less"; 39 | @import "../components/bootstrap/less/wells.less"; 40 | @import "../components/bootstrap/less/close.less"; 41 | 42 | // Components w/ JavaScript 43 | @import "../components/bootstrap/less/modals.less"; 44 | @import "../components/bootstrap/less/tooltip.less"; 45 | @import "../components/bootstrap/less/popovers.less"; 46 | @import "../components/bootstrap/less/carousel.less"; 47 | 48 | // Utility classes 49 | @import "../components/bootstrap/less/utilities.less"; 50 | @import "../components/bootstrap/less/responsive-utilities.less"; 51 | -------------------------------------------------------------------------------- /nbviewer/static/less/slides.less: -------------------------------------------------------------------------------- 1 | // does not bundle reveal because of theme 2 | @import "notebook"; 3 | 4 | /* Overrides of notebook CSS for static HTML export */ 5 | body { 6 | overflow-x: hidden; 7 | overflow-y: auto; 8 | line-height: inherit; 9 | } 10 | 11 | .reveal { 12 | font-size: 130%; 13 | overflow-y: scroll; 14 | 15 | pre { 16 | width: inherit; 17 | padding: 0.4em; 18 | margin: 0px; 19 | font-family: monospace, sans-serif; 20 | font-size: 80%; 21 | box-shadow: 0px 0px 0px rgba(0, 0, 0, 0); 22 | 23 | code { 24 | padding: 0px; 25 | } 26 | } 27 | 28 | section img { 29 | border: 0px solid black; 30 | box-shadow: 0 0 10px rgba(0, 0, 0, 0); 31 | } 32 | 33 | i { 34 | font-style: normal; 35 | font-family: FontAwesome; 36 | font-size: 2em; 37 | } 38 | 39 | .slides { 40 | text-align: left; 41 | } 42 | 43 | &.fade { 44 | opacity: 1; 45 | } 46 | 47 | .text_cell.rendered .rendered_html { 48 | /* The H1 height seems miscalculated, we are just hidding the scrollbar */ 49 | overflow-y: hidden; 50 | } 51 | } 52 | 53 | div.input_area { 54 | padding: 0.06em; 55 | } 56 | 57 | div.code_cell { 58 | background-color: transparent; 59 | } 60 | 61 | div.prompt { 62 | width: 11ex; 63 | padding: 0.4em; 64 | margin: 0px; 65 | font-family: monospace, sans-serif; 66 | font-size: 80%; 67 | text-align: right; 68 | } 69 | 70 | div.output_area pre { 71 | font-family: monospace, sans-serif; 72 | font-size: 80%; 73 | } 74 | 75 | div.output_prompt { 76 | /* 5px right shift to account for margin in parent container */ 77 | margin: 5px 5px 0 0; 78 | } 79 | 80 | .rendered_html p { 81 | text-align: inherit; 82 | } 83 | 84 | .container { 85 | height: inherit; 86 | } 87 | 88 | .footer{ 89 | display: none; 90 | } 91 | 92 | #menubar{ 93 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 94 | font-size: 14px; 95 | 96 | .container .container { 97 | position: absolute; 98 | } 99 | } 100 | 101 | @media screen and (min-width:980px) { 102 | .navbar-inner { 103 | opacity: 0.5; 104 | transition: opacity 0.5s ease-in-out; 105 | -webkit-transition: opacity 0.5s ease-in-out; 106 | } 107 | } 108 | 109 | @media screen and (max-width:767px) { 110 | #menubar { 111 | position: fixed; 112 | opacity: 0.9; 113 | 114 | .container{ 115 | padding: 0px 20px; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /nbviewer/formats.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | 8 | 9 | def default_formats(): 10 | """ 11 | Return the currently-implemented formats. 12 | 13 | These are not classes, but maybe should be: would they survive pickling? 14 | 15 | - exporter: 16 | an Exporter subclass. 17 | if not defined, and key is in nbconvert.export.exporter_map, it will be added 18 | automatically 19 | - nbconvert_template: 20 | the name of the nbconvert template to add to config.ExporterClass 21 | - test: 22 | a function(notebook_object, notebook_json) 23 | conditionally offer a format based on content if truthy. see 24 | `RenderingHandler.filter_exporters` 25 | - postprocess: 26 | a function(html, resources) 27 | perform any modifications to html and resources after nbconvert 28 | - content_Type: 29 | a string specifying the Content-Type of the response from this format. 30 | Defaults to text/html; charset=UTF-8 31 | """ 32 | 33 | def test_slides(nb, json): 34 | """Determines if at least one cell has a non-blank or "-" as its 35 | metadata.slideshow.slide_type value. 36 | 37 | Parameters 38 | ---------- 39 | nb: nbformat.notebooknode.NotebookNode 40 | Top of the parsed notebook object model 41 | json: str 42 | JSON source of the notebook, unused 43 | 44 | Returns 45 | ------- 46 | bool 47 | """ 48 | for cell in nb.cells: 49 | if ( 50 | "metadata" in cell 51 | and "slideshow" in cell.metadata 52 | and cell.metadata.slideshow.get("slide_type", "-") != "-" 53 | ): 54 | return True 55 | return False 56 | 57 | return { 58 | "html": {"nbconvert_template": "lab", "label": "Notebook", "icon": "book"}, 59 | "slides": { 60 | # "nbconvert_template": "slides_reveal", 61 | "label": "Slides", 62 | "icon": "gift", 63 | "test": test_slides, 64 | }, 65 | "script": { 66 | "label": "Code", 67 | "icon": "code", 68 | "content_type": "text/plain; charset=UTF-8", 69 | }, 70 | } 71 | -------------------------------------------------------------------------------- /nbviewer/tests/test_format_slides.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | import requests 8 | 9 | from ..providers.local.tests.test_localfile import LocalFileDefaultTestCase 10 | from .base import NBViewerTestCase 11 | from .base import skip_unless_github_auth 12 | 13 | 14 | class SlidesGistTestCase(NBViewerTestCase): 15 | @skip_unless_github_auth 16 | def test_gist(self): 17 | url = self.url("/format/slides/0c5b3639b10ed3d7cc85/single-cell.ipynb") 18 | r = requests.get(url) 19 | self.assertEqual(r.status_code, 200) 20 | html = r.content 21 | self.assertIn("reveal.js", html) 22 | 23 | @skip_unless_github_auth 24 | def test_html_exporter_link(self): 25 | url = self.url("/format/slides/0c5b3639b10ed3d7cc85/single-cell.ipynb") 26 | r = requests.get(url) 27 | self.assertEqual(r.status_code, 200) 28 | html = r.content 29 | self.assertIn("/gist/minrk/0c5b3639b10ed3d7cc85/single-cell.ipynb", html) 30 | self.assertNotIn("//gist/minrk/0c5b3639b10ed3d7cc85/single-cell.ipynb", html) 31 | 32 | @skip_unless_github_auth 33 | def test_no_slides_exporter_link(self): 34 | url = self.url("/0c5b3639b10ed3d7cc85/single-cell.ipynb") 35 | r = requests.get(url) 36 | self.assertEqual(r.status_code, 200) 37 | html = r.content 38 | self.assertNotIn("/format/slides/gist/minrk/7518294/Untitled0.ipynb", html) 39 | 40 | 41 | class SlideLocalFileDefaultTestCase(LocalFileDefaultTestCase): 42 | def test_slides_local(self): 43 | ## assumes being run from base of this repo 44 | url = self.url("format/slides/localfile/nbviewer/tests/notebook.ipynb") 45 | r = requests.get(url) 46 | self.assertEqual(r.status_code, 200) 47 | html = r.content 48 | self.assertIn("reveal.js", html) 49 | 50 | 51 | class SlidesGitHubTestCase(NBViewerTestCase): 52 | def ipython_example(self, *parts, **kwargs): 53 | ref = kwargs.get("ref", "rel-2.0.0") 54 | return self.url( 55 | "/format/slides/github/ipython/ipython/blob/%s/examples" % ref, *parts 56 | ) 57 | 58 | @skip_unless_github_auth 59 | def test_github(self): 60 | url = self.ipython_example("Index.ipynb") 61 | r = requests.get(url) 62 | self.assertEqual(r.status_code, 200) 63 | html = r.content 64 | self.assertIn("reveal.js", html) 65 | -------------------------------------------------------------------------------- /nbviewer/providers/gist/tests/test_gist.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright (C) Jupyter Development Team 4 | # 5 | # Distributed under the terms of the BSD License. The full license is in 6 | # the file COPYING, distributed as part of this software. 7 | # ----------------------------------------------------------------------------- 8 | import requests 9 | 10 | from ....tests.base import FormatHTMLMixin 11 | from ....tests.base import NBViewerTestCase 12 | from ....tests.base import skip_unless_github_auth 13 | 14 | 15 | class GistTestCase(NBViewerTestCase): 16 | @skip_unless_github_auth 17 | def test_gist(self): 18 | url = self.url("2352771") 19 | r = requests.get(url) 20 | self.assertEqual(r.status_code, 200) 21 | 22 | @skip_unless_github_auth 23 | def test_gist_not_nb(self): 24 | url = self.url("6689377") 25 | r = requests.get(url) 26 | self.assertEqual(r.status_code, 400) 27 | 28 | @skip_unless_github_auth 29 | def test_gist_no_such_file(self): 30 | url = self.url("6689377/no/file.ipynb") 31 | r = requests.get(url) 32 | self.assertEqual(r.status_code, 404) 33 | 34 | @skip_unless_github_auth 35 | def test_gist_list(self): 36 | url = self.url("7518294") 37 | r = requests.get(url) 38 | self.assertEqual(r.status_code, 200) 39 | html = r.text 40 | self.assertIn("Name", html) 41 | 42 | @skip_unless_github_auth 43 | def test_multifile_gist(self): 44 | url = self.url("7518294", "Untitled0.ipynb") 45 | r = requests.get(url) 46 | self.assertEqual(r.status_code, 200) 47 | html = r.text 48 | self.assertIn("Download Notebook", html) 49 | 50 | @skip_unless_github_auth 51 | def test_anonymous_gist(self): 52 | url = self.url("gist/4465051") 53 | r = requests.get(url) 54 | self.assertEqual(r.status_code, 200) 55 | html = r.text 56 | self.assertIn("Download Notebook", html) 57 | 58 | @skip_unless_github_auth 59 | def test_gist_unicode(self): 60 | url = self.url("gist/amueller/3974344") 61 | r = requests.get(url) 62 | self.assertEqual(r.status_code, 200) 63 | html = r.text 64 | self.assertIn("Name", html) 65 | 66 | @skip_unless_github_auth 67 | def test_gist_unicode_content(self): 68 | url = self.url("gist/ocefpaf/cf023a8db7097bd9fe92") 69 | r = requests.get(url) 70 | self.assertEqual(r.status_code, 200) 71 | html = r.text 72 | self.assertNotIn("paramétrica", html) 73 | self.assertIn("paramétrica", html) 74 | 75 | 76 | class FormatHTMLGistTestCase(GistTestCase, FormatHTMLMixin): 77 | pass 78 | -------------------------------------------------------------------------------- /helm-chart/nbviewer/templates/statuspage.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.statuspage.enabled -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ template "nbviewer.fullname" . }}-statuspage 6 | labels: 7 | component: statuspage 8 | {{- include "nbviewer.labels" . | nindent 4 }} 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | component: statuspage 14 | {{- include "nbviewer.matchLabels" . | nindent 6 }} 15 | template: 16 | metadata: 17 | labels: 18 | component: statuspage 19 | {{- include "nbviewer.matchLabels" . | nindent 8 }} 20 | annotations: 21 | # This lets us autorestart when the secret changes! 22 | checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} 23 | {{- if .Values.annotations }} 24 | {{- .Values.annotations | toYaml | trimSuffix "\n" | nindent 8 }} 25 | {{- end }} 26 | spec: 27 | nodeSelector: {{ toJson .Values.nodeSelector }} 28 | volumes: 29 | - name: secret 30 | secret: 31 | secretName: {{ template "nbviewer.fullname" . }} 32 | containers: 33 | - name: statuspage 34 | image: {{ .Values.statuspage.image }} 35 | 36 | resources: 37 | {{- .Values.statuspage.resources | toYaml | trimSuffix "\n" | nindent 12 }} 38 | {{- with .Values.imagePullPolicy }} 39 | imagePullPolicy: {{ . }} 40 | {{- end }} 41 | env: 42 | - name: PYTHONUNBUFFERED 43 | value: "1" 44 | {{- if .Values.github.clientId }} 45 | - name: GITHUB_OAUTH_KEY 46 | valueFrom: 47 | secretKeyRef: 48 | name: {{ template "nbviewer.fullname" . }} 49 | key: github-clientId 50 | {{- end }} 51 | {{- if .Values.github.clientSecret }} 52 | - name: GITHUB_OAUTH_SECRET 53 | valueFrom: 54 | secretKeyRef: 55 | name: {{ template "nbviewer.fullname" . }} 56 | key: github-clientSecret 57 | {{- end }} 58 | {{- if .Values.github.accessToken }} 59 | - name: GITHUB_API_TOKEN 60 | valueFrom: 61 | secretKeyRef: 62 | name: {{ template "nbviewer.fullname" . }} 63 | key: github-accessToken 64 | {{- end }} 65 | - name: STATUSPAGE_API_KEY 66 | valueFrom: 67 | secretKeyRef: 68 | name: {{ template "nbviewer.fullname" . }} 69 | key: statuspage-apiKey 70 | - name: STATUSPAGE_PAGE_ID 71 | value: {{ .Values.statuspage.pageId }} 72 | - name: STATUSPAGE_METRIC_ID 73 | value: {{ .Values.statuspage.metricId }} 74 | 75 | {{- end -}} 76 | -------------------------------------------------------------------------------- /nbviewer/frontpage.json: -------------------------------------------------------------------------------- 1 | {"title": "nbviewer", 2 | "subtitle": "A simple way to share Jupyter Notebooks", 3 | "text": "Enter the location of a Jupyter Notebook to have it rendered here:", 4 | "show_input": true, 5 | "sections":[ 6 | { 7 | "header":"Programming Languages", 8 | "links":[ 9 | { 10 | "text": "IPython", 11 | "target": "/github/ipython/ipython/blob/6.x/examples/IPython%20Kernel/Index.ipynb", 12 | "img": "/img/example-nb/ipython-thumb.png" 13 | }, 14 | { 15 | "text": "IRuby", 16 | "target": "/github/SciRuby/sciruby-notebooks/blob/master/getting_started.ipynb", 17 | "img": "/img/example-nb/iruby-nb.png" 18 | }, 19 | { 20 | "text": "IJulia", 21 | "target": "/url/github/binder-examples/demo-julia/blob/main/demo.ipynb", 22 | "img": "/img/example-nb/ijulia-preview.png" 23 | } 24 | ] 25 | }, 26 | { 27 | "header":"Books", 28 | "links":[ 29 | { 30 | "text": "Python for Signal Processing", 31 | "target": "/github/unpingco/Python-for-Signal-Processing/", 32 | "img": "/img/example-nb/python-signal.png" 33 | }, 34 | { 35 | "text": "Mining the Social Web", 36 | "target": "/github/mikhailklassen/Mining-the-Social-Web-3rd-Edition/tree/master/notebooks", 37 | "img": "/img/example-nb/mining-slice.png" 38 | }, 39 | { 40 | "text": "Probabilistic Programming", 41 | "target": "/github/CamDavidsonPilon/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers/blob/master/Chapter1_Introduction/Ch1_Introduction_PyMC3.ipynb", 42 | "img": "/img/example-nb/probabilistic-bayesian.png" 43 | } 44 | ] 45 | }, 46 | { 47 | "header":"Misc", 48 | "links":[ 49 | { 50 | "text": "XKCD Plot With Matplotlib", 51 | "target": "/url/jakevdp.github.io/downloads/notebooks/XKCD_plots.ipynb", 52 | "img": "/img/example-nb/XKCD-Matplotlib.png" 53 | }, 54 | { 55 | "text": "Python for Vision Research", 56 | "target": "/github/gestaltrevision/python_for_visres/blob/master/index.ipynb", 57 | "img": "/img/example-nb/python_for_visres.png" 58 | }, 59 | { 60 | "text": "Partial Differential Equations Solver", 61 | "target": "/github/waltherg/notebooks/blob/master/2013-12-03-Crank_Nicolson.ipynb", 62 | "img": "/img/example-nb/pde_solver_with_numpy.png" 63 | }, 64 | { 65 | "text": "Analysis of current events", 66 | "target": "/gist/darribas/4121857", 67 | "img": "/img/example-nb/gaza.png" 68 | }, 69 | { 70 | "text": "Jaynes-Cummings model", 71 | "target": "/github/jrjohansson/qutip-lectures/blob/master/Lecture-1-Jaynes-Cumming-model.ipynb", 72 | "img": "/img/example-nb/jaynes-cummings.png" 73 | } 74 | ] 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /nbviewer/providers/github/tests/test_handlers.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import os 3 | from unittest import TestCase 4 | 5 | from ....utils import transform_ipynb_uri 6 | from ..handlers import uri_rewrites 7 | 8 | uri_rewrite_list = uri_rewrites() 9 | 10 | 11 | class TestRewrite(TestCase): 12 | def assert_rewrite(self, uri, rewrite): 13 | new = transform_ipynb_uri(uri, uri_rewrite_list) 14 | self.assertEqual(new, rewrite) 15 | 16 | def assert_rewrite_ghe(self, uri, rewrite): 17 | os.environ["GITHUB_API_URL"] = "https://example.com/api/v3/" 18 | uri_rewrite_ghe_list = uri_rewrites() 19 | os.environ.pop("GITHUB_API_URL", None) 20 | new = transform_ipynb_uri(uri, uri_rewrite_ghe_list) 21 | self.assertEqual(new, rewrite) 22 | 23 | def test_githubusercontent(self): 24 | uri = "https://raw.githubusercontent.com/user/reopname/deadbeef/a mřížka.ipynb" 25 | rewrite = "/github/user/reopname/blob/deadbeef/a mřížka.ipynb" 26 | self.assert_rewrite(uri, rewrite) 27 | 28 | def test_blob(self): 29 | uri = "https://github.com/user/reopname/blob/deadbeef/a mřížka.ipynb" 30 | rewrite = "/github/user/reopname/blob/deadbeef/a mřížka.ipynb" 31 | self.assert_rewrite(uri, rewrite) 32 | 33 | def test_raw_uri(self): 34 | uri = "https://github.com/user/reopname/raw/deadbeef/a mřížka.ipynb" 35 | rewrite = "/github/user/reopname/blob/deadbeef/a mřížka.ipynb" 36 | self.assert_rewrite(uri, rewrite) 37 | 38 | def test_raw_subdomain(self): 39 | uri = "https://raw.github.com/user/reopname/deadbeef/a mřížka.ipynb" 40 | rewrite = "/github/user/reopname/blob/deadbeef/a mřížka.ipynb" 41 | self.assert_rewrite(uri, rewrite) 42 | 43 | def test_tree(self): 44 | uri = "https://github.com/user/reopname/tree/deadbeef/a mřížka.ipynb" 45 | rewrite = "/github/user/reopname/tree/deadbeef/a mřížka.ipynb" 46 | self.assert_rewrite(uri, rewrite) 47 | 48 | def test_userrepo(self): 49 | uri = "username/reponame" 50 | rewrite = "/github/username/reponame/tree/master/" 51 | self.assert_rewrite(uri, rewrite) 52 | 53 | def test_user(self): 54 | uri = "username" 55 | rewrite = "/github/username/" 56 | self.assert_rewrite(uri, rewrite) 57 | 58 | def test_ghe_blob(self): 59 | uri = "https://example.com/user/reopname/blob/deadbeef/a mřížka.ipynb" 60 | rewrite = "/github/user/reopname/blob/deadbeef/a mřížka.ipynb" 61 | self.assert_rewrite_ghe(uri, rewrite) 62 | 63 | def test_ghe_raw_uri(self): 64 | uri = "https://example.com/user/reopname/raw/deadbeef/a mřížka.ipynb" 65 | rewrite = "/github/user/reopname/blob/deadbeef/a mřížka.ipynb" 66 | self.assert_rewrite_ghe(uri, rewrite) 67 | 68 | def test_ghe_tree(self): 69 | uri = "https://example.com/user/reopname/tree/deadbeef/a mřížka.ipynb" 70 | rewrite = "/github/user/reopname/tree/deadbeef/a mřížka.ipynb" 71 | self.assert_rewrite_ghe(uri, rewrite) 72 | -------------------------------------------------------------------------------- /nbviewer/tests/test_security.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # ----------------------------------------------------------------------------- 3 | # Copyright (C) Jupyter Development Team 4 | # 5 | # Distributed under the terms of the BSD License. The full license is in 6 | # the file COPYING, distributed as part of this software. 7 | # ----------------------------------------------------------------------------- 8 | import pytest 9 | import requests 10 | 11 | from ..providers.local.tests.test_localfile import ( 12 | LocalFileRelativePathTestCase as LFRPTC, 13 | ) 14 | from .base import NBViewerTestCase 15 | from .base import skip_unless_github_auth 16 | 17 | 18 | class XSSTestCase(NBViewerTestCase): 19 | def _xss(self, path, pattern=" instead of escaping it to %2f. 31 | self._xss("/github/bburky/xss/tree/%3Cscript%3Ealert(1)%3C%2fscript%3E/") 32 | 33 | @skip_unless_github_auth 34 | def test_gist_filenames(self): 35 | self._xss("/gist/bburky/c020825874798a6544a7") 36 | 37 | 38 | class LocalDirectoryTraversalTestCase(LFRPTC): 39 | def test_url(self): 40 | ## assumes being run from base of this repo 41 | url = self.url("localfile/../README.md") 42 | r = requests.get(url) 43 | self.assertEqual(r.status_code, 404) 44 | 45 | 46 | class URLLeakTestCase(NBViewerTestCase): 47 | @skip_unless_github_auth 48 | def test_gist(self): 49 | url = self.url("/github/jupyter") 50 | r = requests.get(url) 51 | self.assertEqual(r.status_code, 200) 52 | html = r.content 53 | self.assertNotIn("client_id", html) 54 | self.assertNotIn("client_secret", html) 55 | self.assertNotIn("access_token", html) 56 | 57 | 58 | class JupyterHubServiceTestCase(NBViewerTestCase): 59 | HUB_SETTINGS = { 60 | "JUPYTERHUB_SERVICE_NAME": "nbviewer-test", 61 | "JUPYTERHUB_API_TOKEN": "test-token", 62 | "JUPYTERHUB_API_URL": "http://127.0.0.1:8080/hub/api", 63 | "JUPYTERHUB_BASE_URL": "/", 64 | "JUPYTERHUB_SERVICE_URL": "http://127.0.0.1:%d" % NBViewerTestCase.port, 65 | "JUPYTERHUB_SERVICE_PREFIX": "/services/nbviewer-test", 66 | } 67 | 68 | environment_variables = HUB_SETTINGS 69 | 70 | @classmethod 71 | def get_server_cmd(cls): 72 | return super().get_server_cmd() + ["--localfiles=."] 73 | 74 | def test_login_redirect(self): 75 | url = self.url("/services/nbviewer-test/github/jupyter") 76 | r = requests.get(url, allow_redirects=False) 77 | self.assertEqual(r.status_code, 302) 78 | self.assertEqual( 79 | r.headers["location"], 80 | "/hub/login?next=%2Fservices%2Fnbviewer-test%2Fgithub%2Fjupyter", 81 | ) 82 | 83 | url = self.url("services/nbviewer-test/localfile/nbviewer/tests/notebook.ipynb") 84 | r = requests.get(url, allow_redirects=False) 85 | self.assertEqual(r.status_code, 302) 86 | self.assertEqual( 87 | r.headers["location"], 88 | "/hub/login?next=%2Fservices%2Fnbviewer-test%2Flocalfile%2Fnbviewer%2Ftests%2Fnotebook.ipynb", 89 | ) 90 | -------------------------------------------------------------------------------- /nbviewer/providers/url/handlers.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | from urllib import robotparser 8 | from urllib.parse import urlparse 9 | 10 | from tornado import httpclient 11 | from tornado import web 12 | from tornado.escape import url_unescape 13 | 14 | from .. import _load_handler_from_location 15 | from ...utils import quote 16 | from ...utils import response_text 17 | from ..base import cached 18 | from ..base import RenderingHandler 19 | 20 | 21 | class URLHandler(RenderingHandler): 22 | """Renderer for /url or /urls""" 23 | 24 | async def get_notebook_data(self, secure, netloc, url): 25 | proto = "http" + secure 26 | netloc = url_unescape(netloc) 27 | 28 | if "/?" in url: 29 | url, query = url.rsplit("/?", 1) 30 | else: 31 | query = None 32 | 33 | remote_url = "{}://{}/{}".format(proto, netloc, quote(url)) 34 | 35 | if query: 36 | remote_url = remote_url + "?" + query 37 | if not url.endswith(".ipynb"): 38 | # this is how we handle relative links (files/ URLs) in notebooks 39 | # if it's not a .ipynb URL and it is a link from a notebook, 40 | # redirect to the original URL rather than trying to render it as a notebook 41 | refer_url = self.request.headers.get("Referer", "").split("://")[-1] 42 | if refer_url.startswith(self.request.host + "/url"): 43 | self.redirect(remote_url) 44 | return 45 | 46 | parse_result = urlparse(remote_url) 47 | 48 | robots_url = parse_result.scheme + "://" + parse_result.netloc + "/robots.txt" 49 | 50 | public = False # Assume non-public 51 | 52 | try: 53 | robots_response = await self.fetch(robots_url) 54 | robotstxt = response_text(robots_response) 55 | rfp = robotparser.RobotFileParser() 56 | rfp.set_url(robots_url) 57 | rfp.parse(robotstxt.splitlines()) 58 | public = rfp.can_fetch("*", remote_url) 59 | except httpclient.HTTPError: 60 | self.log.debug( 61 | "Robots.txt not available for {}".format(remote_url), exc_info=True 62 | ) 63 | public = True 64 | except Exception as e: 65 | self.log.error(e) 66 | 67 | return remote_url, public 68 | 69 | async def deliver_notebook(self, remote_url, public): 70 | response = await self.fetch(remote_url) 71 | 72 | try: 73 | nbjson = response_text(response, encoding="utf-8") 74 | except UnicodeDecodeError: 75 | self.log.error("Notebook is not utf8: %s", remote_url, exc_info=True) 76 | raise web.HTTPError(400) 77 | 78 | await self.finish_notebook( 79 | nbjson, 80 | download_url=remote_url, 81 | msg="file from url: %s" % remote_url, 82 | public=public, 83 | request=self.request, 84 | ) 85 | 86 | @cached 87 | async def get(self, secure, netloc, url): 88 | remote_url, public = await self.get_notebook_data(secure, netloc, url) 89 | 90 | await self.deliver_notebook(remote_url, public) 91 | 92 | 93 | def default_handlers(handlers=[], **handler_names): 94 | """Tornado handlers""" 95 | 96 | url_handler = _load_handler_from_location(handler_names["url_handler"]) 97 | 98 | return handlers + [ 99 | (r"/url(?P[s]?)/(?P[^/]+)/(?P.*)", url_handler, {}) 100 | ] 101 | 102 | 103 | def uri_rewrites(rewrites=[]): 104 | return rewrites + [("^http(s?)://(.*)$", "/url{0}/{1}"), ("^(.*)$", "/url/{0}")] 105 | -------------------------------------------------------------------------------- /nbviewer/static/css/theme/css_linalg.css: -------------------------------------------------------------------------------- 1 | div.cell { 2 | width: inherit ; 3 | background-color: #f3f3f3 ; 4 | } 5 | 6 | .container { 7 | max-width:50em; 8 | } 9 | 10 | /* block fixes margin on input boxes */ 11 | /* in firefox */ 12 | .input.hbox { 13 | max-width: 50em; 14 | } 15 | 16 | .input_area { 17 | background-color: white ; 18 | } 19 | 20 | .output_area pre { 21 | font-family: "Source Code Pro", source-code-pro, Consolas, monospace; 22 | border: 0px; 23 | } 24 | 25 | div.output_text { 26 | font-family: "Source Code Pro", source-code-pro, Consolas, monospace; 27 | } 28 | 29 | div.text_cell { 30 | max-width: 35em; 31 | text-align: left; 32 | } 33 | 34 | div.prompt { 35 | width: 0px; 36 | visibility: hidden ; 37 | } 38 | 39 | .code_cell { 40 | background-color: #f3f3f3; 41 | } 42 | 43 | .highlight { 44 | background-color: #ffffff; 45 | } 46 | 47 | div.input_prompt { 48 | visibility: hidden; 49 | width: 0 ; 50 | } 51 | 52 | div.text_cell_render { 53 | font-family: "Minion Pro", "minion-pro", "Charis SIL", Palatino, serif ; 54 | font-size: 14pt ; 55 | line-height: 145% ; 56 | max-width: 35em ; 57 | text-align: left ; 58 | background-color: #f3f3f3 ; 59 | } 60 | 61 | div.text_cell_render h1 { 62 | display: block; 63 | font-size: 28pt; 64 | color: #3B3B3B; 65 | margin-bottom: 0em; 66 | margin-top: 0.5em; 67 | 68 | } 69 | 70 | .rendered_html li { 71 | margin-bottom: .25em; 72 | color: #3B3B3B;; 73 | } 74 | 75 | div.text_cell_render h2:before { 76 | content: "\2FFA"; 77 | margin-right: 0.5em; 78 | font-size: .5em; 79 | vertical-align: baseline; 80 | border-top: 1px; 81 | 82 | } 83 | 84 | .hiterm { 85 | font-weight: 500; 86 | color: #DC143C; 87 | } 88 | 89 | .text_cell_render h2 { 90 | font-size: 20pt; 91 | margin-bottom: 0em; 92 | margin-top: 0.5em; 93 | display: block; 94 | color: #3B3B3B; 95 | } 96 | 97 | .MathJax_Display { 98 | /*text-align: center ;*/ 99 | margin-left: 2em ; 100 | margin-top: .5em ; 101 | margin-bottom: .5em ; 102 | } 103 | 104 | .text_cell_render h3 { 105 | font-size: 14pt; 106 | font-weight: 600; 107 | font-style: italic; 108 | margin-bottom: -0.5em; 109 | margin-top: -0.25em; 110 | color: #3B3B3B; 111 | text-indent: 2em; 112 | } 113 | 114 | .text_cell_render h5 { 115 | font-weight: 300; 116 | font-size: 14pt; 117 | color: #4057A1; 118 | font-style: italic; 119 | margin-bottom: .5em; 120 | margin-top: 0.5em; 121 | display: block; 122 | } 123 | 124 | .CodeMirror { 125 | font-family: "Source Code Pro", source-code-pro, Consolas, monospace ; 126 | font-size: 10pt; 127 | background: #fffffe; /* #f0f8fb #e2eef9*/ 128 | border: 0px; 129 | } 130 | 131 | .rendered_html { 132 | 133 | } 134 | 135 | .rendered_html code { 136 | font-family: "Source Code Pro", source-code-pro,Consolas, monospace; 137 | font-size: 85%; 138 | } 139 | 140 | pre, code, kbd, samp { font-family: "Source Code Pro", source-code-pro, Consola, monospace; } 141 | 142 | .rendered_html p { 143 | text-align: left; 144 | color: #3B3B3B; 145 | margin-bottom: .5em; 146 | 147 | } 148 | 149 | .rendered_html p+p { 150 | text-indent: 1em; 151 | margin-top: 0; 152 | } 153 | 154 | .rendered_html ol { 155 | list-style: decimal; 156 | /*margin: 1em 2em;*/ 157 | } 158 | 159 | .rendered_html ol ol { 160 | list-style: decimal; 161 | } 162 | 163 | .rendered_html ol ol ol { 164 | list-style: decimal; 165 | } 166 | 167 | body{background-color:#f3f3f3;} 168 | 169 | .rendered_html p.hangpar { 170 | text-indent: 0; 171 | } 172 | 173 | 174 | -------------------------------------------------------------------------------- /nbviewer/providers/github/tests/test_client.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import unittest.mock as mock 3 | 4 | from tornado.httpclient import AsyncHTTPClient 5 | from tornado.log import app_log 6 | from tornado.testing import AsyncTestCase 7 | 8 | from ....utils import quote 9 | from ..client import AsyncGitHubClient 10 | 11 | 12 | class GithubClientTest(AsyncTestCase): 13 | """Tests that the github API client makes the correct http requests.""" 14 | 15 | def setUp(self): 16 | super().setUp() 17 | # Need a mock HTTPClient for the github client to talk to. 18 | self.http_client = mock.create_autospec(AsyncHTTPClient) 19 | 20 | # patch the enviornment so that we get a known url prefix. 21 | with mock.patch("os.environ.get", return_value="https://api.github.com/"): 22 | self.gh_client = AsyncGitHubClient(log=app_log, client=self.http_client) 23 | 24 | def _get_url(self): 25 | """Get the last url requested from the mock http client.""" 26 | args, kw = self.http_client.fetch.call_args 27 | return args[0] 28 | 29 | def assertStartsWith(self, string, beginning): 30 | """Assert that a url has the correct beginning. 31 | 32 | Github API requests involve non-trivial query strings. This is useful 33 | when you want to compare URLs, but don't care about the querystring. 34 | """ 35 | if string.startswith(beginning): 36 | return 37 | self.assertTrue( 38 | string.startswith(beginning), 39 | "%s does not start with %s" % (string, beginning), 40 | ) 41 | 42 | def test_basic_fetch(self): 43 | """Test the mock http client is hit""" 44 | self.gh_client.fetch("https://api.github.com/url") 45 | self.assertTrue(self.http_client.fetch.called) 46 | 47 | def test_fetch_params(self): 48 | """Test params are passed through.""" 49 | params = {"unique_param_name": 1} 50 | self.gh_client.fetch("https://api.github.com/url", params=params) 51 | url = self._get_url() 52 | self.assertTrue("unique_param_name" in url) 53 | 54 | def test_log_rate_limit(self): 55 | pass 56 | 57 | def test_get_repos(self): 58 | self.gh_client.get_repos("username") 59 | url = self._get_url() 60 | self.assertStartsWith(url, "https://api.github.com/users/username/repos") 61 | 62 | def test_get_contents(self): 63 | user = "username" 64 | repo = "my_awesome_repo" 65 | path = "möre-path" 66 | self.gh_client.get_contents(user, repo, path) 67 | url = self._get_url() 68 | correct_url = "https://api.github.com" + quote( 69 | "/repos/username/my_awesome_repo/contents/möre-path" 70 | ) 71 | self.assertStartsWith(url, correct_url) 72 | 73 | def test_get_branches(self): 74 | user = "username" 75 | repo = "my_awesome_repo" 76 | self.gh_client.get_branches(user, repo) 77 | url = self._get_url() 78 | correct_url = "https://api.github.com/repos/username/my_awesome_repo/branches" 79 | self.assertStartsWith(url, correct_url) 80 | 81 | def test_get_tags(self): 82 | user = "username" 83 | repo = "my_awesome_repo" 84 | self.gh_client.get_tags(user, repo) 85 | url = self._get_url() 86 | correct_url = "https://api.github.com/repos/username/my_awesome_repo/tags" 87 | self.assertStartsWith(url, correct_url) 88 | 89 | def test_get_tree(self): 90 | user = "username" 91 | repo = "my_awesome_repo" 92 | path = "extra-path" 93 | self.gh_client.get_tree(user, repo, path) 94 | url = self._get_url() 95 | correct_url = ( 96 | "https://api.github.com/repos/username/my_awesome_repo/git/trees/master" 97 | ) 98 | self.assertStartsWith(url, correct_url) 99 | 100 | def test_get_gist(self): 101 | gist_id = "ap90avn23iovv2ovn2309n" 102 | self.gh_client.get_gist(gist_id) 103 | url = self._get_url() 104 | correct_url = "https://api.github.com/gists/" + gist_id 105 | self.assertStartsWith(url, correct_url) 106 | 107 | def test_get_gists(self): 108 | user = "username" 109 | self.gh_client.get_gists(user) 110 | url = self._get_url() 111 | correct_url = "https://api.github.com/users/username/gists" 112 | self.assertStartsWith(url, correct_url) 113 | -------------------------------------------------------------------------------- /nbviewer/tests/test_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from subprocess import PIPE 4 | from subprocess import Popen 5 | from tempfile import NamedTemporaryFile 6 | 7 | 8 | # Also copied mostly from JupyterHub since again -- if not broken, don't fix. 9 | def test_generate_config(): 10 | with NamedTemporaryFile(prefix="nbviewer_config", suffix=".py") as tf: 11 | cfg_file = tf.name 12 | with open(cfg_file, "w") as f: 13 | f.write("c.A = 5") 14 | p = Popen( 15 | [ 16 | sys.executable, 17 | "-m", 18 | "nbviewer", 19 | "--generate-config", 20 | "--config-file={}".format(cfg_file), 21 | ], 22 | stdout=PIPE, 23 | stdin=PIPE, 24 | ) 25 | out, _ = p.communicate(b"n") 26 | out = out.decode("utf8", "replace") 27 | assert os.path.exists(cfg_file) 28 | with open(cfg_file) as f: 29 | cfg_text = f.read() 30 | assert cfg_text == "c.A = 5" 31 | 32 | p = Popen( 33 | [ 34 | sys.executable, 35 | "-m", 36 | "nbviewer", 37 | "--generate-config", 38 | "--config-file={}".format(cfg_file), 39 | ], 40 | stdout=PIPE, 41 | stdin=PIPE, 42 | ) 43 | out, _ = p.communicate(b"x\ny") 44 | out = out.decode("utf8", "replace") 45 | assert os.path.exists(cfg_file) 46 | with open(cfg_file) as f: 47 | cfg_text = f.read() 48 | os.remove(cfg_file) 49 | assert cfg_file in out 50 | assert "NBViewer.name" not in cfg_text # This shouldn't be configurable 51 | assert "NBViewer.answer_yes" in cfg_text 52 | assert "NBViewer.base_url" in cfg_text 53 | assert "NBViewer._base_url" not in cfg_text # This shouldn't be configurable 54 | assert "NBViewer.binder_base_url" in cfg_text 55 | assert "NBViewer.cache_expiry_max" in cfg_text 56 | assert "NBViewer.cache_expiry_min" in cfg_text 57 | assert "NBViewer.client" in cfg_text 58 | assert "NBViewer.config_file" in cfg_text 59 | assert "NBViewer.content_security_policy" in cfg_text 60 | assert "NBViewer.default_format" in cfg_text 61 | assert "NBViewer.frontpage" in cfg_text 62 | assert "NBViewer.generate_config" in cfg_text 63 | assert "NBViewer.host" in cfg_text 64 | assert "NBViewer.index" in cfg_text 65 | assert "NBViewer.ipywidgets_base_url" in cfg_text 66 | assert "NBViewer.jupyter_js_widgets_version" in cfg_text 67 | assert "NBViewer.jupyter_widgets_html_manager_version" in cfg_text 68 | assert "NBViewer.localfile_any_user" in cfg_text 69 | assert "NBViewer.local_handler" in cfg_text 70 | assert "NBViewer.localfile_follow_symlinks" in cfg_text 71 | assert "NBViewer.localfiles" in cfg_text 72 | assert "NBViewer.mathjax_url" in cfg_text 73 | assert "NBViewer.max_cache_uris" in cfg_text 74 | assert "NBViewer.mc_threads" in cfg_text 75 | assert "NBViewer.no_cache" in cfg_text 76 | assert "NBViewer.no_check_certificate" in cfg_text 77 | assert "NBViewer.port" in cfg_text 78 | assert "NBViewer.processes" in cfg_text 79 | assert "NBViewer.providers" in cfg_text 80 | assert "NBViewer.provider_rewrites" in cfg_text 81 | assert "NBViewer.proxy_host" in cfg_text 82 | assert "NBViewer.proxy_port" in cfg_text 83 | assert "NBViewer.rate_limit" in cfg_text 84 | assert "NBViewer.rate_limit_interval" in cfg_text 85 | assert "NBViewer.render_timeout" in cfg_text 86 | assert "NBViewer.sslcert" in cfg_text 87 | assert "NBViewer.sslkey" in cfg_text 88 | assert "NBViewer.static_path" in cfg_text 89 | assert "NBViewer.static_url_prefix" in cfg_text 90 | assert ( 91 | "NBViewer._static_url_prefix" not in cfg_text 92 | ) # This shouldn't be configurable 93 | assert "NBViewer.statsd_host" in cfg_text 94 | assert "NBViewer.statsd_port" in cfg_text 95 | assert "NBViewer.statsd_prefix" in cfg_text 96 | assert "NBViewer.template_path" in cfg_text 97 | assert ( 98 | "NBViewer.default_endpoint" not in cfg_text 99 | ) # Shouldn't be configurable, is a property 100 | assert "NBViewer.env" not in cfg_text # Shouldn't be configurable, is a property 101 | assert "NBViewer.fetch_kwargs" not in cfg_text 102 | assert "NBViewer.formats" not in cfg_text 103 | assert "NBViewer.frontpage_setup" not in cfg_text 104 | assert "NBViewer.pool" not in cfg_text 105 | assert "NBViewer.rate_limiter" not in cfg_text 106 | assert "NBViewer.static_paths" not in cfg_text 107 | assert "NBViewer.template_paths" not in cfg_text 108 | -------------------------------------------------------------------------------- /nbviewer/tests/base.py: -------------------------------------------------------------------------------- 1 | """Base class for nbviewer tests. 2 | 3 | Derived from IPython.html notebook test case in 2.0 4 | """ 5 | 6 | # ----------------------------------------------------------------------------- 7 | # Copyright (C) Jupyter Development Team 8 | # 9 | # Distributed under the terms of the BSD License. The full license is in 10 | # the file COPYING, distributed as part of this software. 11 | # ----------------------------------------------------------------------------- 12 | import os 13 | import sys 14 | import time 15 | from contextlib import contextmanager 16 | from subprocess import Popen 17 | from typing import Dict 18 | from unittest import skipIf 19 | from unittest import TestCase 20 | 21 | import requests 22 | from tornado.escape import to_unicode 23 | from tornado.log import app_log 24 | 25 | from nbviewer.providers.github.client import AsyncGitHubClient 26 | from nbviewer.utils import url_path_join 27 | 28 | 29 | class NBViewerTestCase(TestCase): 30 | """A base class for tests that need a running nbviewer server.""" 31 | 32 | port = 12341 33 | 34 | environment_variables: Dict[str, str] = {} 35 | 36 | def assertIn(self, observed, expected, *args, **kwargs): 37 | return super().assertIn( 38 | to_unicode(observed), to_unicode(expected), *args, **kwargs 39 | ) 40 | 41 | def assertNotIn(self, observed, expected, *args, **kwargs): 42 | return super().assertNotIn( 43 | to_unicode(observed), to_unicode(expected), *args, **kwargs 44 | ) 45 | 46 | @classmethod 47 | def wait_until_alive(cls): 48 | """Wait for the server to be alive""" 49 | while True: 50 | try: 51 | requests.get(cls.url()) 52 | except Exception: 53 | time.sleep(0.1) 54 | else: 55 | break 56 | 57 | @classmethod 58 | def wait_until_dead(cls): 59 | """Wait for the server to stop getting requests after shutdown""" 60 | while True: 61 | try: 62 | requests.get(cls.url()) 63 | except Exception: 64 | break 65 | else: 66 | time.sleep(0.1) 67 | 68 | @classmethod 69 | def get_server_cmd(cls): 70 | return [sys.executable, "-m", "nbviewer", "--port=%d" % cls.port] 71 | 72 | @classmethod 73 | def setup_class(cls): 74 | server_cmd = cls.get_server_cmd() 75 | cls.server = Popen( 76 | server_cmd, 77 | # Set environment variables if any 78 | env=dict(os.environ, **cls.environment_variables), 79 | ) 80 | cls.wait_until_alive() 81 | 82 | @classmethod 83 | def teardown_class(cls): 84 | cls.server.terminate() 85 | cls.wait_until_dead() 86 | 87 | @classmethod 88 | def url(cls, *parts): 89 | return url_path_join("http://localhost:%i" % cls.port, *parts) 90 | 91 | 92 | class FormatMixin(object): 93 | @classmethod 94 | def url(cls, *parts): 95 | return url_path_join( 96 | "http://localhost:%i" % cls.port, "format", cls.key, *parts 97 | ) 98 | 99 | 100 | class FormatHTMLMixin(object): 101 | key = "html" 102 | 103 | 104 | class FormatSlidesMixin(object): 105 | key = "slides" 106 | 107 | 108 | @contextmanager 109 | def assert_http_error(status, msg=None): 110 | try: 111 | yield 112 | except requests.HTTPError as e: 113 | real_status = e.response.status_code 114 | assert real_status == status, "Expected status %d, got %d" % ( 115 | real_status, 116 | status, 117 | ) 118 | if msg: 119 | assert msg in str(e), e 120 | else: 121 | assert False, "Expected HTTP error status" 122 | 123 | 124 | def skip_unless_github_auth(f): 125 | """Decorates a function to skip a test unless credentials are available for 126 | AsyhncGitHubClient to authenticate. 127 | 128 | Avoids noisy test failures on PRs due to GitHub API rate limiting with a 129 | valid token that might obscure test failures that are actually meaningful. 130 | 131 | Paraameters 132 | ----------- 133 | f: callable 134 | test function to decorate 135 | 136 | Returns 137 | ------- 138 | callable 139 | unittest.skipIf decorated function 140 | """ 141 | cl = AsyncGitHubClient(log=app_log) 142 | can_auth = "access_token" in cl.auth or ( 143 | "client_id" in cl.auth and "client_secret" in cl.auth 144 | ) 145 | return skipIf(not can_auth, "github creds not available")(f) 146 | -------------------------------------------------------------------------------- /nbviewer/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # ----------------------------------------------------------------------------- 3 | # Copyright (C) Jupyter Development Team 4 | # 5 | # Distributed under the terms of the BSD License. The full license is in 6 | # the file COPYING, distributed as part of this software. 7 | # ----------------------------------------------------------------------------- 8 | from nbviewer import utils 9 | from nbviewer.providers import default_rewrites 10 | from nbviewer.providers import provider_uri_rewrites 11 | 12 | 13 | def test_transform_ipynb_uri(): 14 | test_data = ( 15 | # GIST_RGX 16 | ("1234", "/1234"), 17 | ("1234/", "/1234"), 18 | # GIST_URL_RGX 19 | ("https://gist.github.com/user-name/1234", "/1234"), 20 | ("https://gist.github.com/user-name/1234/", "/1234"), 21 | # GITHUB_URL_RGX 22 | ( 23 | "https://github.com/user-name_/repo-name_/blob/master/path/file.ipynb", 24 | "/github/user-name_/repo-name_/blob/master/path/file.ipynb", 25 | ), 26 | ( 27 | "http://github.com/user-name_/repo-name_/blob/master/path/file.ipynb", 28 | "/github/user-name_/repo-name_/blob/master/path/file.ipynb", 29 | ), 30 | ( 31 | "https://github.com/user-name_/repo-name_/tree/master/path/", 32 | "/github/user-name_/repo-name_/tree/master/path/", 33 | ), 34 | # GITHUB_USER_RGX 35 | ("ipy-thon", "/github/ipy-thon/"), 36 | # GITHUB_USER_REPO_RGX 37 | ("ipy-thon/ipy-thon", "/github/ipy-thon/ipy-thon/tree/master/"), 38 | # DropBox Urls 39 | ("http://www.dropbox.com/s/bar/baz.qux", "/url/dl.dropbox.com/s/bar/baz.qux"), 40 | ( 41 | "https://www.dropbox.com/s/zip/baz.qux", 42 | "/urls/dl.dropbox.com/s/zip/baz.qux", 43 | ), 44 | ( 45 | "https://www.dropbox.com/sh/mhviow274da2wly/CZKwRRcA0k/nested/furthernested/User%2520Interface.ipynb?dl=1", 46 | "/urls/dl.dropbox.com/sh/mhviow274da2wly/CZKwRRcA0k/nested/furthernested/User%2520Interface.ipynb", 47 | ), 48 | # HuggingFace urls 49 | ( 50 | "https://huggingface.co/pceiyos/fake_news_detection_nlp/blob/main/Fake_News_Classificaton.ipynb", 51 | "/urls/huggingface.co/pceiyos/fake_news_detection_nlp/resolve/main/Fake_News_Classificaton.ipynb", 52 | ), 53 | ( 54 | "https://huggingface.co/spaces/NimaBoscarino/climategan/blob/main/notebooks/plot_metrics.ipynb", 55 | "/urls/huggingface.co/spaces/NimaBoscarino/climategan/resolve/main/notebooks/plot_metrics.ipynb", 56 | # This ClimateGAN notebook is served over LFS (as the file is 17.1 MB) 57 | ), 58 | ( 59 | "https://huggingface.co/spaces/dalle-mini/dalle-mini/blob/63679e968109278c5f0169100b1755bbda9f4bc6/tools/inference/inference_pipeline.ipynb", 60 | "/urls/huggingface.co/spaces/dalle-mini/dalle-mini/resolve/63679e968109278c5f0169100b1755bbda9f4bc6/tools/inference/inference_pipeline.ipynb", 61 | # This Dall-e mini notebook is hosted from a specific revision (= git commit) 62 | ), 63 | # URL 64 | ("https://example.org/ipynb", "/urls/example.org/ipynb"), 65 | ("http://example.org/ipynb", "/url/example.org/ipynb"), 66 | ("example.org/ipynb", "/url/example.org/ipynb"), 67 | ("example.org/ipynb", "/url/example.org/ipynb"), 68 | ( 69 | "https://gist.github.com/user/1234/raw/a1b2c3/file.ipynb", 70 | "/urls/gist.github.com/user/1234/raw/a1b2c3/file.ipynb", 71 | ), 72 | ( 73 | "https://gist.github.com/user/1234/raw/a1b2c3/file.ipynb?query=string&is=1", 74 | "/urls/gist.github.com/user/1234/raw/a1b2c3/file.ipynb/%3Fquery%3Dstring%26is%3D1", 75 | ), 76 | ) 77 | uri_rewrite_list = provider_uri_rewrites(default_rewrites) 78 | for ipynb_uri, expected_output in test_data: 79 | output = utils.transform_ipynb_uri(ipynb_uri, uri_rewrite_list) 80 | assert output == expected_output, "%s => %s != %s" % ( 81 | ipynb_uri, 82 | output, 83 | expected_output, 84 | ) 85 | 86 | 87 | def test_quote(): 88 | tests = [ 89 | ("hi", "hi"), 90 | ("hi", "hi"), 91 | ("hi", "hi"), 92 | (" /#", "%20/%23"), 93 | (" /#", "%20/%23"), 94 | (" /#", "%20/%23"), 95 | ("ü /é#/", "%C3%BC%20/%C3%A9%23/"), 96 | ("ü /é#/", "%C3%BC%20/%C3%A9%23/"), 97 | ("ü /é#/", "%C3%BC%20/%C3%A9%23/"), 98 | ] 99 | for s, expected in tests: 100 | quoted = utils.quote(s) 101 | assert quoted == expected 102 | assert type(quoted) == type(expected) 103 | -------------------------------------------------------------------------------- /nbviewer/client.py: -------------------------------------------------------------------------------- 1 | """Async HTTP client with bonus features! 2 | 3 | - Support caching via upstream 304 with ETag, Last-Modified 4 | - Log request timings for profiling 5 | """ 6 | 7 | # Copyright (c) Jupyter Development Team. 8 | # Distributed under the terms of the Modified BSD License. 9 | import asyncio 10 | import hashlib 11 | import pickle 12 | import time 13 | 14 | from tornado.curl_httpclient import CurlAsyncHTTPClient 15 | from tornado.httpclient import HTTPRequest 16 | 17 | from nbviewer.utils import time_block 18 | 19 | # ----------------------------------------------------------------------------- 20 | # Async HTTP Client 21 | # ----------------------------------------------------------------------------- 22 | 23 | # cache headers and their response:request mapping 24 | # use this to map headers in cached response to the headers 25 | # that should be set in the request. 26 | 27 | cache_headers = {"ETag": "If-None-Match", "Last-Modified": "If-Modified-Since"} 28 | 29 | 30 | class NBViewerAsyncHTTPClient(object): 31 | """Subclass of AsyncHTTPClient with bonus logging and caching! 32 | 33 | If upstream servers support 304 cache replies with the following headers: 34 | 35 | - ETag : If-None-Match 36 | - Last-Modified : If-Modified-Since 37 | 38 | Upstream requests are still made every time, 39 | but resources and rate limits may be saved by 304 responses. 40 | 41 | If upstream responds with 304 or an error and a cached response is available, 42 | use the cached response. 43 | 44 | Responses are cached as long as possible. 45 | """ 46 | 47 | cache = None 48 | 49 | def __init__(self, log, client=None): 50 | self.log = log 51 | self.client = client or CurlAsyncHTTPClient() 52 | 53 | def fetch(self, url, params=None, **kwargs): 54 | request = HTTPRequest(url, **kwargs) 55 | 56 | if request.user_agent is None: 57 | request.user_agent = "Tornado-Async-Client" 58 | 59 | # The future which will become the response upon awaiting. 60 | response_future = asyncio.ensure_future(self.smart_fetch(request)) 61 | 62 | return response_future 63 | 64 | async def smart_fetch(self, request): 65 | """ 66 | Before fetching request, first look to see whether it's already in cache. 67 | If so load the response from cache. Only otherwise attempt to fetch the request. 68 | When response code isn't 304 or 400, cache response before loading, else just load. 69 | """ 70 | tic = time.time() 71 | 72 | # when logging, use the URL without params 73 | name = request.url.split("?")[0] 74 | self.log.debug("Fetching %s", name) 75 | 76 | # look for a cached response 77 | cached_response = None 78 | cache_key = hashlib.sha256(request.url.encode("utf8")).hexdigest() 79 | cached_response = await self._get_cached_response(cache_key, name) 80 | toc = time.time() 81 | self.log.info("Upstream cache get %s %.2f ms", name, 1e3 * (toc - tic)) 82 | 83 | if cached_response: 84 | self.log.info("Upstream cache hit %s", name) 85 | # add cache headers, if any 86 | for resp_key, req_key in cache_headers.items(): 87 | value = cached_response.headers.get(resp_key) 88 | if value: 89 | request.headers[req_key] = value 90 | return cached_response 91 | else: 92 | self.log.info("Upstream cache miss %s", name) 93 | 94 | response = await self.client.fetch(request) 95 | dt = time.time() - tic 96 | self.log.info("Fetched %s in %.2f ms", name, 1e3 * dt) 97 | await self._cache_response(cache_key, name, response) 98 | return response 99 | 100 | async def _get_cached_response(self, cache_key, name): 101 | """Get the cached response, if any""" 102 | if not self.cache: 103 | return 104 | try: 105 | cached_pickle = await self.cache.get(cache_key) 106 | if cached_pickle: 107 | self.log.info("Type of self.cache is: %s", type(self.cache)) 108 | return pickle.loads(cached_pickle) 109 | except Exception: 110 | self.log.error("Upstream cache get failed %s", name, exc_info=True) 111 | 112 | async def _cache_response(self, cache_key, name, response): 113 | """Cache the response, if any cache headers we understand are present.""" 114 | if not self.cache: 115 | return 116 | with time_block("Upstream cache set %s" % name, logger=self.log): 117 | # cache the response 118 | try: 119 | pickle_response = pickle.dumps(response, pickle.HIGHEST_PROTOCOL) 120 | await self.cache.set(cache_key, pickle_response) 121 | except Exception: 122 | self.log.error("Upstream cache failed %s" % name, exc_info=True) 123 | -------------------------------------------------------------------------------- /nbviewer/templates/notebook.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% import "layout.html" as layout with context %} 4 | 5 | 6 | {% block otherlinks %} 7 | {% for fmt_name, fmt in formats.items() %} 8 | {% if format != fmt_name %} 9 | {% if fmt_name == default_format %} 10 | {{ layout.head_icon(from_base(format_base), "View as " ~ fmt.label, fmt.icon) }} 11 | {% else %} 12 | {{ layout.head_icon(from_base(format_prefix, fmt_name, format_base), "View as " ~ fmt.label, fmt.icon) }} 13 | {% endif %} 14 | {% endif %} 15 | {% endfor %} 16 | 17 | {% if "kernelspec" in nb.metadata %} 18 | {{ layout.head_icon("#", nb.metadata.kernelspec.display_name + " Kernel", "server") }} 19 | {% endif %} 20 | 21 | {% if provider_url %} 22 | {{ layout.head_icon(provider_url, "View on " + provider_label, provider_icon) }} 23 | {% endif %} 24 | 25 | {% if executor_url %} 26 | {{ layout.head_icon(executor_url, "Execute on " + executor_label, executor_icon) }} 27 | {% endif %} 28 | 29 | {{ layout.head_icon(download_url, "Download Notebook", "download", True) }} 30 | {% endblock %} 31 | 32 | 33 | {% block extra_head %} 34 | {{ super() }} 35 | 36 | {# Twitter Card #} 37 | 38 | {% block style_base %} 39 | 40 | {% endblock %} 41 | 42 | {% if css_theme %} 43 | 44 | {% endif %} 45 | 46 | {% block mathjax %} 47 | 49 | 77 | {% endblock mathjax %} 78 | 79 | {% block ipywidgets %} 80 | 81 | 105 | 106 | {% endblock ipywidgets %} 107 | {% endblock extra_head %} 108 | 109 | 110 | {% block body %} 111 | 112 | {{ link_breadcrumbs(breadcrumbs) }} 113 | {{ body | safe}} 114 | 115 | {% endblock %} 116 | 117 | 118 | {% block extra_script %} 119 | {{super()}} 120 | 131 | {% endblock extra_script %} 132 | 133 | 134 | {% block version_info %} 135 | {{super()}} 136 | {% if jupyter_info %} 137 |

138 | nbconvert version: 139 | {{jupyter_info['nbconvert_version']}} 140 | 141 |

142 | {% endif %} 143 | {% endblock version_info %} 144 | 145 | 146 | {% block extra_footer %} 147 | {{super()}} 148 | {% if date %} 149 |

150 | Rendered 151 | ({{date}}) 152 |

153 | 157 | {% endif %} 158 | {% endblock extra_footer %} 159 | -------------------------------------------------------------------------------- /nbviewer/static/img/nav_logo.svg: -------------------------------------------------------------------------------- 1 | Imported Layers 2 | -------------------------------------------------------------------------------- /nbviewer/providers/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | 8 | default_providers = [ 9 | "nbviewer.providers.{}".format(prov) for prov in ["url", "github", "gist"] 10 | ] 11 | 12 | default_rewrites = [ 13 | "nbviewer.providers.{}".format(prov) 14 | for prov in ["gist", "github", "dropbox", "huggingface", "url"] 15 | ] 16 | 17 | 18 | def provider_handlers(providers, **handler_kwargs): 19 | """Load tornado URL handlers from an ordered list of dotted-notation modules 20 | which contain a `default_handlers` function 21 | 22 | `default_handlers` should accept a list of handlers and returns an 23 | augmented list of handlers: this allows the addition of, for 24 | example, custom URLs which should be intercepted before being 25 | handed to the basic `url` handler 26 | 27 | `handler_kwargs` is a dict of dicts: first dict is `handler_names`, which 28 | specifies the handler_classes to load for the providers, the second 29 | is `handler_settings` (see comments in `format_handlers` in nbviewer/handlers.py) 30 | """ 31 | handler_names = handler_kwargs["handler_names"] 32 | handler_settings = handler_kwargs["handler_settings"] 33 | 34 | urlspecs = _load_provider_feature("default_handlers", providers, **handler_names) 35 | for handler_setting in handler_settings: 36 | if handler_settings[handler_setting]: 37 | # here we modify the URLSpec dict to have the key-value pairs from 38 | # handler_settings in NBViewer.init_tornado_application 39 | # kwargs passed to initialize are None by default but can be added 40 | # https://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.initialize 41 | for urlspec in urlspecs: 42 | urlspec[2][handler_setting] = handler_settings[handler_setting] 43 | return urlspecs 44 | 45 | 46 | def provider_uri_rewrites(providers): 47 | """Load (regex, template) tuples from an ordered list of dotted-notation 48 | modules which contain a `uri_rewrites` function 49 | 50 | `uri_rewrites` should accept a list of rewrites and returns an 51 | augmented list of rewrites: this allows the addition of, for 52 | example, the greedy behavior of the `gist` and `github` providers 53 | """ 54 | return _load_provider_feature("uri_rewrites", providers) 55 | 56 | 57 | def _load_provider_feature(feature, providers, **handler_names): 58 | """Load the named feature from an ordered list of dotted-notation modules 59 | which each implements the feature. 60 | 61 | The feature will be passed a list of feature implementations and must 62 | return that list, suitably modified. 63 | 64 | `handler_names` is the same as the `handler_names` attribute of the NBViewer class 65 | """ 66 | 67 | # Ex: provider = 'nbviewer.providers.url' 68 | # provider.rsplit(',', 1) = ['nbviewer.providers', 'url'] 69 | # provider_type = 'url' 70 | provider_types = [provider.rsplit(".", 1)[-1] for provider in providers] 71 | 72 | if "github" in provider_types: 73 | provider_types.append("github_blob") 74 | provider_types.append("github_tree") 75 | provider_types.remove("github") 76 | 77 | provider_handlers = {} 78 | 79 | # Ex: provider_type = 'url' 80 | for provider_type in provider_types: 81 | # Ex: provider_handler_key = 'url_handler' 82 | provider_handler_key = provider_type + "_handler" 83 | try: 84 | # Ex: handler_names['url_handler'] 85 | handler_names[provider_handler_key] 86 | except KeyError: 87 | continue 88 | else: 89 | # Ex: provider_handlers['url_handler'] = handler_names['url_handler'] 90 | provider_handlers[provider_handler_key] = handler_names[ 91 | provider_handler_key 92 | ] 93 | 94 | features = [] 95 | 96 | # Ex: provider = 'nbviewer.providers.url' 97 | for provider in providers: 98 | # Ex: module = __import__('nbviewer.providers.url', fromlist=['default_handlers']) 99 | module = __import__(provider, fromlist=[feature]) 100 | # Ex: getattr(module, 'default_handlers') = the `default_handlers` function from 101 | # nbviewer.providers.url (in handlers.py of nbviewer/providers/url) 102 | # so in example, features = nbviewer.providers.url.default_handlers(list_of_already_loaded_handlers, **handler_names) 103 | # => features = list_of_already_loaded_handlers + [URLSpec of chosen URL handler] 104 | features = getattr(module, feature)(features, **handler_names) 105 | return features 106 | 107 | 108 | def _load_handler_from_location(handler_location): 109 | # Ex: handler_location = 'nbviewer.providers.url.URLHandler' 110 | # module_name = 'nbviewer.providers.url', handler_name = 'URLHandler' 111 | module_name, handler_name = tuple(handler_location.rsplit(".", 1)) 112 | 113 | module = __import__(module_name, fromlist=[handler_name]) 114 | handler = getattr(module, handler_name) 115 | return handler 116 | -------------------------------------------------------------------------------- /helm-chart/nbviewer/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "nbviewer.fullname" . }} 5 | labels: 6 | component: nbviewer 7 | {{- include "nbviewer.labels" . | nindent 4 }} 8 | spec: 9 | replicas: {{ .Values.replicas }} 10 | selector: 11 | matchLabels: 12 | component: nbviewer 13 | {{- include "nbviewer.matchLabels" . | nindent 6 }} 14 | {{- if .Values.deploymentStrategy }} 15 | strategy: 16 | {{- .Values.deploymentStrategy | toYaml | trimSuffix "\n" | nindent 4 }} 17 | {{- end }} 18 | template: 19 | metadata: 20 | labels: 21 | component: nbviewer 22 | {{- include "nbviewer.matchLabels" . | nindent 8 }} 23 | annotations: 24 | # This lets us autorestart when the secret changes! 25 | checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} 26 | {{- if .Values.annotations }} 27 | {{- .Values.annotations | toYaml | trimSuffix "\n" | nindent 8 }} 28 | {{- end }} 29 | spec: 30 | nodeSelector: {{ toJson .Values.nodeSelector }} 31 | volumes: 32 | {{- if .Values.extraVolumes }} 33 | {{- .Values.extraVolumes | toYaml | trimSuffix "\n" | nindent 8 }} 34 | {{- end }} 35 | {{- if .Values.initContainers }} 36 | initContainers: 37 | {{- .Values.initContainers | toYaml | trimSuffix "\n" | nindent 8 }} 38 | {{- end }} 39 | containers: 40 | {{- if .Values.extraContainers }} 41 | {{- .Values.extraContainers | toYaml | trimSuffix "\n" | nindent 8 }} 42 | {{- end }} 43 | - name: nbviewer 44 | image: {{ .Values.image }} 45 | command: 46 | - python3 47 | - "-m" 48 | - nbviewer 49 | - --port=5000 50 | {{- if .Values.nbviewer.extraArgs }} 51 | {{- .Values.nbviewer.extraArgs | toYaml | trimSuffix "\n" | nindent 12 }} 52 | {{- end }} 53 | 54 | volumeMounts: 55 | 56 | # - mountPath: /etc/nbviewer/values.json 57 | # subPath: values.json 58 | # name: values 59 | 60 | {{- if .Values.extraVolumeMounts }} 61 | {{- .Values.extraVolumeMounts | toYaml | trimSuffix "\n" | nindent 12 }} 62 | {{- end }} 63 | resources: 64 | {{- .Values.resources | toYaml | trimSuffix "\n" | nindent 12 }} 65 | {{- with .Values.imagePullPolicy }} 66 | imagePullPolicy: {{ . }} 67 | {{- end }} 68 | env: 69 | - name: PYTHONUNBUFFERED 70 | value: "1" 71 | - name: HELM_RELEASE_NAME 72 | value: {{ .Release.Name | quote }} 73 | - name: POD_NAMESPACE 74 | valueFrom: 75 | fieldRef: 76 | fieldPath: metadata.namespace 77 | {{- if .Values.github.clientId }} 78 | - name: GITHUB_OAUTH_KEY 79 | valueFrom: 80 | secretKeyRef: 81 | name: {{ template "nbviewer.fullname" . }} 82 | key: github-clientId 83 | {{- end }} 84 | {{- if .Values.github.clientSecret }} 85 | - name: GITHUB_OAUTH_SECRET 86 | valueFrom: 87 | secretKeyRef: 88 | name: {{ template "nbviewer.fullname" . }} 89 | key: github-clientSecret 90 | {{- end }} 91 | {{- if .Values.github.accessToken }} 92 | - name: GITHUB_API_TOKEN 93 | valueFrom: 94 | secretKeyRef: 95 | name: {{ template "nbviewer.fullname" . }} 96 | key: github-accessToken 97 | {{- end }} 98 | - name: MEMCACHIER_SERVERS 99 | value: {{ .Release.Name }}-memcached:11211 100 | {{- if .Values.extraEnv }} 101 | {{- range $key, $value := .Values.extraEnv }} 102 | - name: {{ $key | quote }} 103 | value: {{ $value | quote }} 104 | {{- end }} 105 | {{- end }} 106 | ports: 107 | - containerPort: 5000 108 | name: nbviewer 109 | {{- if .Values.livenessProbe.enabled }} 110 | # livenessProbe notes: 111 | # We don't know how long hub database upgrades could take 112 | # so having a liveness probe could be a bit risky unless we put 113 | # a initialDelaySeconds value with long enough margin for that 114 | # to not be an issue. If it is too short, we could end up aborting 115 | # database upgrades midway or ending up in an infinite restart 116 | # loop. 117 | livenessProbe: 118 | initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} 119 | periodSeconds: {{ .Values.livenessProbe.periodSeconds }} 120 | httpGet: 121 | path: {{ .Values.nbviewer.baseUrl | trimSuffix "/" | quote }} 122 | port: nbviewer 123 | {{- end }} 124 | {{- if .Values.readinessProbe.enabled }} 125 | readinessProbe: 126 | initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} 127 | periodSeconds: {{ .Values.readinessProbe.periodSeconds }} 128 | httpGet: 129 | path: {{ .Values.nbviewer.baseUrl | trimSuffix "/" | quote }} 130 | port: nbviewer 131 | {{- end }} 132 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import hashlib 6 | import json 7 | import os 8 | import shutil 9 | import sys 10 | import tempfile 11 | from tarfile import TarFile 12 | from urllib.request import urlretrieve 13 | 14 | import invoke 15 | 16 | NOTEBOOK_VERSION = "5.7.8" # the notebook version whose LESS we will use 17 | NOTEBOOK_CHECKSUM = "573e0ae650c5d76b18b6e564ba6d21bf321d00847de1d215b418acb64f056eb8" # sha256 checksum of notebook tarball 18 | 19 | APP_ROOT = os.path.dirname(__file__) 20 | NPM_BIN = os.path.join(APP_ROOT, "node_modules", ".bin") 21 | NOTEBOOK_STATIC_PATH = os.path.join( 22 | APP_ROOT, "notebook-%s" % NOTEBOOK_VERSION, "notebook", "static" 23 | ) 24 | NOTEBOOK_URL = f"https://files.pythonhosted.org/packages/source/n/notebook/notebook-{NOTEBOOK_VERSION}.tar.gz" 25 | 26 | 27 | @invoke.task 28 | def test(ctx): 29 | ctx.run("nosetests -v") 30 | 31 | 32 | @invoke.task 33 | def bower(ctx): 34 | ctx.run( 35 | "cd {}/nbviewer/static &&".format(APP_ROOT) 36 | + " {}/bower install".format(NPM_BIN) 37 | + " --config.interactive=false --allow-root" 38 | ) 39 | 40 | 41 | @invoke.task 42 | def notebook_static(ctx): 43 | if os.path.exists(NOTEBOOK_STATIC_PATH): 44 | return 45 | 46 | fname = "notebook-%s.tar.gz" % NOTEBOOK_VERSION 47 | nb_archive = os.path.join(APP_ROOT, fname) 48 | if not os.path.exists(nb_archive): 49 | print("Downloading from pypi -> %s" % nb_archive) 50 | urlretrieve(NOTEBOOK_URL, nb_archive) 51 | with open(nb_archive, "rb") as f: 52 | checksum = hashlib.sha256(f.read()).hexdigest() 53 | if checksum != NOTEBOOK_CHECKSUM: 54 | print("Notebook sdist checksum mismatch", file=sys.stderr) 55 | print("Expected: %s" % NOTEBOOK_CHECKSUM, file=sys.stderr) 56 | print("Got: %s" % checksum, file=sys.stderr) 57 | sys.exit(1) 58 | with TarFile.open(nb_archive, "r:gz") as nb_archive_file: 59 | print("Extract {0} in {1}".format(nb_archive, nb_archive_file.extractall())) 60 | 61 | 62 | @invoke.task 63 | def less(ctx, debug=False): 64 | notebook_static(ctx) 65 | if debug: 66 | extra = "--source-map" 67 | else: 68 | extra = " --clean-css='--s1 --advanced --compatibility=ie8'" 69 | 70 | tmpl = ( 71 | "cd {app_root}/nbviewer/static/less " 72 | " && {npm_bin}/lessc" 73 | " {extra} " 74 | " --include-path={include_path}" 75 | " {name}.less ../build/{name}.css" 76 | " && {npm_bin}/postcss ../build/{name}.css --use autoprefixer -d ../build/" 77 | ) 78 | 79 | for name in ["styles", "notebook", "slides", "custom"]: 80 | ctx.run( 81 | tmpl.format( 82 | app_root=APP_ROOT, 83 | name=name, 84 | extra=extra, 85 | include_path=NOTEBOOK_STATIC_PATH, 86 | npm_bin=NPM_BIN, 87 | ) 88 | ) 89 | 90 | 91 | @invoke.task 92 | def screenshots(ctx, root="http://localhost:5000/", dest="./screenshots"): 93 | dest = os.path.abspath(dest) 94 | 95 | script = """ 96 | root = "{root}" 97 | 98 | urls = ({{name, url}} for name, url of {{ 99 | home: "" 100 | dir: "github/ipython/ipython/tree/3.x/examples/" 101 | user: "github/ipython/" 102 | gists: "gist/fperez/" 103 | notebook: "github/ipython/ipython/blob/3.x/examples/Notebook/Notebook%20Basics.ipynb"}}) 104 | 105 | screens = ({{name, w, h}} for name, [w, h] of {{ 106 | smartphone_portrait: [320, 480] 107 | smartphone_landscape: [480, 320] 108 | tablet_portrait: [768, 1024] 109 | tablet_landscape: [1024, 768] 110 | desktop_standard: [1280, 1024] 111 | desktop_1080p: [1920, 1080] 112 | }}) 113 | 114 | casper.start root 115 | 116 | casper.each screens, (_, screen) -> 117 | @then -> 118 | @viewport screen.w, screen.h, -> 119 | _.each urls, (_, page) -> 120 | @thenOpen root + page.url, -> 121 | @wait 1000 122 | @then -> 123 | @echo "#{{page.name}} #{{screen.name}}" 124 | @capture "{dest}/#{{page.name}}-#{{screen.name}}.png" 125 | 126 | casper.run() 127 | """.format( 128 | root=root, dest=dest 129 | ) 130 | 131 | tmpdir = tempfile.mkdtemp() 132 | tmpfile = os.path.join(tmpdir, "screenshots.coffee") 133 | with open(tmpfile, "w+") as f: 134 | f.write(script) 135 | ctx.run("casperjs test {script}".format(script=tmpfile)) 136 | 137 | shutil.rmtree(tmpdir) 138 | 139 | 140 | @invoke.task 141 | def sdist(ctx): 142 | bower(ctx) 143 | less(ctx) 144 | ctx.run("python setup.py sdist") 145 | 146 | 147 | @invoke.task 148 | def git_info(ctx): 149 | sys.path.insert(0, os.path.join(APP_ROOT, "nbviewer")) 150 | try: 151 | from utils import git_info, GIT_INFO_JSON 152 | 153 | info = git_info(APP_ROOT, force_git=True) 154 | except Exception as e: 155 | print("Failed to get git info", e) 156 | return 157 | print("Writing git info to %s" % GIT_INFO_JSON) 158 | with open(GIT_INFO_JSON, "w") as f: 159 | json.dump(info, f) 160 | sys.path.pop(0) 161 | 162 | 163 | @invoke.task 164 | def release(ctx): 165 | bower(ctx) 166 | less(ctx) 167 | ctx.run("python setup.py sdist bdist_wheel upload") 168 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile 6 | # 7 | anyio==4.10.0 8 | # via jupyter-server 9 | argon2-cffi==25.1.0 10 | # via jupyter-server 11 | argon2-cffi-bindings==25.1.0 12 | # via argon2-cffi 13 | arrow==1.3.0 14 | # via isoduration 15 | asttokens==3.0.0 16 | # via stack-data 17 | attrs==25.4.0 18 | # via 19 | # jsonschema 20 | # referencing 21 | beautifulsoup4==4.14.3 22 | # via nbconvert 23 | bleach[css]==6.2.0 24 | # via nbconvert 25 | certifi==2025.8.3 26 | # via elastic-transport 27 | cffi==2.0.0 28 | # via argon2-cffi-bindings 29 | decorator==5.2.1 30 | # via ipython 31 | defusedxml==0.7.1 32 | # via nbconvert 33 | elastic-transport==9.1.0 34 | # via elasticsearch 35 | elasticsearch==9.1.1 36 | # via -r requirements.in 37 | exceptiongroup==1.3.0 38 | # via 39 | # anyio 40 | # ipython 41 | executing==2.2.1 42 | # via stack-data 43 | fastjsonschema==2.21.2 44 | # via nbformat 45 | fqdn==1.5.1 46 | # via jsonschema 47 | idna==3.10 48 | # via 49 | # anyio 50 | # jsonschema 51 | ipython==8.37.0 52 | # via -r requirements.in 53 | isoduration==20.11.0 54 | # via jsonschema 55 | jedi==0.19.2 56 | # via ipython 57 | jinja2==3.1.6 58 | # via 59 | # jupyter-server 60 | # nbconvert 61 | jsonpointer==3.0.0 62 | # via jsonschema 63 | jsonschema[format-nongpl]==4.25.1 64 | # via 65 | # jupyter-events 66 | # nbformat 67 | jsonschema-specifications==2025.9.1 68 | # via jsonschema 69 | jupyter-client==8.6.3 70 | # via 71 | # -r requirements.in 72 | # jupyter-server 73 | # nbclient 74 | jupyter-core==5.8.1 75 | # via 76 | # jupyter-client 77 | # jupyter-server 78 | # nbclient 79 | # nbconvert 80 | # nbformat 81 | jupyter-events==0.12.0 82 | # via jupyter-server 83 | jupyter-server==2.17.0 84 | # via -r requirements.in 85 | jupyter-server-terminals==0.5.3 86 | # via jupyter-server 87 | jupyterlab-pygments==0.3.0 88 | # via nbconvert 89 | lark==1.2.2 90 | # via rfc3987-syntax 91 | markdown==3.9 92 | # via -r requirements.in 93 | markupsafe==3.0.2 94 | # via 95 | # jinja2 96 | # nbconvert 97 | matplotlib-inline==0.1.7 98 | # via ipython 99 | mistune==3.1.4 100 | # via nbconvert 101 | nbclient==0.10.2 102 | # via nbconvert 103 | nbconvert==7.16.6 104 | # via 105 | # -r requirements.in 106 | # jupyter-server 107 | nbformat==5.10.4 108 | # via 109 | # -r requirements.in 110 | # jupyter-server 111 | # nbclient 112 | # nbconvert 113 | overrides==7.7.0 114 | # via jupyter-server 115 | packaging==25.0 116 | # via 117 | # jupyter-events 118 | # jupyter-server 119 | # nbconvert 120 | pandocfilters==1.5.1 121 | # via nbconvert 122 | parso==0.8.5 123 | # via jedi 124 | pexpect==4.9.0 125 | # via ipython 126 | platformdirs==4.5.0 127 | # via jupyter-core 128 | prometheus-client==0.22.1 129 | # via jupyter-server 130 | prompt-toolkit==3.0.52 131 | # via ipython 132 | ptyprocess==0.7.0 133 | # via 134 | # pexpect 135 | # terminado 136 | pure-eval==0.2.3 137 | # via stack-data 138 | pycparser==2.23 139 | # via cffi 140 | pycurl==7.45.6 141 | # via -r requirements.in 142 | pygments==2.19.2 143 | # via 144 | # ipython 145 | # nbconvert 146 | pylibmc==1.6.3 147 | # via -r requirements.in 148 | python-dateutil==2.9.0.post0 149 | # via 150 | # arrow 151 | # elasticsearch 152 | # jupyter-client 153 | python-json-logger==4.0.0 154 | # via jupyter-events 155 | pyyaml==6.0.2 156 | # via jupyter-events 157 | pyzmq==27.1.0 158 | # via 159 | # jupyter-client 160 | # jupyter-server 161 | referencing==0.36.2 162 | # via 163 | # jsonschema 164 | # jsonschema-specifications 165 | # jupyter-events 166 | rfc3339-validator==0.1.4 167 | # via 168 | # jsonschema 169 | # jupyter-events 170 | rfc3986-validator==0.1.1 171 | # via 172 | # jsonschema 173 | # jupyter-events 174 | rfc3987-syntax==1.1.0 175 | # via jsonschema 176 | rpds-py==0.27.1 177 | # via 178 | # jsonschema 179 | # referencing 180 | send2trash==1.8.3 181 | # via jupyter-server 182 | six==1.17.0 183 | # via 184 | # python-dateutil 185 | # rfc3339-validator 186 | sniffio==1.3.1 187 | # via anyio 188 | soupsieve==2.8 189 | # via beautifulsoup4 190 | stack-data==0.6.3 191 | # via ipython 192 | statsd==4.0.1 193 | # via -r requirements.in 194 | terminado==0.18.1 195 | # via 196 | # jupyter-server 197 | # jupyter-server-terminals 198 | tinycss2==1.4.0 199 | # via bleach 200 | tornado==6.5.2 201 | # via 202 | # -r requirements.in 203 | # jupyter-client 204 | # jupyter-server 205 | # terminado 206 | traitlets==5.14.3 207 | # via 208 | # ipython 209 | # jupyter-client 210 | # jupyter-core 211 | # jupyter-events 212 | # jupyter-server 213 | # matplotlib-inline 214 | # nbclient 215 | # nbconvert 216 | # nbformat 217 | types-python-dateutil==2.9.0.20250822 218 | # via arrow 219 | typing-extensions==4.15.0 220 | # via 221 | # anyio 222 | # beautifulsoup4 223 | # elasticsearch 224 | # exceptiongroup 225 | # ipython 226 | # mistune 227 | # referencing 228 | uri-template==1.3.0 229 | # via jsonschema 230 | urllib3==2.6.0 231 | # via elastic-transport 232 | wcwidth==0.2.14 233 | # via prompt-toolkit 234 | webcolors==24.11.1 235 | # via jsonschema 236 | webencodings==0.5.1 237 | # via 238 | # bleach 239 | # tinycss2 240 | websocket-client==1.8.0 241 | # via jupyter-server 242 | -------------------------------------------------------------------------------- /nbviewer/handlers.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | from tornado import web 8 | 9 | from .providers import _load_handler_from_location 10 | from .providers import provider_handlers 11 | from .providers import provider_uri_rewrites 12 | from .providers.base import BaseHandler 13 | from .providers.base import format_prefix 14 | from .utils import transform_ipynb_uri 15 | from .utils import url_path_join 16 | 17 | # ----------------------------------------------------------------------------- 18 | # Handler classes 19 | # ----------------------------------------------------------------------------- 20 | 21 | 22 | class Custom404(BaseHandler): 23 | """Render our 404 template""" 24 | 25 | def prepare(self): 26 | # skip parent prepare() step, just render the 404 27 | raise web.HTTPError(404) 28 | 29 | 30 | class IndexHandler(BaseHandler): 31 | """Render the index""" 32 | 33 | def render_index_template(self, **namespace): 34 | return self.render_template( 35 | "index.html", 36 | title=self.frontpage_setup.get("title", None), 37 | subtitle=self.frontpage_setup.get("subtitle", None), 38 | text=self.frontpage_setup.get("text", None), 39 | show_input=self.frontpage_setup.get("show_input", True), 40 | sections=self.frontpage_setup.get("sections", []), 41 | **namespace, 42 | ) 43 | 44 | def get(self): 45 | self.finish(self.render_index_template()) 46 | 47 | 48 | class FAQHandler(BaseHandler): 49 | """Render the markdown FAQ page""" 50 | 51 | def get(self): 52 | self.finish(self.render_template("faq.md")) 53 | 54 | 55 | class CreateHandler(BaseHandler): 56 | """handle creation via frontpage form 57 | 58 | only redirects to the appropriate URL 59 | """ 60 | 61 | uri_rewrite_list = None 62 | 63 | def post(self): 64 | value = self.get_argument("gistnorurl", "") 65 | redirect_url = transform_ipynb_uri(value, self.get_provider_rewrites()) 66 | self.log.info("create %s => %s", value, redirect_url) 67 | self.redirect(url_path_join(self.base_url, redirect_url)) 68 | 69 | def get_provider_rewrites(self): 70 | # storing this on a class attribute is a little icky, but is better 71 | # than the global this was refactored from. 72 | if self.uri_rewrite_list is None: 73 | # providers is a list of module import paths 74 | providers = self.settings["provider_rewrites"] 75 | 76 | type(self).uri_rewrite_list = provider_uri_rewrites(providers) 77 | return self.uri_rewrite_list 78 | 79 | 80 | # ----------------------------------------------------------------------------- 81 | # Default handler URL mapping 82 | # ----------------------------------------------------------------------------- 83 | 84 | 85 | def format_handlers(formats, urlspecs, **handler_settings): 86 | """ 87 | Tornado handler URLSpec of form (route, handler_class, initalize_kwargs) 88 | https://www.tornadoweb.org/en/stable/web.html#tornado.web.URLSpec 89 | kwargs passed to initialize are None by default but can be added 90 | https://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.initialize 91 | """ 92 | urlspecs = [ 93 | (prefix + url, handler, {"format": format, "format_prefix": prefix}) 94 | for format in formats 95 | for url, handler, initialize_kwargs in urlspecs 96 | for prefix in [format_prefix + format] 97 | ] 98 | for handler_setting in handler_settings: 99 | if handler_settings[handler_setting]: 100 | # here we modify the URLSpec dict to have the key-value pairs from 101 | # handler_settings in NBViewer.init_tornado_application 102 | for urlspec in urlspecs: 103 | urlspec[2][handler_setting] = handler_settings[handler_setting] 104 | return urlspecs 105 | 106 | 107 | def init_handlers(formats, providers, base_url, localfiles, **handler_kwargs): 108 | """ 109 | `handler_kwargs` is a dict of dicts: first dict is `handler_names`, which 110 | specifies the handler_classes to load for the providers, the second 111 | is `handler_settings` (see comments in format_handlers) 112 | Only `handler_settings` should get added to the initialize_kwargs in the 113 | handler URLSpecs, which is why we pass only it to `format_handlers` 114 | but both it and `handler_names` to `provider_handlers` 115 | """ 116 | handler_settings = handler_kwargs["handler_settings"] 117 | handler_names = handler_kwargs["handler_names"] 118 | 119 | create_handler = _load_handler_from_location(handler_names["create_handler"]) 120 | custom404_handler = _load_handler_from_location(handler_names["custom404_handler"]) 121 | faq_handler = _load_handler_from_location(handler_names["faq_handler"]) 122 | index_handler = _load_handler_from_location(handler_names["index_handler"]) 123 | 124 | # If requested endpoint matches multiple routes, it only gets handled by handler 125 | # corresponding to the first matching route. So order of URLSpecs in this list matters. 126 | pre_providers = [ 127 | ("/?", index_handler, {}), 128 | ("/index.html", index_handler, {}), 129 | (r"/faq/?", faq_handler, {}), 130 | (r"/create/?", create_handler, {}), 131 | # don't let super old browsers request data-uris 132 | (r".*/data:.*;base64,.*", custom404_handler, {}), 133 | ] 134 | 135 | post_providers = [(r"/(robots\.txt|favicon\.ico)", web.StaticFileHandler, {})] 136 | 137 | # Add localfile handlers if the option is set 138 | if localfiles: 139 | # Put local provider first as per the comment at 140 | # https://github.com/jupyter/nbviewer/pull/727#discussion_r144448440. 141 | providers.insert(0, "nbviewer.providers.local") 142 | 143 | handlers = provider_handlers(providers, **handler_kwargs) 144 | 145 | raw_handlers = ( 146 | pre_providers 147 | + handlers 148 | + format_handlers(formats, handlers, **handler_settings) 149 | + post_providers 150 | ) 151 | 152 | new_handlers = [] 153 | for handler in raw_handlers: 154 | pattern = url_path_join(base_url, handler[0]) 155 | new_handler = tuple([pattern] + list(handler[1:])) 156 | new_handlers.append(new_handler) 157 | new_handlers.append((r".*", custom404_handler, {})) 158 | 159 | return new_handlers 160 | -------------------------------------------------------------------------------- /nbviewer/providers/github/tests/test_github.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # ----------------------------------------------------------------------------- 3 | # Copyright (C) Jupyter Development Team 4 | # 5 | # Distributed under the terms of the BSD License. The full license is in 6 | # the file COPYING, distributed as part of this software. 7 | # ----------------------------------------------------------------------------- 8 | import requests 9 | 10 | from ....tests.base import FormatHTMLMixin 11 | from ....tests.base import NBViewerTestCase 12 | from ....tests.base import skip_unless_github_auth 13 | 14 | 15 | class GitHubTestCase(NBViewerTestCase): 16 | @skip_unless_github_auth 17 | def ipython_example(self, *parts, **kwargs): 18 | ref = kwargs.get("ref", "rel-2.0.0") 19 | return self.url("github/ipython/ipython/blob/%s/examples" % ref, *parts) 20 | 21 | @skip_unless_github_auth 22 | def test_github(self): 23 | url = self.ipython_example("Index.ipynb") 24 | r = requests.get(url) 25 | self.assertEqual(r.status_code, 200) 26 | 27 | @skip_unless_github_auth 28 | def test_github_unicode(self): 29 | url = self.url( 30 | "github/tlapicka/IPythonNotebooks/blob", 31 | "ee6d2d13b96023e5f5e38e4516803eb22ede977e", 32 | "Matplotlib -- osy a mřížka.ipynb", 33 | ) 34 | r = requests.get(url) 35 | self.assertEqual(r.status_code, 200) 36 | 37 | @skip_unless_github_auth 38 | def test_github_blob_redirect_unicode(self): 39 | url = self.url( 40 | "/urls/github.com/tlapicka/IPythonNotebooks/blob", 41 | "ee6d2d13b96023e5f5e38e4516803eb22ede977e", 42 | "Matplotlib -- osy a mřížka.ipynb", 43 | ) 44 | r = requests.get(url) 45 | self.assertEqual(r.status_code, 200) 46 | # verify redirect 47 | self.assertIn("/github/tlapicka/IPythonNotebooks/blob/", r.request.url) 48 | 49 | @skip_unless_github_auth 50 | def test_github_raw_redirect_unicode(self): 51 | url = self.url( 52 | "/url/raw.github.com/tlapicka/IPythonNotebooks", 53 | "ee6d2d13b96023e5f5e38e4516803eb22ede977e", 54 | "Matplotlib -- osy a mřížka.ipynb", 55 | ) 56 | r = requests.get(url) 57 | self.assertEqual(r.status_code, 200) 58 | # verify redirect 59 | self.assertIn("/github/tlapicka/IPythonNotebooks/blob/", r.request.url) 60 | 61 | @skip_unless_github_auth 62 | def test_github_tag(self): 63 | url = self.ipython_example("Index.ipynb", ref="rel-2.0.0") 64 | r = requests.get(url) 65 | self.assertEqual(r.status_code, 200) 66 | 67 | @skip_unless_github_auth 68 | def test_github_commit(self): 69 | url = self.ipython_example( 70 | "Index.ipynb", ref="7f5cbd622058396f1f33c4b26c8d205a8dd26d16" 71 | ) 72 | r = requests.get(url) 73 | self.assertEqual(r.status_code, 200) 74 | 75 | @skip_unless_github_auth 76 | def test_github_blob_redirect(self): 77 | url = self.url( 78 | "urls/github.com/ipython/ipython/blob/rel-2.0.0/examples", "Index.ipynb" 79 | ) 80 | r = requests.get(url) 81 | self.assertEqual(r.status_code, 200) 82 | # verify redirect 83 | self.assertIn("/github/ipython/ipython/blob/master", r.request.url) 84 | 85 | @skip_unless_github_auth 86 | def test_github_raw_redirect(self): 87 | url = self.url( 88 | "urls/raw.github.com/ipython/ipython/rel-2.0.0/examples", "Index.ipynb" 89 | ) 90 | r = requests.get(url) 91 | self.assertEqual(r.status_code, 200) 92 | # verify redirect 93 | self.assertIn("/github/ipython/ipython/blob/rel-2.0.0/examples", r.request.url) 94 | 95 | @skip_unless_github_auth 96 | def test_github_rawusercontent_redirect(self): 97 | """Test GitHub's new raw domain""" 98 | url = self.url( 99 | "urls/raw.githubusercontent.com/ipython/ipython/rel-2.0.0/examples", 100 | "Index.ipynb", 101 | ) 102 | r = requests.get(url) 103 | self.assertEqual(r.status_code, 200) 104 | # verify redirect 105 | self.assertIn("/github/ipython/ipython/blob/rel-2.0.0/examples", r.request.url) 106 | 107 | @skip_unless_github_auth 108 | def test_github_raw_redirect_2(self): 109 | """test /url/github.com/u/r/raw/ redirects""" 110 | url = self.url( 111 | "url/github.com/ipython/ipython/blob/rel-2.0.0/examples", "Index.ipynb" 112 | ) 113 | r = requests.get(url) 114 | self.assertEqual(r.status_code, 200) 115 | # verify redirect 116 | self.assertIn("/github/ipython/ipython/blob/rel-2.0.0", r.request.url) 117 | 118 | @skip_unless_github_auth 119 | def test_github_repo_redirect(self): 120 | url = self.url("github/ipython/ipython") 121 | r = requests.get(url) 122 | self.assertEqual(r.status_code, 200) 123 | # verify redirect 124 | self.assertIn("/github/ipython/ipython/tree/master", r.request.url) 125 | 126 | @skip_unless_github_auth 127 | def test_github_tree(self): 128 | url = self.url("github/ipython/ipython/tree/rel-2.0.0/IPython/") 129 | r = requests.get(url) 130 | self.assertEqual(r.status_code, 200) 131 | self.assertIn("__init__.py", r.text) 132 | 133 | @skip_unless_github_auth 134 | def test_github_tree_redirect(self): 135 | url = self.url("github/ipython/ipython/tree/rel-2.0.0/MANIFEST.in") 136 | r = requests.get(url) 137 | self.assertEqual(r.status_code, 200) 138 | # verify redirect 139 | self.assertIn("/github/ipython/ipython/blob/rel-2.0.0", r.request.url) 140 | self.assertIn("global-exclude", r.text) 141 | 142 | @skip_unless_github_auth 143 | def test_github_blob_redirect_II(self): 144 | url = self.url("github/ipython/ipython/blob/rel-2.0.0/IPython") 145 | r = requests.get(url) 146 | self.assertEqual(r.status_code, 200) 147 | # verify redirect 148 | self.assertIn("/github/ipython/ipython/tree/rel-2.0.0/IPython", r.request.url) 149 | self.assertIn("__init__.py", r.text) 150 | 151 | @skip_unless_github_auth 152 | def test_github_ref_list(self): 153 | url = self.url("github/ipython/ipython/tree/master") 154 | r = requests.get(url) 155 | self.assertEqual(r.status_code, 200) 156 | html = r.text 157 | # verify branch is linked 158 | self.assertIn("/github/ipython/ipython/tree/2.x/", html) 159 | # verify tag is linked 160 | self.assertIn("/github/ipython/ipython/tree/rel-2.3.0/", html) 161 | 162 | 163 | class FormatHTMLGitHubTestCase(NBViewerTestCase, FormatHTMLMixin): 164 | pass 165 | -------------------------------------------------------------------------------- /nbviewer/cache.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | import asyncio 8 | import zlib 9 | from asyncio import Future 10 | from concurrent.futures import ThreadPoolExecutor 11 | from time import monotonic 12 | 13 | from tornado.log import app_log 14 | 15 | try: 16 | import pylibmc # type: ignore 17 | except ModuleNotFoundError: 18 | pylibmc = None # type: ignore 19 | 20 | # ----------------------------------------------------------------------------- 21 | # Code 22 | # ----------------------------------------------------------------------------- 23 | 24 | 25 | class MockCache(object): 26 | """Mock Cache. Just stores nothing and always return None on get.""" 27 | 28 | def __init__(self, *args, **kwargs): 29 | pass 30 | 31 | async def get(self, key): 32 | f = Future() 33 | f.set_result(None) 34 | return await f 35 | 36 | async def set(self, key, value, *args, **kwargs): 37 | f = Future() 38 | f.set_result(None) 39 | return await f 40 | 41 | async def add(self, key, value, *args, **kwargs): 42 | f = Future() 43 | f.set_result(True) 44 | return await f 45 | 46 | async def incr(self, key): 47 | f = Future() 48 | f.set_result(None) 49 | return await f 50 | 51 | 52 | class DummyAsyncCache(object): 53 | """Dummy Async Cache. Just stores things in a dict of fixed size.""" 54 | 55 | def __init__(self, limit=10): 56 | self._cache = {} 57 | self._cache_order = [] 58 | self.limit = limit 59 | 60 | async def get(self, key): 61 | f = Future() 62 | f.set_result(self._get(key)) 63 | return await f 64 | 65 | def _get(self, key): 66 | value, deadline = self._cache.get(key, (None, None)) 67 | if deadline and deadline < monotonic(): 68 | self._cache.pop(key) 69 | self._cache_order.remove(key) 70 | else: 71 | return value 72 | 73 | async def set(self, key, value, expires=0): 74 | if key in self._cache and self._cache_order[-1] != key: 75 | idx = self._cache_order.index(key) 76 | del self._cache_order[idx] 77 | self._cache_order.append(key) 78 | else: 79 | if len(self._cache) >= self.limit: 80 | oldest = self._cache_order.pop(0) 81 | self._cache.pop(oldest) 82 | self._cache_order.append(key) 83 | 84 | if not expires: 85 | deadline = None 86 | else: 87 | deadline = monotonic() + expires 88 | 89 | self._cache[key] = (value, deadline) 90 | f = Future() 91 | f.set_result(True) 92 | return await f 93 | 94 | async def add(self, key, value, expires=0): 95 | f = Future() 96 | if self._get(key) is not None: 97 | f.set_result(False) 98 | else: 99 | await self.set(key, value, expires) 100 | f.set_result(True) 101 | return await f 102 | 103 | async def incr(self, key): 104 | f = Future() 105 | if self._get(key) is not None: 106 | value, deadline = self._cache[key] 107 | value = value + 1 108 | self._cache[key] = (value, deadline) 109 | else: 110 | value = None 111 | f.set_result(value) 112 | return await f 113 | 114 | 115 | class AsyncMemcache(object): 116 | """Wrap pylibmc.Client to run in a background thread 117 | 118 | via concurrent.futures.ThreadPoolExecutor 119 | """ 120 | 121 | def __init__(self, *args, **kwargs): 122 | self.pool = kwargs.pop("pool", None) or ThreadPoolExecutor(1) 123 | 124 | self.mc = pylibmc.Client(*args, **kwargs) 125 | self.mc_pool = pylibmc.ThreadMappedPool(self.mc) 126 | 127 | self.loop = asyncio.get_event_loop() 128 | 129 | async def _call_in_thread(self, method_name, *args, **kwargs): 130 | # https://stackoverflow.com/questions/34376814/await-future-from-executor-future-cant-be-used-in-await-expression 131 | 132 | key = args[0] 133 | if "multi" in method_name: 134 | key = sorted(key)[0].decode("ascii") + "[%i]" % len(key) 135 | app_log.debug("memcache submit %s %s", method_name, key) 136 | 137 | def f(): 138 | app_log.debug("memcache %s %s", method_name, key) 139 | with self.mc_pool.reserve() as mc: 140 | meth = getattr(mc, method_name) 141 | return meth(*args, **kwargs) 142 | 143 | return await self.loop.run_in_executor(self.pool, f) 144 | 145 | async def get(self, *args, **kwargs): 146 | return await self._call_in_thread("get", *args, **kwargs) 147 | 148 | async def set(self, *args, **kwargs): 149 | return await self._call_in_thread("set", *args, **kwargs) 150 | 151 | async def add(self, *args, **kwargs): 152 | return await self._call_in_thread("add", *args, **kwargs) 153 | 154 | async def incr(self, *args, **kwargs): 155 | return await self._call_in_thread("incr", *args, **kwargs) 156 | 157 | 158 | class AsyncMultipartMemcache(AsyncMemcache): 159 | """subclass of AsyncMemcache that splits large files into multiple chunks 160 | 161 | because memcached limits record size to 1MB 162 | """ 163 | 164 | def __init__(self, *args, **kwargs): 165 | self.chunk_size = kwargs.pop("chunk_size", 950000) 166 | self.max_chunks = kwargs.pop("max_chunks", 16) 167 | super().__init__(*args, **kwargs) 168 | 169 | async def get(self, key, *args, **kwargs): 170 | keys = [("%s.%i" % (key, idx)).encode() for idx in range(self.max_chunks)] 171 | values = await self._call_in_thread("get_multi", keys, *args, **kwargs) 172 | parts = [] 173 | for key in keys: 174 | if key not in values: 175 | break 176 | parts.append(values[key]) 177 | if parts: 178 | compressed = b"".join(parts) 179 | try: 180 | result = zlib.decompress(compressed) 181 | except zlib.error as e: 182 | app_log.error("zlib decompression of %s failed: %s", key, e) 183 | else: 184 | return result 185 | 186 | async def set(self, key, value, *args, **kwargs): 187 | chunk_size = self.chunk_size 188 | compressed = zlib.compress(value) 189 | offsets = range(0, len(compressed), chunk_size) 190 | app_log.debug("storing %s in %i chunks", key, len(offsets)) 191 | if len(offsets) > self.max_chunks: 192 | raise ValueError("file is too large: %sB" % len(compressed)) 193 | values = {} 194 | for idx, offset in enumerate(offsets): 195 | values[("%s.%i" % (key, idx)).encode()] = compressed[ 196 | offset : offset + chunk_size 197 | ] 198 | return await self._call_in_thread("set_multi", values, *args, **kwargs) 199 | -------------------------------------------------------------------------------- /nbviewer/providers/github/client.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (C) Jupyter Development Team 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | # ----------------------------------------------------------------------------- 7 | import json 8 | import os 9 | 10 | from tornado.httpclient import AsyncHTTPClient 11 | from tornado.httpclient import HTTPError 12 | from tornado.httputil import url_concat 13 | 14 | from ...utils import quote 15 | from ...utils import response_text 16 | from ...utils import url_path_join 17 | 18 | 19 | # ----------------------------------------------------------------------------- 20 | # Async GitHub Client 21 | # ----------------------------------------------------------------------------- 22 | 23 | 24 | class AsyncGitHubClient: 25 | """AsyncHTTPClient wrapper with methods for common requests""" 26 | 27 | auth = None 28 | 29 | def __init__(self, log, client=None): 30 | self.log = log 31 | self.client = client or AsyncHTTPClient() 32 | self.github_api_url = os.environ.get( 33 | "GITHUB_API_URL", "https://api.github.com/" 34 | ) 35 | self.authenticate() 36 | 37 | def authenticate(self): 38 | self.auth = { 39 | "client_id": os.environ.get("GITHUB_OAUTH_KEY", ""), 40 | "client_secret": os.environ.get("GITHUB_OAUTH_SECRET", ""), 41 | "access_token": os.environ.get("GITHUB_API_TOKEN", ""), 42 | } 43 | 44 | def fetch(self, url, params=None, **kwargs): 45 | """Add GitHub auth to self.client.fetch""" 46 | if not url.startswith(self.github_api_url): 47 | raise ValueError("Only fetch GitHub urls with GitHub auth (%s)" % url) 48 | params = {} if params is None else params 49 | kwargs.setdefault("user_agent", "Tornado-Async-GitHub-Client") 50 | 51 | if self.auth["client_id"] and self.auth["client_secret"]: 52 | kwargs["auth_username"] = self.auth["client_id"] 53 | kwargs["auth_password"] = self.auth["client_secret"] 54 | 55 | if self.auth["access_token"]: 56 | headers = kwargs.setdefault("headers", {}) 57 | headers["Authorization"] = "token " + self.auth["access_token"] 58 | 59 | url = url_concat(url, params) 60 | future = self.client.fetch(url, **kwargs) 61 | future.add_done_callback(self._log_rate_limit) 62 | return future 63 | 64 | def _log_rate_limit(self, future): 65 | """log GitHub rate limit headers 66 | 67 | - error if 0 remaining 68 | - warn if 10% or less remain 69 | - debug otherwise 70 | """ 71 | try: 72 | r = future.result() 73 | except HTTPError as e: 74 | r = e.response 75 | if r is None: 76 | # some errors don't have a response (e.g. failure to build request) 77 | return 78 | limit_s = r.headers.get("X-RateLimit-Limit", "") 79 | remaining_s = r.headers.get("X-RateLimit-Remaining", "") 80 | if not remaining_s or not limit_s: 81 | if r.code < 300: 82 | self.log.warn( 83 | "No rate limit headers. Did GitHub change? %s", 84 | json.dumps(dict(r.headers), indent=1), 85 | ) 86 | return 87 | 88 | remaining = int(remaining_s) 89 | limit = int(limit_s) 90 | if remaining == 0 and r.code >= 400: 91 | text = response_text(r) 92 | try: 93 | message = json.loads(text)["message"] 94 | except Exception: 95 | # Can't extract message, log full reply 96 | message = text 97 | self.log.error("GitHub rate limit (%s) exceeded: %s", limit, message) 98 | return 99 | 100 | if 10 * remaining > limit: 101 | log = self.log.info 102 | else: 103 | log = self.log.warn 104 | log("%i/%i GitHub API requests remaining", remaining, limit) 105 | 106 | def github_api_request(self, path, **kwargs): 107 | """Make a GitHub API request to URL 108 | 109 | URL is constructed from url and params, if specified. 110 | **kwargs are passed to client.fetch unmodified. 111 | """ 112 | url = url_path_join(self.github_api_url, quote(path)) 113 | return self.fetch(url, **kwargs) 114 | 115 | def get_gist(self, gist_id, **kwargs): 116 | """Get a gist""" 117 | path = "gists/{}".format(gist_id) 118 | return self.github_api_request(path, **kwargs) 119 | 120 | def get_contents(self, user, repo, path, ref=None, **kwargs): 121 | """Make contents API request - either file contents or directory listing""" 122 | path = "repos/{user}/{repo}/contents/{path}".format(**locals()) 123 | if ref is not None: 124 | params = kwargs.setdefault("params", {}) 125 | params["ref"] = ref 126 | return self.github_api_request(path, **kwargs) 127 | 128 | def get_repos(self, user, **kwargs): 129 | """List a user's repos""" 130 | path = "users/{user}/repos".format(user=user) 131 | return self.github_api_request(path, **kwargs) 132 | 133 | def get_gists(self, user, **kwargs): 134 | """List a user's gists""" 135 | path = "users/{user}/gists".format(user=user) 136 | return self.github_api_request(path, **kwargs) 137 | 138 | def get_repo(self, user, repo, **kwargs): 139 | """List a repo's branches""" 140 | path = "repos/{user}/{repo}".format(user=user, repo=repo) 141 | return self.github_api_request(path, **kwargs) 142 | 143 | def get_tree(self, user, repo, path, ref="master", recursive=False, **kwargs): 144 | """Get a git tree""" 145 | # only need a recursive fetch if it's not in the top-level dir 146 | if "/" in path: 147 | recursive = True 148 | path = "repos/{user}/{repo}/git/trees/{ref}".format(**locals()) 149 | if recursive: 150 | params = kwargs.setdefault("params", {}) 151 | params["recursive"] = True 152 | tree = self.github_api_request(path, **kwargs) 153 | return tree 154 | 155 | def get_branches(self, user, repo, **kwargs): 156 | """List a repo's branches""" 157 | path = "repos/{user}/{repo}/branches".format(user=user, repo=repo) 158 | return self.github_api_request(path, **kwargs) 159 | 160 | def get_tags(self, user, repo, **kwargs): 161 | """List a repo's branches""" 162 | path = "repos/{user}/{repo}/tags".format(user=user, repo=repo) 163 | return self.github_api_request(path, **kwargs) 164 | 165 | def extract_tree_entry(self, path, tree_response): 166 | """extract a single tree entry from 167 | a tree response using for a path 168 | 169 | raises 404 if not found 170 | 171 | Useful for finding the blob url for a given path. 172 | """ 173 | tree_response.rethrow() 174 | self.log.debug(tree_response) 175 | jsondata = response_text(tree_response) 176 | data = json.loads(jsondata) 177 | for entry in data["tree"]: 178 | if entry["path"] == path: 179 | return entry 180 | 181 | raise HTTPError(404, "%s not found among %i files" % (path, len(data["tree"]))) 182 | -------------------------------------------------------------------------------- /nbviewer/templates/layout.html: -------------------------------------------------------------------------------- 1 | {% macro head_text(url, name, bold=False) -%} 2 |
  • 3 | 4 | {% if bold %}{% endif %} 5 | {{name}} 6 | {% if bold %}{% endif %} 7 | 8 |
  • 9 | {%- endmacro %} 10 | 11 | 12 | {% macro head_icon(url, name, icon, download=False) -%} 13 |
  • 14 | 15 | 16 | {{name}} 17 | 18 |
  • 19 | {%- endmacro %} 20 | 21 | 22 | {% macro link_breadcrumbs(crumbs) -%} 23 | {% if crumbs %} 24 | 31 | {% endif %} 32 | {%- endmacro %} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {{title|default("Jupyter Notebook Viewer", true)}} 41 | 42 | 43 | 44 | {% if not public %} 45 | 46 | {% endif %} 47 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 60 | 62 | 64 | 66 | 67 | {%- block extra_head %} 68 | {%- if extra_head_html %} 69 | {{ extra_head_html | safe }} 70 | {%- endif %} 71 | {%- endblock %} 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 81 | 82 | 83 | 84 | 86 | 119 | 120 | {% block container -%} 121 |
    122 | {% block body %}{% endblock %} 123 |
    124 | {%- endblock container %} 125 | 126 | 127 | {% block footer %} 128 |
    129 |
    130 |
    131 |

    132 | This website does not host notebooks, it only renders notebooks 133 | available on other websites. 134 |

    135 |
    136 | 137 |
    138 |

    139 | Delivered by Fastly, 140 | Rendered by OVHcloud 141 |

    142 |

    143 | nbviewer GitHub repository. 144 |

    145 |
    146 | 147 |
    148 | {% block version_info %} 149 | {% if git_data %} 150 |

    151 | nbviewer version: 152 | 153 | {{git_data['sha'][:7]}} 154 | 155 |

    156 | {% endif %} 157 | {% endblock version_info %} 158 | {% block extra_footer %}{% endblock extra_footer %} 159 |
    160 |
    161 |
    162 | {% endblock footer %} 163 | 164 | 165 | 166 | 167 | 168 | {% block extra_script %}{% endblock %} 169 | 170 | {% if google_analytics_id %} 171 | 181 | {% endif %} 182 | 198 | 199 | 200 | 201 | --------------------------------------------------------------------------------