├── 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 |
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 |
12 |
13 |
14 |
15 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/nbviewer/templates/popular.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 |
4 | {% block body %}
5 |
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 |
17 |
Read the FAQ for more information.
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 | | Name | Modified |
7 |
8 |
9 |
10 | |
11 | {% if len(breadcrumbs) > 1 %}
12 | ..
13 | {% endif %}
14 | |
15 | |
16 |
17 | {% for entry in entries %}
18 |
19 | |
20 | {% if entry.url %}
21 |
22 | {% endif %}
23 | {{entry.name}}
24 | {% if entry.url %}
25 |
26 | {% endif %}
27 | |
28 |
29 | {{ entry.modtime }}
30 | |
31 |
32 | {% endfor %}
33 |
34 |
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 | | Name |
9 |
10 | {% endblock header_row %}
11 |
12 |
13 | {% block entries %}
14 | {% for entry in entries %}
15 | {% block entry scoped %}
16 |
17 | |
18 |
19 |
20 | {{entry.name}}
21 |
22 | |
23 |
24 | {% endblock entry %}
25 | {% endfor %}
26 | {% endblock entries %}
27 |
28 |
29 | {% block page_links %}
30 | {% if prev_url or next_url %}
31 |
32 | |
33 | {% if prev_url %}
34 |
35 |
36 | prev
37 |
38 | {% endif %}
39 |
40 | {% if next_url and prev_url %}
41 | ...
42 | {% endif %}
43 |
44 | {% if next_url %}
45 |
46 | next
47 |
48 |
49 | {% endif %}
50 | |
51 |
52 | {% endif %}
53 | {% endblock page_links %}
54 |
55 |
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 |
33 | {% endif %}
34 |
35 | {% for section in sections %}
36 |
37 | {{section.header}}
38 |
39 |
40 | {% for link in section.links %}
41 | -
42 |
{{link.text}}
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 |
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 |
41 | {{ ref_dropdown(ref, branches, tags) }}
42 |
43 | {% endif %}
44 |
45 | {{ link_breadcrumbs(breadcrumbs) }}
46 |
47 |
48 |
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 |
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 |
17 |
18 |
19 | {%- endmacro %}
20 |
21 |
22 | {% macro link_breadcrumbs(crumbs) -%}
23 | {% if crumbs %}
24 |
25 | {% for crumb in crumbs %}
26 | -
27 | {{crumb['name']}}
28 |
29 | {% endfor %}
30 |
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 |
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 |
--------------------------------------------------------------------------------