├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cookiecutter.json ├── workflow.png └── {{cookiecutter.extension_name}} ├── .cookiecutter.yaml ├── .gitignore ├── .travis.yml ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── package.json ├── setup.py ├── setupbase.py ├── src ├── embed.js ├── index.js ├── lab_extension.js ├── nb_extension.js ├── nb_index.js └── renderer.js ├── style └── index.css ├── webpack.config.js └── {{cookiecutter.extension_name}} ├── __init__.py └── _version.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | sudo: false 5 | cache: 6 | pip: true 7 | directories: 8 | - $HOME/.npm 9 | before_install: 10 | - pip install -U pip setuptools 11 | - nvm install 6 12 | - pip install cookiecutter 13 | install: 14 | - pushd $(mktemp -d) 15 | - cookiecutter $TRAVIS_BUILD_DIR --no-input 16 | - pushd json_renderer 17 | - pip install -U -v -e . 18 | - pip uninstall -y json_renderer 19 | - pip install . 20 | before_script: 21 | - | 22 | export DISPLAY=:99.0 23 | sh -e /etc/init.d/xvfb start 24 | script: 25 | - jupyter nbextension enable --py --sys-prefix json_renderer 26 | - jupyter nbextension list 27 | - pip install jupyterlab 28 | - jupyter lab path 29 | - jupyter labextension list 30 | - jupyter labextension list 2>&1 | grep -q json_renderer 31 | - jupyter labextension link 32 | - popd 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mimerender-cookiecutter 2 | 3 | A [cookiecutter](https://github.com/audreyr/cookiecutter) template for creating 4 | a JupyterLab and Jupyter Notebook extension for rendering specific mime types and file extensions. 5 | 6 | ## Examples 7 | 8 | * [jupyterlab_json](https://github.com/jupyterlab/jupyter-renderers) 9 | * [jupyterlab_geojson](https://github.com/jupyterlab/jupyter-renderers) 10 | * [jupyterlab_plotly](https://github.com/jupyterlab/jupyter-renderers) 11 | 12 | ## Usage 13 | 14 | Install cookiecutter: 15 | 16 | ``` 17 | pip install cookiecutter 18 | ``` 19 | 20 | Use cookiecutter to generate a package: 21 | 22 | ``` 23 | cookiecutter https://github.com/jupyterlab/mimerender-cookiecutter 24 | ``` 25 | 26 | ## Prompts 27 | 28 | The cookiecutter will prompt you with the following questions and generate a project according to your responses: 29 | 30 | * `author_name`: Your full name. This will be used in the generated Python and npm packages. 31 | * `author_email`: Your email address. This will be used in the generated Python and npm packages. 32 | * `mime_type`: A valid mime type (e.g. `application/json`, `application/geo+json`). This will be used to render output data of this mime type with your extension. 33 | * `mime_short_name`: A display name (no spaces) for your mime type (e.g. `JSON`, `GeoJSON`). This will be used in the generated Python and npm packages, README, and class names. 34 | * `file_type`: **_OPTIONAL_** A valid file type (e.g. `json`, `geojson`). This will be used to open files of this type with your extension. 35 | * `extension_name`: Your JupyterLab and Jupyter Notebook extension name (e.g. `jupyerlab_json`, `jupyerlab_geojson`). 36 | 37 | ## Project structure 38 | 39 | The project is divided into 2 top-level directories, one for each extension (lab and notebook). 40 | 41 | In most cases, you will only need to edit the `OutputWidget._render` method in `labextension/src/output.js` (for rendering output data of a specific mime type) and the `DocWidget.onUpdateRequest` method in `labextension/src/doc.js` (if your extension should render files of a specific type). 42 | 43 | * `extension_name` 44 | * `extension_name`: The Python package 45 | * `static`: Compiled Javascript for the notebook extension. 46 | * `__init__.py`: Exports paths and metadata of lab and notebook extensions and exports an optional `display` method that can be imported into a notebook and used to easily display data using this renderer 47 | * `src` - The extension source. 48 | * `embed.js`: Entry point for embedded widget 49 | * `extension.js`: Integration point with Jupyter Notebook 50 | * `index.js`: Entry point for the Jupyter Notebook extension 51 | * `plugin.js`: Entry point for the JupyterLab extension 52 | * `renderer.js`: Methods for rendering output data of `mime_type` defined in prompts 53 | 54 | ## Workflow 55 | 56 | This cookiecutter will change over time to adapt to API changes and best practices, so you should be able to rebase your extension from an updated version of the cookiecutter. The following is the recommended workflow: 57 | 58 | ![workflow](workflow.png) 59 | 60 | * Create a branch called `cookiecutter` that will serve as the base for another branch called `develop`. All feature branches shoud be based on `develop` and merged into `master`. 61 | * When a new version of mimerender-cookiecutter is available: 62 | * Checkout `cookiecutter` branch: `git checkout cookiecutter` 63 | * Re-run the cookiecutter command from the repository root: `cookiecutter https://github.com/jupyterlab/mimerender-cookiecutter [--checkout BRANCH_NAME] --output-dir .. --config-file .cookiecutter.yaml --no-input --overwrite-if-exists` 64 | * `--checkout` is optional and specifies a branch of mimerender-cookiecutter to checkout (e.g. `react`). 65 | * `--output-dir ..` allows us to run the cookiecutter in the repo root vs. the parent directory because cookiecutter will always render output in a child directory of the `output-dir`. 66 | * `--config-file .cookiecutter.yaml` will run the cookiecutter with the original inputs which are persisted to `.cookiecutter.yaml` in the repo root. 67 | * `--no-input` will skip the cookiecutter prompts. 68 | * `--overwrite-if-exists` will replace our previous cookiecutter output with the most up-to-date output. 69 | * Commit these changes to `cookiecutter` with a message like "mimerender-cookiecutter [VERSION_NUMBER]": `git commit -m "mimerender-cookiecutter [VERSION_NUMBER]"` 70 | * Checkout the `develop` branch: `git checkout develop` 71 | * Rebase it from `cookiecutter`: `git rebase cookiecutter` 72 | * You may encounter some merge conflicts, so resolve them and complete the rebase using `git rebase --continue` 73 | * Publish a new version of your extension by following the instructions in `RELEASE.md`. 74 | 75 | ## Package names 76 | 77 | We suggest that extension names start with `jupyterlab_` and use underscores if needed to improve readability, such as `jupyterlab_myextension`. 78 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "author_name": "", 3 | "author_email": "", 4 | "mime_type": "application/json", 5 | "use_file_type": ["yes", "no"], 6 | "file_type": "json", 7 | "mime_short_name": "JSON", 8 | "extension_name": "{{ cookiecutter.mime_short_name|lower|replace(' ', '_') }}_renderer" 9 | } 10 | -------------------------------------------------------------------------------- /workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/mimerender-cookiecutter/bb3eeb12865fec33da7f5a0d57b7cc3dc13c2cb0/workflow.png -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/.cookiecutter.yaml: -------------------------------------------------------------------------------- 1 | default_context: 2 | author_name: "{{cookiecutter.author_name}}" 3 | author_email: "{{cookiecutter.author_email}}" 4 | mime_type: "{{cookiecutter.mime_type}}" 5 | file_type: "{{cookiecutter.file_type}}" 6 | mime_short_name: "{{cookiecutter.mime_short_name}}" 7 | extension_name: "{{cookiecutter.extension_name}}" 8 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/.gitignore: -------------------------------------------------------------------------------- 1 | labextension/lib/ 2 | nbextension/lib/ 3 | nbextension/embed/ 4 | node_modules/ 5 | npm-debug.log 6 | *.egg-info/ 7 | build/ 8 | dist/ 9 | 10 | # Compiled javascript 11 | {{cookiecutter.extension_name}}/__pycache__/ 12 | {{cookiecutter.extension_name}}/static/ 13 | 14 | # OS X 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | sudo: false 5 | cache: 6 | pip: true 7 | directories: 8 | - $HOME/.npm 9 | before_install: 10 | - pip install -U pip setuptools 11 | - nvm install 6 12 | install: 13 | - pip install --upgrade ".[test]" -v 14 | script: 15 | - jupyter nbextension list 16 | - jupyter nbextension enable --py --sys-prefix {{ cookiecutter.extension_name }} 17 | - pip install jupyterlab 18 | - jupyter labextension list 19 | - jupyter labextension list 2>&1 | grep -q {{ cookiecutter.extension_name }} 20 | - jupyter labextension link 21 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft {{cookiecutter.extension_name}}/nbextension 2 | include {{cookiecutter.extension_name}}/labextension/*.tgz 3 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.extension_name}} 2 | 3 | A JupyterLab and Jupyter Notebook extension for rendering {{cookiecutter.mime_short_name}} 4 | 5 | ![output renderer](http://g.recordit.co/QAsC7YULcY.gif) 6 | 7 | ## Prerequisites 8 | 9 | * JupyterLab ^0.28.0 and/or Notebook >=4.3.0 10 | 11 | ## Usage 12 | 13 | To render {{cookiecutter.mime_short_name}} output in IPython: 14 | 15 | ```python 16 | from {{cookiecutter.extension_name}} import {{cookiecutter.mime_short_name}} 17 | 18 | {{cookiecutter.mime_short_name}}({ 19 | "string": "string", 20 | "array": [1, 2, 3], 21 | "bool": True, 22 | "object": { 23 | "foo": "bar" 24 | } 25 | }) 26 | ``` 27 | 28 | {%- if cookiecutter.use_file_type == "yes" -%} 29 | To render a `.{{cookiecutter.file_type}}` file as a tree, simply open it: 30 | 31 | ![file renderer](http://g.recordit.co/cbf0xnQHKn.gif) 32 | {% endif %} 33 | 34 | ## Install 35 | 36 | ```bash 37 | pip install {{cookiecutter.extension_name}} 38 | # For JupyterLab 39 | jupyter lab build 40 | # For Notebook 41 | jupyter nbextension enable --py --sys-prefix {{cookiecutter.extension_name}} 42 | ``` 43 | 44 | ## Development 45 | 46 | ```bash 47 | pip install -e . 48 | # For JupyterLab 49 | jupyter labextension link 50 | jupyter lab --watch 51 | # For Notebook 52 | jupyter nbextension install --symlink --py --sys-prefix {{cookiecutter.extension_name}} 53 | jupyter nbextension enable --py --sys-prefix {{cookiecutter.extension_name}} 54 | ``` 55 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a {{cookiecutter.extension_name}} release 2 | 3 | This document guides an extension maintainer through creating and publishing a release of {{cookiecutter.extension_name}}. This process creates a Python source package and a Python universal wheel and uploads them to PyPI. 4 | 5 | ## Update version number 6 | 7 | Update the version number in `_version.py`, and `package.json`. 8 | 9 | Commit your changes, add git tag for this version, and push both commit and tag to your origin/remote repo. 10 | 11 | ## Remove generated files 12 | 13 | Remove old Javascript bundle and Python package builds: 14 | 15 | ```bash 16 | git clean -xfd 17 | ``` 18 | 19 | ## Build the package 20 | 21 | Build the Javascript extension bundle, then build the Python package and wheel: 22 | 23 | ```bash 24 | python setup.py sdist 25 | python setup.py bdist_wheel --universal 26 | ``` 27 | 28 | ## Upload the package 29 | 30 | Upload the Python package and wheel with [twine](https://github.com/pypa/twine). See the Python documentation on [package uploading](https://packaging.python.org/distributing/#uploading-your-project-to-pypi) 31 | for [twine](https://github.com/pypa/twine) setup instructions and for why twine is the recommended uploading method. 32 | 33 | ```bash 34 | twine upload dist/* 35 | ``` 36 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{cookiecutter.extension_name}}", 3 | "version": "0.1.0", 4 | "description": "A package for rendering {{cookiecutter.mime_short_name}} in Jupyter", 5 | "author": "{{cookiecutter.author_name}} <{{cookiecutter.author_email}}>", 6 | "main": "lib/index.js", 7 | "keywords": [ 8 | "jupyter", 9 | "jupyterlab", 10 | "jupyterlab-extension" 11 | ], 12 | "scripts": { 13 | "build:lib": "babel src -d lib", 14 | "build:labextension": "cd {{ cookiecutter.extension_name }} && rimraf labextension && mkdirp labextension && cd labextension && npm pack ../..", 15 | "build:nbextension": "webpack", 16 | "build:all": "npm run build:lib && npm run build:labextension && npm run build:nbextension", 17 | "build": "npm run build:lib", 18 | "prepare": "npm run build", 19 | "watch:nbextension": "webpack --watch", 20 | "watch:lib": "babel src -d lib --watch", 21 | "watch": "npm-run-all -p watch:*" 22 | }, 23 | "babel": { 24 | "presets": ["latest"], 25 | "plugins": ["transform-class-properties"] 26 | }, 27 | "jupyterlab": { 28 | "mimeExtension": "lib/lab_extension" 29 | }, 30 | "dependencies": {}, 31 | "devDependencies": { 32 | "babel-cli": "^6.26.0", 33 | "babel-core": "^6.18.2", 34 | "babel-loader": "^6.4.0", 35 | "babel-preset-latest": "^6.16.0", 36 | "babel-plugin-transform-class-properties": "^6.19.0", 37 | "css-loader": "^0.25.0", 38 | "file-loader": "^0.9.0", 39 | "json-loader": "^0.5.4", 40 | "npm-run-all": "^4.1.1", 41 | "mkdirp": "^0.5.1", 42 | "rimraf": "^2.6.2", 43 | "style-loader": "^0.13.1", 44 | "url-loader": "^0.5.7", 45 | "webpack": "^2.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) {{ cookiecutter.author_name }}. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | from __future__ import print_function 8 | 9 | # the name of the project 10 | name = '{{ cookiecutter.extension_name }}' 11 | 12 | #----------------------------------------------------------------------------- 13 | # Minimal Python version sanity check 14 | #----------------------------------------------------------------------------- 15 | 16 | import sys 17 | 18 | v = sys.version_info 19 | if v[:2] < (3, 3): 20 | # Note: 3.3 is untested, but we'll still allow it 21 | error = "ERROR: %s requires Python version 3.3 or above." % name 22 | print(error, file=sys.stderr) 23 | sys.exit(1) 24 | 25 | #----------------------------------------------------------------------------- 26 | # get on with it 27 | #----------------------------------------------------------------------------- 28 | 29 | import io 30 | import os 31 | from os.path import join as pjoin 32 | from glob import glob 33 | 34 | from setuptools import setup, find_packages 35 | 36 | from setupbase import (create_cmdclass, install_npm, ensure_targets, 37 | combine_commands, expand_data_files) 38 | 39 | here = os.path.abspath(os.path.dirname(__file__)) 40 | nbextension = pjoin(here, name, 'nbextension') 41 | labextension = pjoin(here, name, 'labextension') 42 | 43 | 44 | # Representative files that should exist after a successful build 45 | jstargets = [ 46 | pjoin(nbextension, 'extension.js'), 47 | pjoin(here, 'lib', 'lab_extension.js'), 48 | ] 49 | 50 | version_ns = {} 51 | with io.open(pjoin(here, name, '_version.py'), encoding="utf8") as f: 52 | exec(f.read(), {}, version_ns) 53 | 54 | 55 | cmdclass = create_cmdclass(('jsdeps',)) 56 | cmdclass['jsdeps'] = combine_commands( 57 | install_npm(here, build_cmd='build:all'), 58 | ensure_targets(jstargets), 59 | ) 60 | 61 | 62 | package_data = { 63 | name: [ 64 | 'nbextension/*.*js*', 65 | 'labextension/*.tgz' 66 | ] 67 | } 68 | 69 | data_files = expand_data_files([ 70 | ('share/jupyter/nbextensions/{{ cookiecutter.extension_name }}', [pjoin(nbextension, '*.js*')]), 71 | ('share/jupyter/lab/extensions', [pjoin(labextension, '*.tgz')]) 72 | ]) 73 | 74 | 75 | setup_args = dict( 76 | name = name, 77 | version = version_ns['__version__'], 78 | scripts = glob(pjoin('scripts', '*')), 79 | cmdclass = cmdclass, 80 | packages = find_packages(here), 81 | package_data = package_data, 82 | include_package_data = True, 83 | data_files = data_files, 84 | author = '{{cookiecutter.author_name}}', 85 | author_email = '{{cookiecutter.author_email}}', 86 | url = 'http://jupyter.org', 87 | license = 'BSD', 88 | platforms = "Linux, Mac OS X, Windows", 89 | keywords = ['ipython', 'jupyter'], 90 | classifiers = [ 91 | 'Intended Audience :: Developers', 92 | 'Intended Audience :: System Administrators', 93 | 'Intended Audience :: Science/Research', 94 | 'License :: OSI Approved :: BSD License', 95 | 'Programming Language :: Python', 96 | 'Programming Language :: Python :: 2.7', 97 | 'Programming Language :: Python :: 3', 98 | 'Programming Language :: Python :: 3.3', 99 | 'Programming Language :: Python :: 3.4', 100 | 'Programming Language :: Python :: 3.5', 101 | ], 102 | install_requires = [ 103 | 'notebook>=4.3.0' 104 | ] 105 | ) 106 | 107 | if __name__ == '__main__': 108 | setup(**setup_args) 109 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/setupbase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Jupyter Development Team. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | """ 8 | This file originates from the 'jupyter-packaging' package, and 9 | contains a set of useful utilities for including npm packages 10 | within a Python package. 11 | """ 12 | 13 | import os 14 | from os.path import join as pjoin 15 | import functools 16 | import pipes 17 | import sys 18 | from glob import glob 19 | from subprocess import check_call 20 | 21 | from setuptools import Command 22 | from setuptools.command.build_py import build_py 23 | from setuptools.command.sdist import sdist 24 | from setuptools.command.develop import develop 25 | from setuptools.command.bdist_egg import bdist_egg 26 | from distutils import log 27 | 28 | try: 29 | from wheel.bdist_wheel import bdist_wheel 30 | except ImportError: 31 | bdist_wheel = None 32 | 33 | if sys.platform == 'win32': 34 | from subprocess import list2cmdline 35 | else: 36 | def list2cmdline(cmd_list): 37 | return ' '.join(map(pipes.quote, cmd_list)) 38 | 39 | 40 | __version__ = '0.1.0' 41 | 42 | # --------------------------------------------------------------------------- 43 | # Top Level Variables 44 | # --------------------------------------------------------------------------- 45 | 46 | here = os.path.dirname(__file__) 47 | is_repo = os.path.exists(pjoin(here, '.git')) 48 | node_modules = pjoin(here, 'node_modules') 49 | 50 | npm_path = ':'.join([ 51 | pjoin(here, 'node_modules', '.bin'), 52 | os.environ.get('PATH', os.defpath), 53 | ]) 54 | 55 | if "--skip-npm" in sys.argv: 56 | print("Skipping npm install as requested.") 57 | skip_npm = True 58 | sys.argv.remove("--skip-npm") 59 | else: 60 | skip_npm = False 61 | 62 | # --------------------------------------------------------------------------- 63 | # Public Functions 64 | # --------------------------------------------------------------------------- 65 | 66 | 67 | def expand_data_files(data_file_patterns): 68 | """Expand data file patterns to a valid data_files spec. 69 | 70 | Parameters 71 | ----------- 72 | data_file_patterns: list(tuple) 73 | A list of (directory, glob patterns) for the data file locations. 74 | The globs themselves do not recurse. 75 | """ 76 | data_files = [] 77 | for (directory, patterns) in data_file_patterns: 78 | files = [] 79 | for p in patterns: 80 | files.extend([os.path.relpath(f, here) for f in glob(p)]) 81 | data_files.append((directory, files)) 82 | return data_files 83 | 84 | 85 | def find_packages(top): 86 | """ 87 | Find all of the packages. 88 | """ 89 | packages = [] 90 | for d, dirs, _ in os.walk(top, followlinks=True): 91 | if os.path.exists(pjoin(d, '__init__.py')): 92 | packages.append(os.path.relpath(d, top).replace(os.path.sep, '.')) 93 | elif d != top: 94 | # Do not look for packages in subfolders if current is not a package 95 | dirs[:] = [] 96 | return packages 97 | 98 | 99 | def update_package_data(distribution): 100 | """update build_py options to get package_data changes""" 101 | build_py = distribution.get_command_obj('build_py') 102 | build_py.finalize_options() 103 | 104 | 105 | def create_cmdclass(wrappers=None): 106 | """Create a command class with the given optional wrappers. 107 | 108 | Parameters 109 | ---------- 110 | wrappers: list(str), optional 111 | The cmdclass names to run before running other commands 112 | """ 113 | egg = bdist_egg if 'bdist_egg' in sys.argv else bdist_egg_disabled 114 | wrappers = wrappers or [] 115 | wrapper = functools.partial(wrap_command, wrappers) 116 | cmdclass = dict( 117 | build_py=wrapper(build_py, strict=is_repo), 118 | sdist=wrapper(sdist, strict=True), 119 | bdist_egg=egg, 120 | develop=wrapper(develop, strict=True) 121 | ) 122 | if bdist_wheel: 123 | cmdclass['bdist_wheel'] = wrapper(bdist_wheel, strict=True) 124 | return cmdclass 125 | 126 | 127 | def run(cmd, *args, **kwargs): 128 | """Echo a command before running it. Defaults to repo as cwd""" 129 | log.info('> ' + list2cmdline(cmd)) 130 | kwargs.setdefault('cwd', here) 131 | kwargs.setdefault('shell', sys.platform == 'win32') 132 | if not isinstance(cmd, list): 133 | cmd = cmd.split() 134 | return check_call(cmd, *args, **kwargs) 135 | 136 | 137 | def is_stale(target, source): 138 | """Test whether the target file/directory is stale based on the source 139 | file/directory. 140 | """ 141 | if not os.path.exists(target): 142 | return True 143 | target_mtime = recursive_mtime(target) or 0 144 | return compare_recursive_mtime(source, cutoff=target_mtime) 145 | 146 | 147 | class BaseCommand(Command): 148 | """Empty command because Command needs subclasses to override too much""" 149 | user_options = [] 150 | 151 | def initialize_options(self): 152 | pass 153 | 154 | def finalize_options(self): 155 | pass 156 | 157 | def get_inputs(self): 158 | return [] 159 | 160 | def get_outputs(self): 161 | return [] 162 | 163 | 164 | def combine_commands(*commands): 165 | """Return a Command that combines several commands.""" 166 | 167 | class CombinedCommand(Command): 168 | 169 | def initialize_options(self): 170 | self.commands = [] 171 | for C in commands: 172 | self.commands.append(C(self.distribution)) 173 | for c in self.commands: 174 | c.initialize_options() 175 | 176 | def finalize_options(self): 177 | for c in self.commands: 178 | c.finalize_options() 179 | 180 | def run(self): 181 | for c in self.commands: 182 | c.run() 183 | return CombinedCommand 184 | 185 | 186 | def compare_recursive_mtime(path, cutoff, newest=True): 187 | """Compare the newest/oldest mtime for all files in a directory. 188 | 189 | Cutoff should be another mtime to be compared against. If an mtime that is 190 | newer/older than the cutoff is found it will return True. 191 | E.g. if newest=True, and a file in path is newer than the cutoff, it will 192 | return True. 193 | """ 194 | if os.path.isfile(path): 195 | mt = mtime(path) 196 | if newest: 197 | if mt > cutoff: 198 | return True 199 | elif mt < cutoff: 200 | return True 201 | for dirname, _, filenames in os.walk(path, topdown=False): 202 | for filename in filenames: 203 | mt = mtime(pjoin(dirname, filename)) 204 | if newest: # Put outside of loop? 205 | if mt > cutoff: 206 | return True 207 | elif mt < cutoff: 208 | return True 209 | return False 210 | 211 | 212 | def recursive_mtime(path, newest=True): 213 | """Gets the newest/oldest mtime for all files in a directory.""" 214 | if os.path.isfile(path): 215 | return mtime(path) 216 | current_extreme = None 217 | for dirname, _, filenames in os.walk(path, topdown=False): 218 | for filename in filenames: 219 | mt = mtime(pjoin(dirname, filename)) 220 | if newest: # Put outside of loop? 221 | if mt >= (current_extreme or mt): 222 | current_extreme = mt 223 | elif mt <= (current_extreme or mt): 224 | current_extreme = mt 225 | return current_extreme 226 | 227 | 228 | def mtime(path): 229 | """shorthand for mtime""" 230 | return os.stat(path).st_mtime 231 | 232 | 233 | def install_npm(path=None, build_dir=None, source_dir=None, build_cmd='build', force=False): 234 | """Return a Command for managing an npm installation. 235 | 236 | Note: The command is skipped if the `--skip-npm` flag is used. 237 | 238 | Parameters 239 | ---------- 240 | path: str, optional 241 | The base path of the node package. Defaults to the repo root. 242 | build_dir: str, optional 243 | The target build directory. If this and source_dir are given, 244 | the JavaScript will only be build if necessary. 245 | source_dir: str, optional 246 | The source code directory. 247 | build_cmd: str, optional 248 | The npm command to build assets to the build_dir. 249 | """ 250 | 251 | class NPM(BaseCommand): 252 | description = 'install package.json dependencies using npm' 253 | 254 | def run(self): 255 | if skip_npm: 256 | log.info('Skipping npm-installation') 257 | return 258 | node_package = path or here 259 | node_modules = pjoin(node_package, 'node_modules') 260 | 261 | if not which("npm"): 262 | log.error("`npm` unavailable. If you're running this command " 263 | "using sudo, make sure `npm` is availble to sudo") 264 | return 265 | if force or is_stale(node_modules, pjoin(node_package, 'package.json')): 266 | log.info('Installing build dependencies with npm. This may ' 267 | 'take a while...') 268 | run(['npm', 'install'], cwd=node_package) 269 | if build_dir and source_dir and not force: 270 | should_build = is_stale(build_dir, source_dir) 271 | else: 272 | should_build = True 273 | if should_build: 274 | run(['npm', 'run', build_cmd], cwd=node_package) 275 | 276 | return NPM 277 | 278 | 279 | def ensure_targets(targets): 280 | """Return a Command that checks that certain files exist. 281 | 282 | Raises a ValueError if any of the files are missing. 283 | 284 | Note: The check is skipped if the `--skip-npm` flag is used. 285 | """ 286 | 287 | class TargetsCheck(BaseCommand): 288 | def run(self): 289 | if skip_npm: 290 | log.info('Skipping target checks') 291 | return 292 | missing = [t for t in targets if not os.path.exists(t)] 293 | if missing: 294 | raise ValueError(('missing files: %s' % missing)) 295 | 296 | return TargetsCheck 297 | 298 | 299 | # `shutils.which` function copied verbatim from the Python-3.3 source. 300 | def which(cmd, mode=os.F_OK | os.X_OK, path=None): 301 | """Given a command, mode, and a PATH string, return the path which 302 | conforms to the given mode on the PATH, or None if there is no such 303 | file. 304 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result 305 | of os.environ.get("PATH"), or can be overridden with a custom search 306 | path. 307 | """ 308 | 309 | # Check that a given file can be accessed with the correct mode. 310 | # Additionally check that `file` is not a directory, as on Windows 311 | # directories pass the os.access check. 312 | def _access_check(fn, mode): 313 | return (os.path.exists(fn) and os.access(fn, mode) and 314 | not os.path.isdir(fn)) 315 | 316 | # Short circuit. If we're given a full path which matches the mode 317 | # and it exists, we're done here. 318 | if _access_check(cmd, mode): 319 | return cmd 320 | 321 | path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep) 322 | 323 | if sys.platform == "win32": 324 | # The current directory takes precedence on Windows. 325 | if os.curdir not in path: 326 | path.insert(0, os.curdir) 327 | 328 | # PATHEXT is necessary to check on Windows. 329 | pathext = os.environ.get("PATHEXT", "").split(os.pathsep) 330 | # See if the given file matches any of the expected path extensions. 331 | # This will allow us to short circuit when given "python.exe". 332 | matches = [cmd for ext in pathext if cmd.lower().endswith(ext.lower())] 333 | # If it does match, only test that one, otherwise we have to try 334 | # others. 335 | files = [cmd] if matches else [cmd + ext.lower() for ext in pathext] 336 | else: 337 | # On other platforms you don't have things like PATHEXT to tell you 338 | # what file suffixes are executable, so just pass on cmd as-is. 339 | files = [cmd] 340 | 341 | seen = set() 342 | for dir in path: 343 | dir = os.path.normcase(dir) 344 | if dir not in seen: 345 | seen.add(dir) 346 | for thefile in files: 347 | name = os.path.join(dir, thefile) 348 | if _access_check(name, mode): 349 | return name 350 | return None 351 | 352 | 353 | # --------------------------------------------------------------------------- 354 | # Private Functions 355 | # --------------------------------------------------------------------------- 356 | 357 | 358 | def wrap_command(cmds, cls, strict=True): 359 | """Wrap a setup command 360 | 361 | Parameters 362 | ---------- 363 | cmds: list(str) 364 | The names of the other commands to run prior to the command. 365 | strict: boolean, optional 366 | Wether to raise errors when a pre-command fails. 367 | """ 368 | class WrappedCommand(cls): 369 | 370 | def run(self): 371 | if not getattr(self, 'uninstall', None): 372 | try: 373 | [self.run_command(cmd) for cmd in cmds] 374 | except Exception: 375 | if strict: 376 | raise 377 | else: 378 | pass 379 | 380 | result = cls.run(self) 381 | # update package data 382 | update_package_data(self.distribution) 383 | return result 384 | return WrappedCommand 385 | 386 | 387 | class bdist_egg_disabled(bdist_egg): 388 | """Disabled version of bdist_egg 389 | Prevents setup.py install performing setuptools' default easy_install, 390 | which it should never ever do. 391 | """ 392 | 393 | def run(self): 394 | sys.exit("Aborting implicit building of eggs. Use `pip install .` " + 395 | " to install from source.") 396 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/src/embed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for the unpkg bundle containing custom model definitions. 3 | * 4 | * It differs from the notebook bundle in that it does not need to define a 5 | * dynamic baseURL for the static assets and may load some css that would 6 | * already be loaded by the notebook otherwise. 7 | */ 8 | 9 | export { version } from '../package.json'; 10 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/src/index.js: -------------------------------------------------------------------------------- 1 | 2 | export { register_renderer, render_cells } from './renderer.js'; 3 | export { version } from '../package.json'; 4 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/src/lab_extension.js: -------------------------------------------------------------------------------- 1 | import { 2 | Widget 3 | } from '@phosphor/widgets'; 4 | 5 | import '../style/index.css'; 6 | 7 | 8 | /** 9 | * The default mime type for the extension. 10 | */ 11 | const MIME_TYPE = '{{cookiecutter.mime_type}}'; 12 | 13 | 14 | /** 15 | * The class name added to the extension. 16 | */ 17 | const CLASS_NAME = 'jp-OutputWidget{{cookiecutter.mime_short_name}}'; 18 | 19 | 20 | /** 21 | * A widget for rendering {{cookiecutter.mime_short_name}}. 22 | */ 23 | export 24 | class OutputWidget extends Widget { 25 | /** 26 | * Construct a new output widget. 27 | */ 28 | constructor(options) { 29 | super(); 30 | this._mimeType = options.mimeType; 31 | this.addClass(CLASS_NAME); 32 | } 33 | 34 | /** 35 | * Render {{cookiecutter.mime_short_name}} into this widget's node. 36 | */ 37 | renderModel(model) { 38 | this.node.textContent = model.data[this._mimeType]; 39 | } 40 | } 41 | 42 | 43 | /** 44 | * A mime renderer factory for {{cookiecutter.mime_short_name}} data. 45 | */ 46 | export 47 | const rendererFactory = { 48 | safe: true, 49 | mimeTypes: [MIME_TYPE], 50 | createRenderer: options => new OutputWidget(options) 51 | }; 52 | 53 | 54 | const extension = { 55 | name: '{{cookiecutter.mime_short_name}}', 56 | rendererFactory, 57 | rank: 0, 58 | dataType: 'json', 59 | {%- if cookiecutter.use_file_type == "yes" -%} 60 | documentWidgetFactoryOptions: { 61 | name: '{{cookiecutter.mime_short_name}}', 62 | primaryFileType: '{{cookiecutter.file_type}}', 63 | fileTypes: ['{{cookiecutter.file_type}}'], 64 | defaultFor: [] 65 | }, 66 | fileTypes: [] 67 | {% endif %} 68 | }; 69 | 70 | export default extension; 71 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/src/nb_extension.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the javascript that is run when the notebook is loaded. 3 | * It contains some requirejs configuration and the `load_ipython_extension` 4 | * which is required for any notebook extension. 5 | */ 6 | 7 | /** 8 | * Configure requirejs. 9 | */ 10 | if (window.require) { 11 | window.require.config({ 12 | map: { 13 | '*': { 14 | '{{cookiecutter.extension_name}}': 'nbextensions/{{cookiecutter.extension_name}}/index' 15 | } 16 | } 17 | }); 18 | } 19 | 20 | /** 21 | * Export the required load_ipython_extention. 22 | */ 23 | export function load_ipython_extension() { 24 | define( 25 | ['nbextensions/{{cookiecutter.extension_name}}/index', 'base/js/namespace'], 26 | (Extension, Jupyter) => { 27 | const { notebook } = Jupyter; 28 | Extension.register_renderer(notebook); 29 | Extension.render_cells(notebook); 30 | } 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/src/nb_index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for the notebook bundle containing custom model definitions. 3 | * Setup notebook base URL 4 | * Some static assets may be required by the custom widget javascript. The base 5 | * url for the notebook is not known at build time and is therefore computed 6 | * dynamically. 7 | */ 8 | 9 | __webpack_public_path__ = document 10 | .querySelector('body') 11 | .getAttribute('data-base-url') + 12 | 'nbextensions/{{cookiecutter.extension_name}}/'; 13 | 14 | /** 15 | * Export widget models and views, and the npm package version number. 16 | */ 17 | export { register_renderer, render_cells } from './renderer.js'; 18 | export { version } from '../package.json'; 19 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/src/renderer.js: -------------------------------------------------------------------------------- 1 | import '../style/index.css'; 2 | 3 | const MIME_TYPE = '{{cookiecutter.mime_type}}'; 4 | const CLASS_NAME = 'output_{{cookiecutter.mime_short_name}} rendered_html'; 5 | 6 | /** 7 | * Render data to the DOM node 8 | */ 9 | function render(props, node) { 10 | const text = document.createTextNode(JSON.stringify(props.data)); 11 | node.appendChild(text); 12 | } 13 | 14 | /** 15 | * Handle when an output is cleared or removed 16 | */ 17 | function handleClearOutput(event, { cell: { output_area } }) { 18 | /* Get rendered DOM node */ 19 | const toinsert = output_area.element.find(CLASS_NAME.split(' ')[0]); 20 | /* e.g. Dispose of resources used by renderer library */ 21 | // if (toinsert) renderLibrary.dispose(toinsert[0]); 22 | } 23 | 24 | /** 25 | * Handle when a new output is added 26 | */ 27 | function handleAddOutput(event, { output, output_area }) { 28 | /* Get rendered DOM node */ 29 | const toinsert = output_area.element.find(CLASS_NAME.split(' ')[0]); 30 | /** e.g. Inject a static image representation into the mime bundle for 31 | * endering on Github, etc. 32 | */ 33 | // if (toinsert) { 34 | // renderLibrary.toPng(toinsert[0]).then(url => { 35 | // const data = url.split(',')[1]; 36 | // output_area.outputs 37 | // .filter(output => output.data[MIME_TYPE]) 38 | // .forEach(output => { 39 | // output.data['image/png'] = data; 40 | // }); 41 | // }); 42 | // } 43 | } 44 | 45 | /** 46 | * Register the mime type and append_mime function with the notebook's 47 | * output area 48 | */ 49 | export function register_renderer(notebook) { 50 | /* Get an instance of output_area from a CodeCell instance */ 51 | const { output_area } = notebook 52 | .get_cells() 53 | .reduce((result, cell) => cell.output_area ? cell : result, {}); 54 | 55 | /* A function to render output of '{{cookiecutter.mime_type}}' mime type */ 56 | const append_mime = function(data, metadata, element) { 57 | /* Create a DOM node to render to */ 58 | const toinsert = this.create_output_subarea( 59 | metadata, 60 | CLASS_NAME, 61 | MIME_TYPE 62 | ); 63 | this.keyboard_manager.register_events(toinsert); 64 | /* Render data to DOM node */ 65 | const props = { data, metadata: metadata[MIME_TYPE] }; 66 | render(props, toinsert[0]); 67 | element.append(toinsert); 68 | return toinsert; 69 | }; 70 | 71 | /* Handle when an output is cleared or removed */ 72 | output_area.events.on('clear_output.CodeCell', handleClearOutput); 73 | 74 | /* Handle when a new output is added */ 75 | output_area.events.on('output_added.OutputArea', handleAddOutput); 76 | 77 | /** 78 | * Calculate the index of this renderer in `output_area.display_order` 79 | * e.g. Insert this renderer after any renderers with mime type that matches 80 | * "+json" 81 | */ 82 | // const mime_types = output_area.mime_types(); 83 | // const json_types = mime_types.filter(mimetype => mimetype.includes('+json')); 84 | // const index = mime_types.lastIndexOf(json_types.pop() + 1); 85 | 86 | /* ...or just insert it at the top */ 87 | const index = 0; 88 | 89 | /** 90 | * Register the mime type and append_mim function with output_area 91 | */ 92 | output_area.register_mime_type(MIME_TYPE, append_mime, { 93 | /* Is output safe? */ 94 | safe: true, 95 | /* Index of renderer in `output_area.display_order` */ 96 | index: index 97 | }); 98 | } 99 | 100 | /** 101 | * Re-render cells with output data of '{{cookiecutter.mime_type}}' mime type 102 | */ 103 | export function render_cells(notebook) { 104 | /* Get all cells in notebook */ 105 | notebook.get_cells().forEach(cell => { 106 | /* If a cell has output data of '{{cookiecutter.mime_type}}' mime type */ 107 | if ( 108 | cell.output_area && 109 | cell.output_area.outputs.find( 110 | output => output.data && output.data[MIME_TYPE] 111 | ) 112 | ) { 113 | /* Re-render the cell */ 114 | notebook.render_cell_output(cell); 115 | } 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/style/index.css: -------------------------------------------------------------------------------- 1 | 2 | .jp-OutputWidget{{cookiecutter.mime_short_name}} { 3 | 4 | } 5 | 6 | 7 | div.output_subarea.output_{{cookiecutter.mime_short_name}} { 8 | padding: 0.4em 0; 9 | max-width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var version = require('./package.json').version; 3 | 4 | /** 5 | * Custom webpack loaders are generally the same for all webpack bundles, hence 6 | * stored in a separate local variable. 7 | */ 8 | var loaders = [ 9 | { 10 | test: /\.js$/, 11 | include: [path.join(__dirname, 'src')], 12 | loader: 'babel-loader', 13 | query: { presets: ['latest'] } 14 | }, 15 | { test: /\.json$/, loader: 'json-loader' }, 16 | { test: /\.css$/, loader: 'style-loader!css-loader' }, 17 | { test: /\.html$/, loader: 'file-loader' }, 18 | { test: /\.(jpg|png|gif)$/, loader: 'file-loader' }, 19 | { 20 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 21 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 22 | }, 23 | { 24 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 25 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 26 | }, 27 | { 28 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 29 | loader: 'url-loader?limit=10000&mimetype=application/octet-stream' 30 | }, 31 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' }, 32 | { 33 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 34 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml' 35 | } 36 | ]; 37 | 38 | var base = { 39 | output: { 40 | libraryTarget: 'amd', 41 | devtoolModuleFilenameTemplate: 'webpack:///[absolute-resource-path]' 42 | }, 43 | devtool: 'source-map', 44 | module: { loaders }, 45 | }; 46 | 47 | module.exports = [ 48 | /** 49 | * Notebook extension 50 | * 51 | * This bundle only contains the part of the JavaScript that is run on 52 | * load of the notebook. This section generally only performs 53 | * some configuration for requirejs, and provides the legacy 54 | * "load_ipython_extension" function which is required for any notebook 55 | * extension. 56 | */ 57 | Object.assign({}, base, { 58 | entry: path.join(__dirname, 'src', 'nb_extension.js'), 59 | output: Object.assign({}, base.output, { 60 | filename: 'extension.js', 61 | path: path.join( 62 | __dirname, 63 | '{{cookiecutter.extension_name}}', 64 | 'nbextension' 65 | ) 66 | }), 67 | externals: [ 68 | 'nbextensions/{{cookiecutter.extension_name}}/index', 69 | 'base/js/namespace' 70 | ] 71 | }), 72 | /** 73 | * This bundle contains the implementation of the extension. 74 | * 75 | * It must be an amd module 76 | */ 77 | Object.assign({}, base, { 78 | entry: path.join(__dirname, 'src', 'nb_index.js'), 79 | output: Object.assign({}, base.output, { 80 | filename: 'index.js', 81 | path: path.join( 82 | __dirname, 83 | '{{cookiecutter.extension_name}}', 84 | 'nbextension' 85 | ) 86 | }) 87 | }) 88 | ]; 89 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/{{cookiecutter.extension_name}}/__init__.py: -------------------------------------------------------------------------------- 1 | from IPython.display import display, JSON 2 | import json 3 | 4 | # Running `npm run build` will create static resources in the static 5 | # directory of this Python package (and create that directory if necessary). 6 | 7 | def _jupyter_nbextension_paths(): 8 | return [{ 9 | 'section': 'notebook', 10 | 'src': 'static', 11 | 'dest': '{{cookiecutter.extension_name}}', 12 | 'require': '{{cookiecutter.extension_name}}/extension' 13 | }] 14 | 15 | # A display class that can be used within a notebook. 16 | # from {{cookiecutter.extension_name}} import {{cookiecutter.mime_short_name}} 17 | # {{cookiecutter.mime_short_name}}(data) 18 | 19 | class {{cookiecutter.mime_short_name}}(JSON): 20 | """A display class for displaying {{cookiecutter.mime_short_name}} visualizations in the Jupyter Notebook and IPython kernel. 21 | 22 | {{cookiecutter.mime_short_name}} expects a JSON-able dict, not serialized JSON strings. 23 | 24 | Scalar types (None, number, string) are not allowed, only dict containers. 25 | """ 26 | 27 | def _ipython_display_(self): 28 | bundle = { 29 | '{{cookiecutter.mime_type}}': self.data, 30 | 'text/plain': '<{{cookiecutter.extension_name}}.{{cookiecutter.mime_short_name}} object>' 31 | } 32 | metadata = { 33 | '{{cookiecutter.mime_type}}': self.metadata 34 | } 35 | display(bundle, metadata=metadata, raw=True) 36 | -------------------------------------------------------------------------------- /{{cookiecutter.extension_name}}/{{cookiecutter.extension_name}}/_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) {{ cookiecutter.author_name }}. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | version_info = (0, 1, 0, 'dev') 8 | __version__ = ".".join(map(str, version_info)) 9 | --------------------------------------------------------------------------------