├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── _static │ ├── sphinx-argparse.css │ └── theme.css ├── api_reference │ ├── internal │ │ ├── modules.rst │ │ └── snakeface.rst │ └── snakeface.rst ├── conf.py ├── getting_started │ ├── auth.rst │ ├── example_workflow.rst │ ├── index.rst │ ├── installation.rst │ ├── notebook.rst │ └── settings.rst ├── images │ ├── after-notebook-login.png │ ├── dashboard.png │ ├── new_workflow.png │ ├── notebook-login.png │ ├── workflow-detail.png │ └── workflow-table.png ├── index.rst ├── requirements.txt └── use_cases │ ├── img │ └── template-404.png │ └── index.rst ├── img ├── snakeface.png └── snakeface.xcf ├── main.py ├── manage.py ├── requirements.txt ├── setup.cfg ├── setup.py └── snakeface ├── __init__.py ├── apps ├── __init__.py ├── api │ ├── __init__.py │ ├── apps.py │ ├── permissions.py │ ├── urls.py │ └── views.py ├── base │ ├── __init__.py │ ├── apps.py │ ├── static │ │ ├── css │ │ │ ├── bootstrap.min.css │ │ │ ├── custom.css │ │ │ ├── demo.css │ │ │ ├── font-awesome.min.css │ │ │ ├── light-bootstrap-dashboard.css │ │ │ ├── theme-emerald.css │ │ │ └── themify-icons.css │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ ├── fontawesome-webfont.woff2 │ │ │ ├── nucleo-icons.eot │ │ │ ├── nucleo-icons.svg │ │ │ ├── nucleo-icons.ttf │ │ │ ├── nucleo-icons.woff │ │ │ ├── nucleo-icons.woff2 │ │ │ └── themify.woff │ │ ├── img │ │ │ ├── clusterb.png │ │ │ ├── compute_server.png │ │ │ ├── favicon.ico │ │ │ ├── grid2.png │ │ │ ├── hero.png │ │ │ ├── logo.png │ │ │ ├── logo_square.png │ │ │ ├── logob.png │ │ │ ├── transparency.jpg │ │ │ └── workstation.png │ │ └── js │ │ │ ├── ScrollToPlugin.min.js │ │ │ ├── bootstrap-notify.js │ │ │ ├── bootstrap-switch.js │ │ │ ├── bootstrap.min.js │ │ │ ├── bootstrap.min.js.map │ │ │ ├── chartist.min.js │ │ │ ├── flexslider.min.js │ │ │ ├── jquery-3.5.1.min.js │ │ │ ├── jquery-3.5.1.slim.min.js │ │ │ ├── light-bootstrap-dashboard.js │ │ │ ├── popper.min.js │ │ │ ├── popper.min.js.map │ │ │ └── tether.min.js │ ├── templates │ │ ├── base │ │ │ ├── base.html │ │ │ ├── footer.html │ │ │ ├── ga.html │ │ │ ├── loader.html │ │ │ ├── navigation.html │ │ │ ├── page.html │ │ │ └── robots.txt │ │ └── messages │ │ │ ├── message.html │ │ │ └── notification.html │ ├── templatetags │ │ └── my_filters.py │ ├── urls.py │ └── views.py ├── main │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── consumers.py │ ├── forms.py │ ├── models.py │ ├── routing.py │ ├── tasks.py │ ├── templates │ │ ├── fields │ │ │ └── status.html │ │ ├── forms │ │ │ ├── boolean_field.html │ │ │ ├── choice_field.html │ │ │ └── text_field.html │ │ ├── main │ │ │ └── index.html │ │ └── workflows │ │ │ ├── detail.html │ │ │ ├── new.html │ │ │ ├── report.html │ │ │ └── workflow_run_table.html │ ├── urls.py │ ├── utils.py │ └── views.py └── users │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── decorators.py │ ├── forms.py │ ├── models.py │ ├── templates │ └── login │ │ └── notebook.html │ ├── urls.py │ ├── utils.py │ └── views.py ├── argparser.py ├── asgi.py ├── client.py ├── context_processors.py ├── logger.py ├── settings.py ├── settings.yml ├── urls.py ├── version.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | backup/ 2 | secret_key.py 3 | app.yaml 4 | app-dev.yaml 5 | *.pyc 6 | *~ 7 | .idea/* 8 | .DS_Store 9 | .env 10 | __pycache__ 11 | static/ 12 | env/ 13 | db.sqlite3 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | This is a manually generated log to track changes to the repository for each release. 4 | Each section should include general headers such as **Implemented enhancements** 5 | and **Merged pull requests**. All closed issued and bug fixes should be 6 | represented by the pull requests that fixed them. Critical items to know are: 7 | 8 | - renamed commands 9 | - deprecated / removed commands 10 | - changed defaults 11 | - backward incompatible changes 12 | 13 | 14 | ## [master](https://github.com/snakemake/snakeface/tree/main) (master) 15 | - removing erroneous variables (0.0.18) 16 | - fixing async bug and adding missing template file (0.0.17) 17 | - removing extra dependencies for API (0.0.16) 18 | - settings file needs to be within install location (0.0.15) 19 | - adding missing settings.yml to install directory (0.0.14) 20 | - adding MANIFEST.in file (0.0.13) 21 | - adding missing social auth libraries (0.0.12) 22 | - early release with snakeface notebooks (0.0.11) 23 | - skeleton release (0.0.0) 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE CHANGELOG.md settings.yml 2 | recursive-include snakeface * 3 | prune snakeface/plugins 4 | recursive-exclude snakeface secret_key.py db.sqlite3 5 | recursive-exclude * migrations 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.pyc 8 | recursive-exclude * *.pyo 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snakemake Interface (snakeface) 2 | 3 | ![img/snakeface.png](img/snakeface.png) 4 | 5 | Snakeface is an interface for snakemake. If you are an individual, you can run 6 | it locally as a notebook to run and manage workflows. If you are an institution or group, 7 | you (will be able to) deploy a shared instance for others to use. To get started, please see 8 | the [documentation](https://snakemake.github.io/snakeface) (not created yet). 9 | 10 | **Snakeface is under development!** We are developing deployment types and features 11 | as they are requested. Currently, a single user notebook deployment is supported. 12 | Please [open an issue](https://github.com/snakemake/snakeface/issues) to request a feature, bug fix, or different deployment type. 13 | See the [documentation](https://snakemake.github.io/snakeface/) to get started. 14 | 15 | ## Thanks 16 | 17 | Snakeface wouldn't be possible without several open source libraries! 18 | 19 | - [Django](https://github.com/django/django) "The web framework for perfectionists" (and dinosaurs) 20 | 21 | ## License 22 | 23 | * Free software: MPL 2.0 License 24 | 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Snakemake.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Snakemake.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Snakemake" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Snakemake" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/sphinx-argparse.css: -------------------------------------------------------------------------------- 1 | .wy-table-responsive table td { 2 | white-space: normal !important; 3 | } 4 | .wy-table-responsive { 5 | overflow: visible !important; 6 | } 7 | -------------------------------------------------------------------------------- /docs/api_reference/internal/modules.rst: -------------------------------------------------------------------------------- 1 | Internal API 2 | ============ 3 | 4 | These pages document the entire internal API of Snakeface. 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | snakeface 10 | -------------------------------------------------------------------------------- /docs/api_reference/internal/snakeface.rst: -------------------------------------------------------------------------------- 1 | snakeface package 2 | ================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | snakeface.argparser module 8 | -------------------------- 9 | 10 | .. automodule:: snakeface.argparser 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | snakeface.client module 16 | ----------------------- 17 | 18 | .. automodule:: snakeface.client 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | snakeface.apps.api module 24 | ------------------------- 25 | 26 | .. automodule:: snakeface.apps.api.permissions 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | .. automodule:: snakeface.apps.api.views 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | .. automodule:: snakeface.apps.api.urls 37 | :members: 38 | :undoc-members: 39 | :show-inheritance: 40 | 41 | 42 | snakeface.apps.main module 43 | -------------------------- 44 | 45 | .. automodule:: snakeface.apps.main.consumers 46 | :members: 47 | :undoc-members: 48 | :show-inheritance: 49 | 50 | .. automodule:: snakeface.apps.main.forms 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | .. automodule:: snakeface.apps.main.models 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | .. automodule:: snakeface.apps.main.routing 61 | :members: 62 | :undoc-members: 63 | :show-inheritance: 64 | 65 | .. automodule:: snakeface.apps.main.tasks 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | .. automodule:: snakeface.apps.main.urls 71 | :members: 72 | :undoc-members: 73 | :show-inheritance: 74 | 75 | .. automodule:: snakeface.apps.main.utils 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | 80 | .. automodule:: snakeface.apps.main.views 81 | :members: 82 | :undoc-members: 83 | :show-inheritance: 84 | 85 | 86 | snakeface.apps.base module 87 | -------------------------- 88 | 89 | .. automodule:: snakeface.apps.base.urls 90 | :members: 91 | :undoc-members: 92 | :show-inheritance: 93 | 94 | .. automodule:: snakeface.apps.base.views 95 | :members: 96 | :undoc-members: 97 | :show-inheritance: 98 | 99 | 100 | snakeface.apps.users module 101 | --------------------------- 102 | 103 | .. automodule:: snakeface.apps.users.decorators 104 | :members: 105 | :undoc-members: 106 | :show-inheritance: 107 | 108 | .. automodule:: snakeface.apps.users.forms 109 | :members: 110 | :undoc-members: 111 | :show-inheritance: 112 | 113 | .. automodule:: snakeface.apps.users.models 114 | :members: 115 | :undoc-members: 116 | :show-inheritance: 117 | 118 | .. automodule:: snakeface.apps.users.urls 119 | :members: 120 | :undoc-members: 121 | :show-inheritance: 122 | 123 | .. automodule:: snakeface.apps.users.utils 124 | :members: 125 | :undoc-members: 126 | :show-inheritance: 127 | 128 | .. automodule:: snakeface.apps.users.views 129 | :members: 130 | :undoc-members: 131 | :show-inheritance: 132 | 133 | 134 | snakeface.settings module 135 | ------------------------- 136 | 137 | .. automodule:: snakeface.settings 138 | :members: 139 | :undoc-members: 140 | :show-inheritance: 141 | 142 | 143 | snakeface.logger module 144 | ------------------------ 145 | 146 | .. automodule:: snakeface.logger 147 | :members: 148 | :undoc-members: 149 | :show-inheritance: 150 | 151 | -------------------------------------------------------------------------------- /docs/api_reference/snakeface.rst: -------------------------------------------------------------------------------- 1 | .. _api_reference_snakeface: 2 | 3 | The Snakeface API 4 | ================= 5 | 6 | These sections detail the internal functions for Snakeface. 7 | 8 | .. automodule:: snakeface 9 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Snakemake documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Feb 1 16:01:02 2014. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | from recommonmark.parser import CommonMarkParser 19 | 20 | source_parsers = {".md": CommonMarkParser} 21 | 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | sys.path.insert(0, os.path.abspath("../")) 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | "sphinx.ext.autodoc", 38 | "sphinx.ext.mathjax", 39 | "sphinx.ext.viewcode", 40 | "sphinxcontrib.napoleon", 41 | "sphinxarg.ext", 42 | "sphinx.ext.autosectionlabel", 43 | ] 44 | 45 | # Snakmake theme (made by SciAni). 46 | html_css_files = ["theme.css"] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ["_templates"] 50 | 51 | # The suffix of source filenames. 52 | source_suffix = [".rst", ".md"] 53 | 54 | # The encoding of source files. 55 | # source_encoding = 'utf-8-sig' 56 | 57 | # The master toctree document. 58 | master_doc = "index" 59 | 60 | # General information about the project. 61 | project = "Snakeface" 62 | copyright = "2020, Johannes Koester and Vanessa Sochat" 63 | 64 | from snakeface import version 65 | 66 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "snakeface.settings") 67 | import django 68 | 69 | django.setup() 70 | 71 | # The version info for the project you're documenting, acts as replacement for 72 | # |version| and |release|, also used in various other places throughout the 73 | # built documents. 74 | # 75 | # The short X.Y version. 76 | version = version.__version__ 77 | 78 | if os.environ.get("READTHEDOCS") == "True": 79 | # Because Read The Docs modifies conf.py, versioneer gives a "dirty" 80 | # version like "5.10.0+0.g28674b1.dirty" that is cleaned here. 81 | version = version.partition("+0.g")[0] 82 | 83 | # The full version, including alpha/beta/rc tags. 84 | release = version 85 | 86 | # The language for content autogenerated by Sphinx. Refer to documentation 87 | # for a list of supported languages. 88 | # language = None 89 | 90 | # There are two options for replacing |today|: either, you set today to some 91 | # non-false value, then it is used: 92 | # today = '' 93 | # Else, today_fmt is used as the format for a strftime call. 94 | # today_fmt = '%B %d, %Y' 95 | 96 | # List of patterns, relative to source directory, that match files and 97 | # directories to ignore when looking for source files. 98 | exclude_patterns = ["_build"] 99 | 100 | # The reST default role (used for this markup: `text`) to use for all 101 | # documents. 102 | # default_role = None 103 | 104 | # If true, '()' will be appended to :func: etc. cross-reference text. 105 | # add_function_parentheses = True 106 | 107 | # If true, the current module name will be prepended to all description 108 | # unit titles (such as .. function::). 109 | # add_module_names = True 110 | 111 | # If true, sectionauthor and moduleauthor directives will be shown in the 112 | # output. They are ignored by default. 113 | # show_authors = False 114 | 115 | # The name of the Pygments (syntax highlighting) style to use. 116 | pygments_style = "sphinx" 117 | 118 | # A list of ignored prefixes for module index sorting. 119 | # modindex_common_prefix = [] 120 | 121 | # If true, keep warnings as "system message" paragraphs in the built documents. 122 | # keep_warnings = False 123 | 124 | 125 | # -- Options for HTML output ---------------------------------------------- 126 | 127 | # The theme to use for HTML and HTML Help pages. See the documentation for 128 | # a list of builtin themes. 129 | html_theme = "sphinx_rtd_theme" 130 | 131 | # Theme options are theme-specific and customize the look and feel of a theme 132 | # further. For a list of options available for each theme, see the 133 | # documentation. 134 | # html_theme_options = {} 135 | 136 | # Add any paths that contain custom themes here, relative to this directory. 137 | # html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() 138 | 139 | # The name for this set of Sphinx documents. If None, it defaults to 140 | # " v documentation". 141 | # html_title = None 142 | 143 | # A shorter title for the navigation bar. Default is the same as html_title. 144 | # html_short_title = None 145 | 146 | # The name of an image file (relative to this directory) to place at the top 147 | # of the sidebar. 148 | # html_logo = None 149 | 150 | # The name of an image file (within the static path) to use as favicon of the 151 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 152 | # pixels large. 153 | # html_favicon = None 154 | 155 | # Add any paths that contain custom static files (such as style sheets) here, 156 | # relative to this directory. They are copied after the builtin static files, 157 | # so a file named "default.css" will overwrite the builtin "default.css". 158 | html_static_path = ["_static"] 159 | 160 | # Add any extra paths that contain custom files (such as robots.txt or 161 | # .htaccess) here, relative to this directory. These files are copied 162 | # directly to the root of the documentation. 163 | # html_extra_path = ["_static/css"] 164 | 165 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 166 | # using the given strftime format. 167 | # html_last_updated_fmt = '%b %d, %Y' 168 | 169 | # If true, SmartyPants will be used to convert quotes and dashes to 170 | # typographically correct entities. 171 | # html_use_smartypants = True 172 | 173 | # Custom sidebar templates, maps document names to template names. 174 | # html_sidebars = {} 175 | 176 | # Additional templates that should be rendered to pages, maps page names to 177 | # template names. 178 | # html_additional_pages = {"index": "index.html"} 179 | 180 | # If false, no module index is generated. 181 | # html_domain_indices = True 182 | 183 | # If false, no index is generated. 184 | # html_use_index = True 185 | 186 | # If true, the index is split into individual pages for each letter. 187 | # html_split_index = False 188 | 189 | # If true, links to the reST sources are added to the pages. 190 | # html_show_sourcelink = True 191 | 192 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 193 | # html_show_sphinx = True 194 | 195 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 196 | # html_show_copyright = True 197 | 198 | # If true, an OpenSearch description file will be output, and all pages will 199 | # contain a tag referring to it. The value of this option must be the 200 | # base URL from which the finished HTML is served. 201 | # html_use_opensearch = '' 202 | 203 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 204 | # html_file_suffix = None 205 | 206 | # Output file base name for HTML help builder. 207 | htmlhelp_basename = "Snakefacedoc" 208 | 209 | 210 | # -- Options for LaTeX output --------------------------------------------- 211 | 212 | latex_elements = { 213 | # The paper size ('letterpaper' or 'a4paper'). 214 | #'papersize': 'letterpaper', 215 | # The font size ('10pt', '11pt' or '12pt'). 216 | #'pointsize': '10pt', 217 | # Additional stuff for the LaTeX preamble. 218 | #'preamble': '', 219 | } 220 | 221 | # Grouping the document tree into LaTeX files. List of tuples 222 | # (source start file, target name, title, 223 | # author, documentclass [howto, manual, or own class]). 224 | latex_documents = [ 225 | ("index", "Snakeface.tex", "Snakeface Documentation", "Johannes Koester", "manual"), 226 | ] 227 | 228 | # The name of an image file (relative to this directory) to place at the top of 229 | # the title page. 230 | # latex_logo = None 231 | 232 | # For "manual" documents, if this is true, then toplevel headings are parts, 233 | # not chapters. 234 | # latex_use_parts = False 235 | 236 | # If true, show page references after internal links. 237 | # latex_show_pagerefs = False 238 | 239 | # If true, show URL addresses after external links. 240 | # latex_show_urls = False 241 | 242 | # Documents to append as an appendix to all manuals. 243 | # latex_appendices = [] 244 | 245 | # If false, no module index is generated. 246 | # latex_domain_indices = True 247 | 248 | 249 | # -- Options for manual page output --------------------------------------- 250 | 251 | # One entry per manual page. List of tuples 252 | # (source start file, name, description, authors, manual section). 253 | man_pages = [ 254 | ( 255 | "index", 256 | "snakeface", 257 | "Snakeface Documentation", 258 | ["Johannes Koester", "Vanessa Sochat"], 259 | 1, 260 | ) 261 | ] 262 | 263 | # If true, show URL addresses after external links. 264 | # man_show_urls = False 265 | 266 | 267 | # -- Options for Texinfo output ------------------------------------------- 268 | 269 | # Grouping the document tree into Texinfo files. List of tuples 270 | # (source start file, target name, title, author, 271 | # dir menu entry, description, category) 272 | texinfo_documents = [ 273 | ( 274 | "index", 275 | "Snakemake", 276 | "Snakeface Documentation", 277 | ["Johannes Koester", "Vanessa Sochat"], 278 | "Snakeface", 279 | "Interface for deploying snakemake pipelines.", 280 | "Miscellaneous", 281 | ), 282 | ] 283 | 284 | # Documents to append as an appendix to all manuals. 285 | # texinfo_appendices = [] 286 | 287 | # If false, no module index is generated. 288 | # texinfo_domain_indices = True 289 | 290 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 291 | # texinfo_show_urls = 'footnote' 292 | 293 | # If true, do not generate a @detailmenu in the "Top" node's menu. 294 | # texinfo_no_detailmenu = False 295 | 296 | 297 | def setup(app): 298 | app.add_stylesheet("sphinx-argparse.css") 299 | -------------------------------------------------------------------------------- /docs/getting_started/auth.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started-auth: 2 | 3 | 4 | Authentication 5 | ============== 6 | 7 | If you don't define an authentication backend (e.g., plugins like ldap, saml, or 8 | OAuth 2), then the default authentication model for Snakeface is akin to a jupyter notebook. 9 | You'll be given a token to enter in the interface, and this will log you in. This 10 | is currently the only authentication supported, as we haven't developed the other 11 | deployment types. 12 | -------------------------------------------------------------------------------- /docs/getting_started/example_workflow.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started-example-workflow: 2 | 3 | ================ 4 | Example Workflow 5 | ================ 6 | 7 | Downloading Tutorial 8 | ==================== 9 | 10 | You likely want to start with an example workflow. We will use the same one from 11 | the `snakemake tutorial _`. 12 | We assume that you have already installed ``snakeface`` (and thus Snakemake and it's 13 | dependencies are on your system). So you can download the example as follows: 14 | 15 | .. code:: console 16 | 17 | $ mkdir snakemake-tutorial 18 | $ cd snakemake-tutorial 19 | $ wget https://github.com/snakemake/snakemake-tutorial-data/archive/v5.24.1.tar.gz 20 | $ tar --wildcards -xf v5.24.1.tar.gz --strip 1 "*/data" "*/environment.yaml" 21 | 22 | 23 | This should extract a ``data`` folder and an ``environment.yaml``. 24 | You should also create the `Snakefile _`. 25 | This Snakefile is the same as in the tutorial, with the addition of adding the ``environment.yaml`` to each 26 | section. 27 | 28 | .. code:: console 29 | 30 | SAMPLES = ["A", "B"] 31 | 32 | rule all: 33 | input: 34 | "calls/all.vcf" 35 | 36 | 37 | rule bwa_map: 38 | input: 39 | "data/genome.fa", 40 | "data/samples/{sample}.fastq" 41 | output: 42 | "mapped_reads/{sample}.bam" 43 | conda: 44 | "environment.yaml" 45 | shell: 46 | "bwa mem {input} | samtools view -Sb - > {output}" 47 | 48 | 49 | rule samtools_sort: 50 | input: 51 | "mapped_reads/{sample}.bam" 52 | output: 53 | "sorted_reads/{sample}.bam" 54 | conda: 55 | "environment.yaml" 56 | shell: 57 | "samtools sort -T sorted_reads/{wildcards.sample} " 58 | "-O bam {input} > {output}" 59 | 60 | 61 | rule samtools_index: 62 | input: 63 | "sorted_reads/{sample}.bam" 64 | output: 65 | "sorted_reads/{sample}.bam.bai" 66 | conda: 67 | "environment.yaml" 68 | shell: 69 | "samtools index {input}" 70 | 71 | 72 | rule bcftools_call: 73 | input: 74 | fa="data/genome.fa", 75 | bam=expand("sorted_reads/{sample}.bam", sample=SAMPLES), 76 | bai=expand("sorted_reads/{sample}.bam.bai", sample=SAMPLES) 77 | output: 78 | "calls/all.vcf" 79 | conda: 80 | "environment.yaml" 81 | shell: 82 | "samtools mpileup -g -f {input.fa} {input.bam} | " 83 | "bcftools call -mv - > {output}" 84 | 85 | 86 | 87 | 88 | Running Snakeface 89 | ================= 90 | 91 | At this point, from this working directory you can run Snakeface. For example, you 92 | might run a :ref:`getting_started-notebook`. Make sure to select ``--use-conda`` 93 | or else the environment above won't be properly sourced. This is one deviation from the main 94 | Snakemake tutorial, which has you install dependencies on the command line before running 95 | the workflow, and the workflow doesn't have the ``conda`` sections. 96 | -------------------------------------------------------------------------------- /docs/getting_started/index.rst: -------------------------------------------------------------------------------- 1 | .. _getting-started: 2 | 3 | =============== 4 | Getting Started 5 | =============== 6 | 7 | Snakeface stands for "Snakemake Interface," and it's exactly that - an interface 8 | for you to easily run and interact with Snakemake workflows. 9 | Although it is still in development, the overarching goal is to be flexible to different needs for your deployment. 10 | This means that you can both run it quickly as a notebook to test a workflow locally, 11 | or deploy it in a cluster environment for your user base. If you have a need 12 | for deployment that is not addressed here, please `let us know `_ 13 | We recommend that you start by setting up the :ref:`getting_started-example-workflow`. 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | installation 19 | example_workflow 20 | notebook 21 | settings 22 | auth 23 | -------------------------------------------------------------------------------- /docs/getting_started/installation.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started-installation: 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | Snakeface can be installed and run from a virtual environment, or from a container. 8 | 9 | 10 | Virtual Environment 11 | =================== 12 | 13 | First, clone the repository code. 14 | 15 | .. code:: console 16 | 17 | $ git clone git@github.com:snakemake/snakeface.git 18 | $ cd snakeface 19 | 20 | 21 | Then you'll want to create a new virtual environment, and install dependencies. 22 | 23 | .. code:: console 24 | 25 | $ python -m venv env 26 | $ source env/bin/activate 27 | $ pip install -r requirements.txt 28 | 29 | 30 | And install Snakeface (from the repository directly) 31 | 32 | .. code:: console 33 | 34 | $ pip install -e . 35 | 36 | 37 | Install via pip 38 | =============== 39 | 40 | Snakeface can also be installed with pip. 41 | 42 | .. code:: console 43 | 44 | $ pip install snakeface 45 | 46 | 47 | Once it's installed, you should be able to inspect the client! 48 | 49 | 50 | .. code:: console 51 | 52 | $ snakeface --help 53 | usage: snakeface [-h] [--version] [--noreload] [--verbosity {0,1,2,3}] 54 | [--workdir [WORKDIR]] [--auth {token}] [--port PORT] 55 | [--verbose] [--log-disable-color] [--log-use-threads] 56 | [--force] 57 | [repo] [dest] {notebook} ... 58 | 59 | Snakeface: interface to snakemake. 60 | 61 | positional arguments: 62 | repo Repository address and destination to deploy, e.g., 63 | 64 | dest Path to clone the repository, should not exist. 65 | 66 | optional arguments: 67 | -h, --help show this help message and exit 68 | --version print the version and exit. 69 | --noreload Tells Django to NOT use the auto-reloader. 70 | --verbosity {0,1,2,3} 71 | Verbosity (0, 1, 2, 3). 72 | --workdir [WORKDIR] Specify the working directory. 73 | --force If the folder exists, force overwrite, meaning remove 74 | and replace. 75 | 76 | SETTINGS: 77 | --auth {token} Authentication type to create for the interface, 78 | defaults to token. 79 | 80 | NETWORKING: 81 | --port PORT Port to serve application on. 82 | 83 | LOGGING: 84 | --verbose verbose output for logging. 85 | --log-disable-color Disable color for snakeface logging. 86 | --log-use-threads Force threads rather than processes. 87 | 88 | actions: 89 | subparsers for Snakeface 90 | 91 | {notebook} snakeface actions 92 | notebook run a snakeface notebook 93 | 94 | 95 | Setup 96 | ===== 97 | 98 | As a user, you most likely want to use Snakeface as an on demand notebook, so no additional 99 | setup is needed other than installing the package. As we add more deployment types that 100 | warrant additional configuration, or in the case of installing Snakeface as a cluster admin, 101 | you likely will want to install from the source repository (or a release) and 102 | edit the settings.yml file in the snakemake folder before deploying your service. 103 | More information will be added as this is developed. If you are interested, you can 104 | look at :ref:`getting_started-settings`. 105 | -------------------------------------------------------------------------------- /docs/getting_started/notebook.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started-notebook: 2 | 3 | ======== 4 | Notebook 5 | ======== 6 | 7 | Make sure that before you run a notebook, you are comfortable with Snakemake 8 | and have a workflow with a Snakefile read to run. If not, you can start 9 | with the instructions for an example workflow (:ref:`getting_started-example-workflow`). 10 | 11 | 12 | Local Notebook 13 | ============== 14 | 15 | If you have installed Snakeface on your own, you likely want a notebook. You can 16 | run snakeface without any arguments to run one by default: This works because 17 | the default install settings have set ``NOTEBOOK_ONLY`` and it will start a Snakeface 18 | session. 19 | 20 | .. code:: console 21 | 22 | $ snakeface 23 | 24 | However, if your center is running Snakeface as a service, you will need to ask for 25 | a notebook explicitly: 26 | 27 | .. code:: console 28 | 29 | $ snakeface notebook 30 | 31 | For either of the two you can optionally specify a port: 32 | 33 | .. code:: console 34 | 35 | $ snakeface notebook --port 5555 36 | $ snakeface --port 5555 37 | 38 | 39 | For the notebook install, you will be given a token in your console, and you can copy 40 | paste it into the interface to log in. 41 | 42 | .. image:: ../images/notebook-login.png 43 | 44 | You can then browse to localhost at the port specified to see the interface! 45 | The first prompt will ask you to create a collection, which is a grouping of workflows. 46 | You might find it useful to organize your projects. 47 | 48 | .. image:: ../images/after-notebook-login.png 49 | 50 | Next, click on the button to create a new workflow. The next 51 | form will provide input fields for all arguments provided by your Snakemake 52 | installation. You can select the blue buttons at the top (they are always at the 53 | top) to jump to a section, and see the command being previewed at the bottom. 54 | The command will always update when you make a new selection. 55 | 56 | .. image:: ../images/new_workflow.png 57 | 58 | Note that if you start running your notebook in a location without any Snakefiles, 59 | you will get a message that tells you to create one first. A Snakefile matching 60 | some pattern of snakefile* (case ignored) must be present. When you've finished your 61 | workflow, click on "Run Workflow." If the workflow is invalid (e.g., you've moved the 62 | Snakefile, or provided conflicting commands) then you'll be returned to this 63 | view with an error message. If it's valid, you'll be redirected to a page to monitor 64 | the workflow. 65 | 66 | .. image:: ../images/workflow-detail.png 67 | 68 | This page also has metadata for how to interact with your workflow if you choose 69 | to run it again with Snakemake from the command line. A token and arguments for monitoring 70 | are required. At the bottom part of the page, there is a status table that updates 71 | automatically via a Web Socket. 72 | 73 | .. image:: ../images/workflow-table.png 74 | 75 | Finally, you'll also be able to see your workflows on the dashboard page in the Workflows table. 76 | 77 | .. image:: ../images/dashboard.png 78 | 79 | 80 | Continuing A Workflow 81 | ===================== 82 | 83 | If you want to start a workflow from the command line to interact with a snakeface 84 | server, or you've already started one with Snakeface and want it to reference the same identifier again, 85 | you can easily run snakemake to do this by adding an environment variable for an 86 | authorization token, and a workflow id. If you look at the workflow details page above, 87 | you'll see that the token and command line arguments are provided for you. You 88 | might re-run an existing workflow like this: 89 | 90 | .. code:: console 91 | 92 | export WMS_MONITOR_TOKEN=a2d0d2f2-dfa8-4fd6-b98c-f3219a2caa8c 93 | snakemake --cores 1 --wms-monitor http://127.0.0.1:5000 --wms-monitor-arg id=3 94 | 95 | 96 | Workflow Reports 97 | ================ 98 | 99 | If you want to add a report file to the workflow, just as you would with command line 100 | Snakemake, you'll need to install additional dependencies first: 101 | 102 | .. code:: console 103 | 104 | pip install snakemake[reports] 105 | 106 | 107 | And then define your report.html file in the reports field. 108 | 109 | 110 | -------------------------------------------------------------------------------- /docs/getting_started/settings.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started-settings: 2 | 3 | 4 | ======== 5 | Settings 6 | ======== 7 | 8 | Settings are defined in the settings.yml file, and are automatically populated 9 | into Snakeface. If you want a notebook, you will likely be good using the defaults. 10 | 11 | 12 | .. list-table:: Title 13 | :widths: 25 65 10 14 | :header-rows: 1 15 | 16 | * - Name 17 | - Description 18 | - Default 19 | * - GOOGLE_ANALYTICS_SITE 20 | - The url of your website for Google Analytics, if desired 21 | - None 22 | * - GOOGLE_ANALYTICS_ID 23 | - The identifier for Google Analytics, if desired 24 | - None 25 | * - TWITTER_USERNAME 26 | - A Twitter username to link to in the footer. 27 | - johanneskoester 28 | * - GITHUB_REPOSITORY 29 | - A GitHub repository to link to in the footer 30 | - https://github.com/snakemake/snakeface 31 | * - GITHUB_DOCUMENTATION 32 | - GitHub documentation (or other) to link to in the footer 33 | - https://snakemake.github.io/snakeface 34 | * - USER_WORKFLOW_LIMIT 35 | - The maximum number of workflows to allow a user to create 36 | - 50 37 | * - USER_WORKFLOW_RUNS_LIMIT 38 | - The maximum number of running workflows to allow 39 | - 50 40 | * - USER_WORKFLOW_GLOBAL_RUNS_LIMIT 41 | - Giving a shared Snakeface interface, the total maximum allowed running at once. 42 | - 1000 43 | * - NOTEBOOK_ONLY 44 | - Only allow notebooks (disables all other auth) 45 | - None 46 | * - MAXIMUM_NOTEBOOK_JOBS 47 | - Given a notebook, the maximum number of jobs to allow running at once 48 | - 2 49 | * - WORKFLOW_UPDATE_SECONDS 50 | - How often to refresh the status table on a workflow details page 51 | - 10 52 | * - EXECUTOR_CLUSTER 53 | - Set this to non null to enable the cluster executor 54 | - None 55 | * - EXECUTOR_GOOGLE_LIFE_SCIENCES 56 | - Set this to non null to enable the GLS executor 57 | - None 58 | * - EXECUTOR_KUBERNETES 59 | - Set this to non null to enable the K8 executor 60 | - None 61 | * - EXECUTOR_GA4GH_TES 62 | - Set this to non null to enable this executor 63 | - None 64 | * - EXECUTOR_TIBANNA 65 | - Set this to non null to enable the tibanna executor 66 | - None 67 | * - DISABLE_SINGULARITY 68 | - Disable Singularity argument groups by setting this to non null 69 | - None 70 | * - DISABLE_CONDA 71 | - Disable Conda argument groups by setting this to non null 72 | - None 73 | * - DISABLE_NOTEBOOKS 74 | - Disable notebook argument groups by setting this to non null 75 | - true 76 | * - ENVIRONMENT 77 | - The global name for the deployment environment 78 | - test 79 | * - HELP_CONTACT_URL 80 | - The help contact email or url used for the API 81 | - https://github.com/snakemake/snakeface/issues 82 | * - SENDGRID_API_KEY 83 | - Not in use yet, will allow sending email notifications 84 | - None 85 | * - SENDGRID_SENDER_EMAIL 86 | - Not in use yet, will allow sending email notifications 87 | - None 88 | * - DOMAIN_NAME 89 | - The server domain name, defaults to a localhost address 90 | - http://127.0.0.1 91 | * - DOMAIN_PORT 92 | - The server port, can be overridden from the command line 93 | - 5000 94 | * - REQUIRE_AUTH 95 | - Should authentication be required? 96 | - true 97 | * - PROFILE 98 | - Set a default profile (see https://github.com/snakemake-profiles) 99 | - None 100 | * - PROFILE 101 | - Set a default profile (see https://github.com/snakemake-profiles) 102 | - None 103 | * - PRIVATE_ONLY 104 | - Make all workflows private (not relevant for notebooks) 105 | - None 106 | * - ENABLE_CACHE 107 | - Enable view caching 108 | - false 109 | * - WORKDIR 110 | - Default working directory (overridden by client and environment) 111 | - None 112 | * - PLUGINS_LDAP_AUTH_ENABLED 113 | - Set to non null to enable 114 | - None 115 | * - PLUGINS_PAM_AUTH_ENABLED 116 | - Set to non null to enable 117 | - None 118 | * - PLUGINS_SAML_AUTH_ENABLED 119 | - Set to non null to enable 120 | - None 121 | -------------------------------------------------------------------------------- /docs/images/after-notebook-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/docs/images/after-notebook-login.png -------------------------------------------------------------------------------- /docs/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/docs/images/dashboard.png -------------------------------------------------------------------------------- /docs/images/new_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/docs/images/new_workflow.png -------------------------------------------------------------------------------- /docs/images/notebook-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/docs/images/notebook-login.png -------------------------------------------------------------------------------- /docs/images/workflow-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/docs/images/workflow-detail.png -------------------------------------------------------------------------------- /docs/images/workflow-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/docs/images/workflow-table.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _manual-main: 2 | 3 | ========= 4 | Snakeface 5 | ========= 6 | 7 | .. image:: https://img.shields.io/discord/753690260830945390?label=discord%20chat 8 | :alt: Discord 9 | :target: https://discord.gg/NUdMtmr 10 | 11 | .. image:: https://img.shields.io/github/stars/snakemake/snakeface?style=social 12 | :alt: GitHub stars 13 | :target: https://github.com/snakemake/snakeface/stargazers 14 | 15 | 16 | Snakeface is the Snakemake Interface, where you can easily run workflows. 17 | To learn more about Snakemake, visit the `official documentation `_ 18 | 19 | .. _main-getting-started: 20 | 21 | ---------------------------------------- 22 | Getting started with Snakemake Interface 23 | ---------------------------------------- 24 | 25 | Snakeface can be used on your local machine to provide a nice interface to running 26 | snakemake workflows, or deployed by a group to run shared workflows. See :ref:`use-cases` for an overview of different use cases. 27 | 28 | .. _main-support: 29 | 30 | ------- 31 | Support 32 | ------- 33 | 34 | * In case of **questions**, please post on `stack overflow `_. 35 | * To **discuss** with other Snakemake users, you can use the `mailing list `_. **Please do not post questions there. Use stack overflow for questions.** 36 | * For **bugs and feature requests**, please use the `issue tracker `_. 37 | * For **contributions**, visit Snakemake on `Github `_. 38 | 39 | --------- 40 | Resources 41 | --------- 42 | 43 | `Snakemake Repository `_ 44 | The Snakemake workflow manager repository houses the core software for Snakemake. 45 | 46 | `Snakemake Wrappers Repository `_ 47 | The Snakemake Wrapper Repository is a collection of reusable wrappers that allow to quickly use popular tools from Snakemake rules and workflows. 48 | 49 | `Snakemake Workflows Project `_ 50 | This project provides a collection of high quality modularized and re-usable workflows. 51 | The provided code should also serve as a best-practices of how to build production ready workflows with Snakemake. 52 | Everybody is invited to contribute. 53 | 54 | `Snakemake Profiles Project `_ 55 | This project provides Snakemake configuration profiles for various execution environments. 56 | Please consider contributing your own if it is still missing. 57 | 58 | `Bioconda `_ 59 | Bioconda can be used from Snakemake for creating completely reproducible workflows by defining the used software versions and providing binaries. 60 | 61 | .. toctree:: 62 | :caption: Getting started 63 | :name: getting_started 64 | :hidden: 65 | :maxdepth: 2 66 | 67 | getting_started/index 68 | 69 | .. toctree:: 70 | :caption: Use Cases 71 | :name: use_cases 72 | :hidden: 73 | :maxdepth: 2 74 | 75 | use_cases/index 76 | 77 | .. toctree:: 78 | :caption: API Reference 79 | :name: api-reference 80 | :hidden: 81 | :maxdepth: 1 82 | 83 | api_reference/snakeface 84 | api_reference/internal/modules 85 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinxcontrib-napoleon 3 | sphinx-argparse 4 | sphinx_rtd_theme 5 | docutils==0.12 6 | recommonmark 7 | configargparse 8 | appdirs 9 | snakeface 10 | -------------------------------------------------------------------------------- /docs/use_cases/img/template-404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/docs/use_cases/img/template-404.png -------------------------------------------------------------------------------- /docs/use_cases/index.rst: -------------------------------------------------------------------------------- 1 | .. _use-cases: 2 | 3 | ========= 4 | Use Cases 5 | ========= 6 | 7 | Snakeface is intended to be flexible to different needs for your deployment. 8 | This means that you can both run it quickly as a notebook :ref:`getting_started-notebook` to test a workflow, 9 | or deploy it in a cluster environment for your user base. If you have a need 10 | for deployment that is not addressed here, please `let us know `_ 11 | -------------------------------------------------------------------------------- /img/snakeface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/img/snakeface.png -------------------------------------------------------------------------------- /img/snakeface.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/img/snakeface.xcf -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from snakeface.wsgi import application 2 | 3 | # This file is only needed for running on Google Cloud App Engine 4 | # App Engine by default looks for a main.py file at the root of the app 5 | # directory with a WSGI-compatible object called app. 6 | # This file imports the WSGI-compatible object of your Django app, 7 | # application from gcpdjango/wsgi.py and renames it app so it is discoverable by 8 | # App Engine without additional configuration. 9 | # Alternatively, you can add a custom entrypoint field in your app.yaml: 10 | # entrypoint: gunicorn -b :$PORT gcpdjango.wsgi 11 | app = application 12 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "snakeface.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==3.0.14 2 | django-ratelimit==3.0.0 3 | django-extensions==3.0.2 4 | sendgrid==6.4.3 5 | snakemake 6 | django-q==1.3.4 7 | # just for postgres 8 | psycopg2-binary==2.8.5 9 | django-gravatar2==1.4.4 10 | django-taggit==1.3.0 11 | djangorestframework==3.11.2 12 | social-auth-app-django==4.0.0 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __author__ = "Vanessa Sochat" 4 | __copyright__ = "Copyright 2020, Vanessa SOchat" 5 | __license__ = "MPL 2.0" 6 | 7 | 8 | from setuptools import setup, find_packages 9 | import os 10 | 11 | 12 | def get_lookup(): 13 | """get version by way of version file, returns a 14 | lookup dictionary with several global variables without 15 | needing to import the software. 16 | """ 17 | lookup = dict() 18 | version_file = os.path.join("snakeface", "version.py") 19 | with open(version_file) as filey: 20 | exec(filey.read(), lookup) 21 | return lookup 22 | 23 | 24 | def get_reqs(lookup=None, key="INSTALL_REQUIRES"): 25 | """get requirements, mean reading in requirements and versions from 26 | the lookup obtained with get_lookup 27 | """ 28 | if lookup == None: 29 | lookup = get_lookup() 30 | 31 | install_requires = [] 32 | for module in lookup[key]: 33 | module_name = module[0] 34 | module_meta = module[1] 35 | if "exact_version" in module_meta: 36 | dependency = "%s==%s" % (module_name, module_meta["exact_version"]) 37 | elif "min_version" in module_meta: 38 | if module_meta["min_version"] == None: 39 | dependency = module_name 40 | else: 41 | dependency = "%s>=%s" % (module_name, module_meta["min_version"]) 42 | elif "max_version" in module_meta: 43 | if module_meta["max_version"] == None: 44 | dependency = module_name 45 | else: 46 | dependency = "%s<=%s" % (module_name, module_meta["max_version"]) 47 | install_requires.append(dependency) 48 | return install_requires 49 | 50 | 51 | # Make sure everything is relative to setup.py 52 | install_path = os.path.dirname(os.path.abspath(__file__)) 53 | os.chdir(install_path) 54 | 55 | # Get version information from the lookup 56 | lookup = get_lookup() 57 | VERSION = lookup["__version__"] 58 | NAME = lookup["NAME"] 59 | AUTHOR = lookup["AUTHOR"] 60 | AUTHOR_EMAIL = lookup["AUTHOR_EMAIL"] 61 | PACKAGE_URL = lookup["PACKAGE_URL"] 62 | KEYWORDS = lookup["KEYWORDS"] 63 | DESCRIPTION = lookup["DESCRIPTION"] 64 | LICENSE = lookup["LICENSE"] 65 | with open("README.md") as filey: 66 | LONG_DESCRIPTION = filey.read() 67 | 68 | ################################################################################ 69 | # MAIN ######################################################################### 70 | ################################################################################ 71 | 72 | if __name__ == "__main__": 73 | 74 | INSTALL_REQUIRES = get_reqs(lookup) 75 | TESTS_REQUIRES = get_reqs(lookup, "TESTS_REQUIRES") 76 | ALL_REQUIRES = get_reqs(lookup, "ALL_REQUIRES") 77 | INSTALL_EMAIL_REQUIRES = get_reqs(lookup, "EMAIL_REQUIRES") 78 | 79 | setup( 80 | name=NAME, 81 | version=VERSION, 82 | author=AUTHOR, 83 | author_email=AUTHOR_EMAIL, 84 | maintainer=AUTHOR, 85 | maintainer_email=AUTHOR_EMAIL, 86 | packages=find_packages(), 87 | include_package_data=True, 88 | zip_safe=False, 89 | url=PACKAGE_URL, 90 | license=LICENSE, 91 | description=DESCRIPTION, 92 | long_description=LONG_DESCRIPTION, 93 | long_description_content_type="text/markdown", 94 | keywords=KEYWORDS, 95 | setup_requires=["pytest-runner"], 96 | install_requires=INSTALL_REQUIRES, 97 | tests_require=TESTS_REQUIRES, 98 | extras_require={ 99 | "all": ALL_REQUIRES, 100 | "email": INSTALL_EMAIL_REQUIRES, 101 | }, 102 | classifiers=[ 103 | "Intended Audience :: Science/Research", 104 | "Intended Audience :: Developers", 105 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", 106 | "Topic :: Software Development", 107 | "Topic :: Scientific/Engineering", 108 | "Natural Language :: English", 109 | "Operating System :: Unix", 110 | "Programming Language :: Python :: 3", 111 | "Programming Language :: Python :: 3.5", 112 | "Programming Language :: Python :: 3.6", 113 | "Programming Language :: Python :: 3.7", 114 | "Programming Language :: Python :: 3.8", 115 | ], 116 | entry_points={"console_scripts": ["snakeface=snakeface.client:main"]}, 117 | ) 118 | -------------------------------------------------------------------------------- /snakeface/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/__init__.py -------------------------------------------------------------------------------- /snakeface/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/__init__.py -------------------------------------------------------------------------------- /snakeface/apps/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/api/__init__.py -------------------------------------------------------------------------------- /snakeface/apps/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = "api" 6 | -------------------------------------------------------------------------------- /snakeface/apps/api/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import BasePermission, SAFE_METHODS 2 | from rest_framework.authtoken.models import Token 3 | 4 | 5 | class AllowAnyGet(BasePermission): 6 | """Allows an anonymous user access for GET requests only.""" 7 | 8 | def has_permission(self, request, view): 9 | 10 | if request.user.is_anonymous and request.method == "GET": 11 | return (True,) 12 | 13 | if request.user.is_staff or request.user.is_superuser: 14 | return True 15 | 16 | return request.method in SAFE_METHODS 17 | 18 | 19 | def check_user_authentication(request): 20 | """Given a request, check that the user is authenticated via a token in 21 | the header. 22 | """ 23 | token = get_token(request) 24 | 25 | # Case 1: no token and auth is required, prompt the user for it 26 | if not token: 27 | return None, 401 28 | 29 | # Case 2: the token is not associated with a user 30 | try: 31 | token = Token.objects.get(key=token) 32 | except: 33 | return None, 403 34 | 35 | return token.user, 200 36 | 37 | 38 | def get_token(request): 39 | """The same as validate_token, but return the token object to check the 40 | associated user. 41 | """ 42 | # Coming from HTTP, look for authorization as bearer token 43 | token = request.META.get("HTTP_AUTHORIZATION") 44 | 45 | if token: 46 | token = token.split(" ")[-1].strip() 47 | try: 48 | return Token.objects.get(key=token) 49 | except Token.DoesNotExist: 50 | pass 51 | 52 | # Next attempt - try to get token via user session 53 | elif request.user.is_authenticated and not request.user.is_anonymous: 54 | try: 55 | return Token.objects.get(user=request.user) 56 | except Token.DoesNotExist: 57 | pass 58 | -------------------------------------------------------------------------------- /snakeface/apps/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.urls import path 3 | 4 | from snakeface.settings import cfg 5 | import snakeface.apps.api.views as api_views 6 | from .permissions import AllowAnyGet 7 | 8 | urlpatterns = [ 9 | path( 10 | "api/service-info", 11 | api_views.ServiceInfo.as_view(), 12 | name="service_info", 13 | ), 14 | path( 15 | "create_workflow", 16 | api_views.CreateWorkflow.as_view(), 17 | name="create_workflow", 18 | ), 19 | path( 20 | "update_workflow_status", 21 | api_views.UpdateWorkflow.as_view(), 22 | name="update_workflow_status", 23 | ), 24 | ] 25 | 26 | 27 | app_name = "api" 28 | -------------------------------------------------------------------------------- /snakeface/apps/api/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from rest_framework.renderers import JSONRenderer 4 | from ratelimit.mixins import RatelimitMixin 5 | from django.shortcuts import get_object_or_404 6 | 7 | from snakeface.apps.main.models import Workflow, WorkflowStatus 8 | from snakeface.settings import cfg 9 | from snakeface.version import __version__ 10 | from rest_framework.response import Response 11 | from rest_framework.views import APIView 12 | from .permissions import check_user_authentication 13 | 14 | import json 15 | 16 | 17 | class ServiceInfo(RatelimitMixin, APIView): 18 | """Return a 200 response to indicate a running service. Note that we are 19 | not currently including all required fields. See: 20 | https://ga4gh.github.io/workflow-execution-service-schemas/docs/#operation/GetServiceInfo 21 | """ 22 | 23 | ratelimit_key = "ip" 24 | ratelimit_rate = settings.VIEW_RATE_LIMIT 25 | ratelimit_block = settings.VIEW_RATE_LIMIT_BLOCK 26 | ratelimit_method = "GET" 27 | renderer_classes = (JSONRenderer,) 28 | 29 | def get(self, request): 30 | print("GET /service-info") 31 | 32 | data = { 33 | "id": "snakeface", 34 | "status": "running", # Extra field looked for by Snakemake 35 | "name": "Snakemake Workflow Interface (SnakeFace)", 36 | "type": {"group": "org.ga4gh", "artifact": "beacon", "version": "1.0.0"}, 37 | "description": "This service provides an interface to interact with Snakemake.", 38 | "organization": {"name": "Snakemake", "url": "https://snakemake.github.io"}, 39 | "contactUrl": cfg.HELP_CONTACT_URL, 40 | "documentationUrl": "https://snakemake.github.io/snakeface", 41 | "createdAt": "2020-12-04T12:57:19Z", 42 | "updatedAt": cfg.UPDATED_AT, 43 | "environment": cfg.ENVIRONMENT, 44 | "version": __version__, 45 | "auth_instructions_url": "", 46 | } 47 | 48 | # Must make model json serializable 49 | return Response(status=200, data=data) 50 | 51 | 52 | class CreateWorkflow(RatelimitMixin, APIView): 53 | """Create a snakemake workflow. Given that we provide an API token, we 54 | expect the workflow model to already be created and simply generate a run 55 | for it. 56 | """ 57 | 58 | ratelimit_key = "ip" 59 | ratelimit_rate = settings.VIEW_RATE_LIMIT 60 | ratelimit_block = settings.VIEW_RATE_LIMIT_BLOCK 61 | ratelimit_method = "GET" 62 | renderer_classes = (JSONRenderer,) 63 | 64 | def get(self, request): 65 | print("GET /create_workflow") 66 | 67 | # If the request provides an id, check for workflow 68 | workflow = request.GET.get("id") 69 | user = None 70 | 71 | if workflow: 72 | workflow = get_object_or_404(Workflow, pk=workflow) 73 | 74 | # Does the server require authentication? 75 | if cfg.REQUIRE_AUTH: 76 | user, response_code = check_user_authentication(request) 77 | if not user: 78 | return Response(status=response_code) 79 | 80 | # If we have a workflow, check that user has permission to use/update 81 | if workflow and user not in workflow.owners.all(): 82 | return Response(status=403) 83 | 84 | # If we don't have a workflow, create one 85 | if workflow: 86 | 87 | # Remove old statuses here 88 | workflow.workflowstatus_set.all().delete() 89 | 90 | else: 91 | # Add additional metadata to creation 92 | snakefile = request.POST.get("snakefile") 93 | workdir = request.POST.get("workdir") 94 | command = request.POST.get("command") 95 | workflow = Workflow(snakefile=snakefile, workdir=workdir, command=command) 96 | workflow.save() 97 | if user: 98 | workflow.owners.add(user) 99 | 100 | data = {"id": workflow.id} 101 | return Response(status=200, data=data) 102 | 103 | 104 | class UpdateWorkflow(RatelimitMixin, APIView): 105 | """Update an existing snakemake workflow. Authentication is required, 106 | and the workflow must exist. 107 | """ 108 | 109 | ratelimit_key = "ip" 110 | ratelimit_rate = settings.VIEW_RATE_LIMIT 111 | ratelimit_block = settings.VIEW_RATE_LIMIT_BLOCK 112 | ratelimit_method = "POST" 113 | renderer_classes = (JSONRenderer,) 114 | 115 | def post(self, request): 116 | print("POST /update_workflow_status") 117 | 118 | # We must have an existing workflow to update 119 | workflow = get_object_or_404(Workflow, pk=request.POST.get("id")) 120 | 121 | # Does the server require authentication? 122 | if cfg.REQUIRE_AUTH: 123 | user, response_code = check_user_authentication(request) 124 | if not user: 125 | return Response(response_code) 126 | 127 | # If we have a workflow, check that user has permission to use/update 128 | if workflow and user not in workflow.owners.all(): 129 | return Response(403) 130 | 131 | # The message should be json dump of attributes 132 | message = json.loads(request.POST.get("msg", {})) 133 | 134 | # Update the workflow with a new status message 135 | WorkflowStatus.objects.create(workflow=workflow, msg=message) 136 | return Response(status=200, data={}) 137 | -------------------------------------------------------------------------------- /snakeface/apps/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/__init__.py -------------------------------------------------------------------------------- /snakeface/apps/base/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class BaseConfig(AppConfig): 7 | name = "base" 8 | -------------------------------------------------------------------------------- /snakeface/apps/base/static/css/custom.css: -------------------------------------------------------------------------------- 1 | .card { 2 | padding:20px; 3 | } 4 | 5 | btn { 6 | cursor: pointer !important; 7 | } 8 | 9 | .btn-info { 10 | cursor: pointer !important; 11 | border-color: #17a2b8 !important; 12 | } 13 | 14 | .alert-secondary, .alert-info { 15 | color: #464a4e !important; 16 | } 17 | 18 | .btn-warning { 19 | cursor: pointer !important; 20 | } 21 | 22 | 23 | #square_logo { 24 | width:50px; 25 | float: right; 26 | padding-bottom: 20px; 27 | } 28 | 29 | .alert button.close { 30 | top: 36% !important; 31 | right: 25px !important; 32 | } 33 | 34 | /* Data tables */ 35 | 36 | table.dataTable thead th, table.dataTable thead td { 37 | border-bottom: 1px solid #CCC !important; 38 | } 39 | 40 | div.dataTables_wrapper div.dataTables_length select { 41 | width: 55px !important; 42 | } 43 | 44 | table.dataTable.no-footer { 45 | border-bottom: 1px solid #CCC !important; 46 | } 47 | -------------------------------------------------------------------------------- /snakeface/apps/base/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/favicon.ico -------------------------------------------------------------------------------- /snakeface/apps/base/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /snakeface/apps/base/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /snakeface/apps/base/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /snakeface/apps/base/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /snakeface/apps/base/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /snakeface/apps/base/static/fonts/nucleo-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/fonts/nucleo-icons.eot -------------------------------------------------------------------------------- /snakeface/apps/base/static/fonts/nucleo-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/fonts/nucleo-icons.ttf -------------------------------------------------------------------------------- /snakeface/apps/base/static/fonts/nucleo-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/fonts/nucleo-icons.woff -------------------------------------------------------------------------------- /snakeface/apps/base/static/fonts/nucleo-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/fonts/nucleo-icons.woff2 -------------------------------------------------------------------------------- /snakeface/apps/base/static/fonts/themify.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/fonts/themify.woff -------------------------------------------------------------------------------- /snakeface/apps/base/static/img/clusterb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/img/clusterb.png -------------------------------------------------------------------------------- /snakeface/apps/base/static/img/compute_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/img/compute_server.png -------------------------------------------------------------------------------- /snakeface/apps/base/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/img/favicon.ico -------------------------------------------------------------------------------- /snakeface/apps/base/static/img/grid2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/img/grid2.png -------------------------------------------------------------------------------- /snakeface/apps/base/static/img/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/img/hero.png -------------------------------------------------------------------------------- /snakeface/apps/base/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/img/logo.png -------------------------------------------------------------------------------- /snakeface/apps/base/static/img/logo_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/img/logo_square.png -------------------------------------------------------------------------------- /snakeface/apps/base/static/img/logob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/img/logob.png -------------------------------------------------------------------------------- /snakeface/apps/base/static/img/transparency.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/img/transparency.jpg -------------------------------------------------------------------------------- /snakeface/apps/base/static/img/workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/base/static/img/workstation.png -------------------------------------------------------------------------------- /snakeface/apps/base/static/js/ScrollToPlugin.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * VERSION: 1.7.5 3 | * DATE: 2015-02-26 4 | * UPDATES AND DOCS AT: http://greensock.com 5 | * 6 | * @license Copyright (c) 2008-2015, GreenSock. All rights reserved. 7 | * This work is subject to the terms at http://greensock.com/standard-license or for 8 | * Club GreenSock members, the software agreement that was issued with your membership. 9 | * 10 | * @author: Jack Doyle, jack@greensock.com 11 | **/ 12 | var _gsScope="undefined"!=typeof module&&module.exports&&"undefined"!=typeof global?global:this||window;(_gsScope._gsQueue||(_gsScope._gsQueue=[])).push(function(){"use strict";var t=document.documentElement,e=window,i=function(i,r){var s="x"===r?"Width":"Height",n="scroll"+s,a="client"+s,o=document.body;return i===e||i===t||i===o?Math.max(t[n],o[n])-(e["inner"+s]||t[a]||o[a]):i[n]-i["offset"+s]},r=_gsScope._gsDefine.plugin({propName:"scrollTo",API:2,version:"1.7.5",init:function(t,r,s){return this._wdw=t===e,this._target=t,this._tween=s,"object"!=typeof r&&(r={y:r}),this.vars=r,this._autoKill=r.autoKill!==!1,this.x=this.xPrev=this.getX(),this.y=this.yPrev=this.getY(),null!=r.x?(this._addTween(this,"x",this.x,"max"===r.x?i(t,"x"):r.x,"scrollTo_x",!0),this._overwriteProps.push("scrollTo_x")):this.skipX=!0,null!=r.y?(this._addTween(this,"y",this.y,"max"===r.y?i(t,"y"):r.y,"scrollTo_y",!0),this._overwriteProps.push("scrollTo_y")):this.skipY=!0,!0},set:function(t){this._super.setRatio.call(this,t);var r=this._wdw||!this.skipX?this.getX():this.xPrev,s=this._wdw||!this.skipY?this.getY():this.yPrev,n=s-this.yPrev,a=r-this.xPrev;this._autoKill&&(!this.skipX&&(a>7||-7>a)&&i(this._target,"x")>r&&(this.skipX=!0),!this.skipY&&(n>7||-7>n)&&i(this._target,"y")>s&&(this.skipY=!0),this.skipX&&this.skipY&&(this._tween.kill(),this.vars.onAutoKill&&this.vars.onAutoKill.apply(this.vars.onAutoKillScope||this._tween,this.vars.onAutoKillParams||[]))),this._wdw?e.scrollTo(this.skipX?r:this.x,this.skipY?s:this.y):(this.skipY||(this._target.scrollTop=this.y),this.skipX||(this._target.scrollLeft=this.x)),this.xPrev=this.x,this.yPrev=this.y}}),s=r.prototype;r.max=i,s.getX=function(){return this._wdw?null!=e.pageXOffset?e.pageXOffset:null!=t.scrollLeft?t.scrollLeft:document.body.scrollLeft:this._target.scrollLeft},s.getY=function(){return this._wdw?null!=e.pageYOffset?e.pageYOffset:null!=t.scrollTop?t.scrollTop:document.body.scrollTop:this._target.scrollTop},s._kill=function(t){return t.scrollTo_x&&(this.skipX=!0),t.scrollTo_y&&(this.skipY=!0),this._super._kill.call(this,t)}}),_gsScope._gsDefine&&_gsScope._gsQueue.pop()(); -------------------------------------------------------------------------------- /snakeface/apps/base/static/js/light-bootstrap-dashboard.js: -------------------------------------------------------------------------------- 1 | // ========================================================= 2 | // Light Bootstrap Dashboard - v2.0.1 3 | // ========================================================= 4 | // 5 | // Product Page: https://www.creative-tim.com/product/light-bootstrap-dashboard 6 | // Copyright 2019 Creative Tim (https://www.creative-tim.com) 7 | // Licensed under MIT (https://github.com/creativetimofficial/light-bootstrap-dashboard/blob/master/LICENSE) 8 | // 9 | // Coded by Creative Tim 10 | // 11 | // ========================================================= 12 | // 13 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | var searchVisible = 0; 16 | var transparent = true; 17 | 18 | var transparentDemo = true; 19 | var fixedTop = false; 20 | 21 | var navbar_initialized = false; 22 | var mobile_menu_visible = 0, 23 | mobile_menu_initialized = false, 24 | toggle_initialized = false, 25 | bootstrap_nav_initialized = false, 26 | $sidebar, 27 | isWindows; 28 | 29 | $(document).ready(function() { 30 | window_width = $(window).width(); 31 | 32 | // check if there is an image set for the sidebar's background 33 | lbd.checkSidebarImage(); 34 | 35 | // Init navigation toggle for small screens 36 | if (window_width <= 991) { 37 | lbd.initRightMenu(); 38 | } 39 | 40 | // Activate the tooltips 41 | $('[rel="tooltip"]').tooltip(); 42 | 43 | // Activate regular switches 44 | if ($("[data-toggle='switch']").length != 0) { 45 | $("[data-toggle='switch']").bootstrapSwitch(); 46 | } 47 | 48 | $('.form-control').on("focus", function() { 49 | $(this).parent('.input-group').addClass("input-group-focus"); 50 | }).on("blur", function() { 51 | $(this).parent(".input-group").removeClass("input-group-focus"); 52 | }); 53 | 54 | // Fixes sub-nav not working as expected on IOS 55 | $('body').on('touchstart.dropdown', '.dropdown-menu', function(e) { 56 | e.stopPropagation(); 57 | }); 58 | }); 59 | 60 | // activate collapse right menu when the windows is resized 61 | $(window).resize(function() { 62 | if ($(window).width() <= 991) { 63 | lbd.initRightMenu(); 64 | } 65 | }); 66 | 67 | lbd = { 68 | misc: { 69 | navbar_menu_visible: 0 70 | }, 71 | checkSidebarImage: function() { 72 | $sidebar = $('.sidebar'); 73 | image_src = $sidebar.data('image'); 74 | 75 | if (image_src !== undefined) { 76 | sidebar_container = ' 51 | 52 | {% endblock %} 53 | {% block scripts %} 54 | 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /snakeface/apps/main/templates/workflows/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base/page.html" %} 2 | {% load crispy_forms_tags %} 3 | {% load my_filters %} 4 | {% load static %} 5 | {% block page_title %}Collection > {{ collection.name }}{% endblock %} 6 | {% block css %} 7 | 23 | {% endblock %} 24 | {% block content %} 25 |
26 | {% if workflow.dag %}
27 |
28 |
29 | {{ workflow.dag | safe }} 30 |
31 |
32 |
{% endif %} 33 |
34 |
35 |
36 |
37 |
38 | {% if workflow.has_report %}View Report{% endif %} 39 | DELETE 40 | CANCEL 41 | EDIT 42 | RE-RUN 43 |
44 |
45 | 46 | 47 | {% if workflow.snakefile %} 48 | 49 | 50 | {% endif %} 51 | {% if workflow.workdir %} 52 | 53 | 54 | {% endif %} 55 | {% if workflow.command %} 56 | 57 | 58 | {% endif %} 59 | 60 | 61 | 62 | 63 | {% if request.user.is_authenticated and request.user in workflow.owners.all %} 64 | 65 | 66 | 67 | 68 | 69 | 71 | {% endif %} 72 | 73 |
Snakefile{{ workflow.snakefile }}
Workdir{{ workflow.workdir }}
Command{{ workflow.command }}
Return Code{{ workflow.retval }}
WMS_MONITOR_TOKEN{{ request.user.token }}
Command Line Interaction

To interact with this workflow from the command line, export this variable and provide the following extra arguments



70 | --wms-monitor {{ DOMAIN }} --wms-monitor-arg id={{ workflow.id }}
74 |
75 |
76 |
77 |
78 | 79 | {% if workflow.error or workflow.output %}
80 |
81 |
82 |
83 |
84 | {% if workflow.error %}

{{ workflow.error | safe }}

{% endif %} 85 | {% if workflow.output %}

{{ workflow.output | safe }}

{% endif %} 86 |
87 |
88 |
89 |
90 |
{% endif %} 91 | 92 |
93 |
94 |
95 |
96 |
97 |
98 |

This table updates every {{ WORKFLOW_UPDATE_SECONDS }} seconds

99 | {% include "workflows/workflow_run_table.html" %} 100 |
101 |
102 |
103 |
104 |
105 |
106 | 107 | {% endblock %} 108 | {% block scripts %} 109 | 212 | {% endblock %} 213 | -------------------------------------------------------------------------------- /snakeface/apps/main/templates/workflows/new.html: -------------------------------------------------------------------------------- 1 | {% extends "base/page.html" %} 2 | {% load my_filters %} 3 | {% load crispy_forms_tags %} 4 | {% load static %} 5 | {% block css %} 6 | 28 | {% endblock %} 29 | {% block content %} 30 |
31 | {% csrf_token %} 32 |
MAIN{% for name, group in groups.items %}{{ name }}{% endfor %} 33 |
34 | 35 |

36 |
37 |
38 |
39 |
40 |

Main

41 |
42 |
43 |
44 |
45 | {{ form | crispy }} 46 | {% if NOTEBOOK %}Your working directory must be within the path where you launched your notebook.{% else %}Your working directory must be within your user space.{% endif %} 47 |
48 |
49 |
50 |
51 |
52 |
53 | 54 | {% for name, group in groups.items %}
55 |
56 |
57 |
58 |

{{ name }}

59 |
60 |
{% for action_name, action in group.items %} 61 |
62 |
63 | {{ action.field | safe }} 64 |
65 |
66 |
{% endfor %} 67 |
68 |
69 |
70 |
{% endfor %} 71 | 72 | 73 | 74 |
75 | 76 | 77 |
78 | 79 | {% endblock %} 80 | {% block scripts %} 81 | 136 | {% endblock %} 137 | -------------------------------------------------------------------------------- /snakeface/apps/main/templates/workflows/report.html: -------------------------------------------------------------------------------- 1 | {{ report | safe }} 2 | -------------------------------------------------------------------------------- /snakeface/apps/main/templates/workflows/workflow_run_table.html: -------------------------------------------------------------------------------- 1 | {% load my_filters %} 2 |
3 |
4 |
5 |
6 | {% with fields=workflow.message_fields %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
OrderLevelJobMessage
18 | {% endwith %} 19 | 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /snakeface/apps/main/urls.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | from django.urls import path 6 | from . import views 7 | from . import tasks 8 | 9 | urlpatterns = [ 10 | path("", views.index, name="dashboard"), 11 | path("workflows/run//", tasks.run_workflow, name="run_workflow"), 12 | path("workflows/new/", views.new_workflow, name="new_workflow"), 13 | path("workflows//", views.view_workflow, name="view_workflow"), 14 | path("workflows/command/", views.workflow_command, name="workflow_command"), 15 | path( 16 | "workflows//statuses/", 17 | views.workflow_statuses, 18 | name="workflow_statuses", 19 | ), 20 | path("workflows//edit/", views.edit_workflow, name="edit_workflow"), 21 | path( 22 | "workflows//report/", 23 | views.view_workflow_report, 24 | name="view_workflow_report", 25 | ), 26 | path("workflows//delete/", views.delete_workflow, name="delete_workflow"), 27 | path("workflows//cancel/", views.cancel_workflow, name="cancel_workflow"), 28 | ] 29 | 30 | app_name = "main" 31 | -------------------------------------------------------------------------------- /snakeface/apps/main/utils.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | from snakeface.settings import cfg 6 | import subprocess 7 | import threading 8 | 9 | import tempfile 10 | import os 11 | import re 12 | 13 | 14 | def get_workdir_choices(path=None): 15 | """Given the working directory set on init, return potential subdirectories.""" 16 | path = path or cfg.WORKDIR 17 | choices = [(path, "/")] 18 | 19 | # Recursive to working directory is default 20 | for root, dirs, files in sorted(os.walk(path)): 21 | for f in sorted(dirs): 22 | if f == "__pycache__": 23 | continue 24 | fullpath = os.path.join(root, f) 25 | # Ignore all hidden files and paths 26 | if "/." in f or "/." in fullpath or "/." in root: 27 | continue 28 | choices.append((fullpath, fullpath)) 29 | return choices 30 | 31 | 32 | def get_snakefile_choices(path=None): 33 | """Given the working directory set on init, return all discovered snakefiles.""" 34 | path = path or cfg.WORKDIR 35 | choices = [] 36 | 37 | # Recursive to working directory is default 38 | for root, dirs, files in sorted(os.walk(path)): 39 | for f in sorted(files): 40 | fullpath = os.path.join(root, f) 41 | if re.search("Snakefile", f): 42 | choices.append((fullpath, fullpath)) 43 | return choices 44 | 45 | 46 | def write_file(filename, content): 47 | """Write some text content to a file""" 48 | with open(filename, "w") as fd: 49 | fd.write(content) 50 | return filename 51 | 52 | 53 | def read_file(filename): 54 | """Write some text content to a file""" 55 | with open(filename, "r") as fd: 56 | content = fd.read() 57 | return content 58 | 59 | 60 | def get_tmpfile(prefix="", suffix=""): 61 | """get a temporary file with an optional prefix. By default, the file 62 | is closed (and just a name returned). 63 | 64 | Arguments: 65 | - prefix (str) : prefix with this string 66 | """ 67 | tmpdir = tempfile.gettempdir() 68 | prefix = os.path.join(tmpdir, os.path.basename(prefix)) 69 | fd, tmp_file = tempfile.mkstemp(prefix=prefix, suffix=suffix) 70 | os.close(fd) 71 | return tmp_file 72 | 73 | 74 | class ThreadRunner(threading.Thread): 75 | """We need to be able to run a Snakemake job as a thread, and kill it if 76 | an exception is raised based on it's id 77 | """ 78 | 79 | def set_workflow(self, workflow): 80 | self.workflow = workflow 81 | 82 | @property 83 | def thread_id(self): 84 | """Return the id of the thread, either attributed to the class or 85 | by matching the Thread instance 86 | """ 87 | if hasattr(self, "_thread_id"): 88 | return self._thread_id 89 | for thread_id, thread in threading._active.items(): 90 | if thread is self: 91 | return thread_id 92 | 93 | 94 | class CommandRunner(object): 95 | """Wrapper to use subprocess to run a command. This is based off of pypi 96 | vendor distlib SubprocesMixin. 97 | """ 98 | 99 | def __init__(self): 100 | self.reset() 101 | 102 | def reset(self): 103 | self.error = [] 104 | self.output = [] 105 | self.retval = None 106 | 107 | def reader(self, stream, context): 108 | """Get output and error lines and save to command runner.""" 109 | # Make sure we save to the correct field 110 | lines = self.error 111 | if context == "stdout": 112 | lines = self.output 113 | 114 | while True: 115 | s = stream.readline() 116 | if not s: 117 | break 118 | lines.append(s.decode("utf-8")) 119 | stream.close() 120 | 121 | def run_command( 122 | self, cmd, env=None, cancel_func=None, cancel_func_kwargs=None, **kwargs 123 | ): 124 | self.reset() 125 | cancel_func_kwargs = cancel_func_kwargs or {} 126 | 127 | # If we need to update the environment 128 | # **IMPORTANT: this will include envars from host. Absolutely cannot 129 | # be any secrets (they should be defined in the app settings file) 130 | envars = os.environ.copy() 131 | if env: 132 | envars.update(env) 133 | 134 | p = subprocess.Popen( 135 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=envars, **kwargs 136 | ) 137 | 138 | # Create threads for error and output 139 | t1 = threading.Thread(target=self.reader, args=(p.stdout, "stdout")) 140 | t1.start() 141 | t2 = threading.Thread(target=self.reader, args=(p.stderr, "stderr")) 142 | t2.start() 143 | 144 | # Continue running unless cancel function is called 145 | counter = 0 146 | while True: 147 | 148 | # Check on process for finished or cancelled 149 | if p.poll() != None: 150 | print("Return value found, stopping.") 151 | break 152 | 153 | # Check the cancel function every 100 loops 154 | elif ( 155 | counter % 10000 == 0 156 | and cancel_func 157 | and cancel_func(**cancel_func_kwargs) 158 | ): 159 | print("Process is terminated") 160 | p.terminate() 161 | break 162 | counter += 1 163 | 164 | # p.wait() 165 | t1.join() 166 | t2.join() 167 | self.retval = p.returncode 168 | return self.output 169 | -------------------------------------------------------------------------------- /snakeface/apps/main/views.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | from django.shortcuts import render, redirect, get_object_or_404 6 | from django.http import HttpResponseForbidden, JsonResponse 7 | from django.contrib import messages 8 | import os 9 | 10 | from ratelimit.decorators import ratelimit 11 | from snakeface.argparser import SnakefaceParser 12 | from snakeface.settings import cfg 13 | from snakeface.apps.main.models import Workflow 14 | from snakeface.apps.main.forms import WorkflowForm 15 | from snakeface.apps.main.tasks import run_workflow, serialize_workflow_statuses 16 | from snakeface.apps.users.decorators import login_is_required 17 | from snakeface.settings import ( 18 | VIEW_RATE_LIMIT as rl_rate, 19 | VIEW_RATE_LIMIT_BLOCK as rl_block, 20 | ) 21 | 22 | 23 | # Dashboard 24 | 25 | 26 | @login_is_required 27 | @ratelimit(key="ip", rate=rl_rate, block=rl_block) 28 | def index(request): 29 | workflows = None 30 | if request.user.is_authenticated: 31 | workflows = Workflow.objects.filter(owners=request.user) 32 | return render( 33 | request, 34 | "main/index.html", 35 | {"workflows": workflows, "page_title": "Dashboard"}, 36 | ) 37 | 38 | 39 | # Workflows 40 | 41 | 42 | @login_is_required 43 | @ratelimit(key="ip", rate=rl_rate, block=rl_block) 44 | def delete_workflow(request, wid): 45 | workflow = get_object_or_404(Workflow, pk=wid) 46 | 47 | # Ensure that the user is an owner 48 | if request.user not in workflow.owners.all(): 49 | return HttpResponseForbidden() 50 | workflow.delete() 51 | return redirect("main:dashboard") 52 | 53 | 54 | @login_is_required 55 | @ratelimit(key="ip", rate=rl_rate, block=rl_block) 56 | def cancel_workflow(request, wid): 57 | workflow = get_object_or_404(Workflow, pk=wid) 58 | 59 | # Ensure that the user is an owner 60 | if request.user not in workflow.owners.all(): 61 | return HttpResponseForbidden() 62 | workflow.status = "CANCELLED" 63 | workflow.save() 64 | messages.info( 65 | request, "Your workflow has been cancelled, and will stop within 10 seconds." 66 | ) 67 | return redirect("main:view_workflow", wid=workflow.id) 68 | 69 | 70 | @login_is_required 71 | @ratelimit(key="ip", rate=rl_rate, block=rl_block) 72 | def edit_workflow(request, wid): 73 | 74 | workflow = get_object_or_404(Workflow, pk=wid) 75 | 76 | # Ensure that the user is an owner 77 | if request.user not in workflow.owners.all(): 78 | return HttpResponseForbidden() 79 | 80 | # Give a warning if the snakefile doesn't exist 81 | if not os.path.exists(workflow.snakefile): 82 | messages.warning( 83 | request, "Warning: This snakefile doesn't appear to exist anymore." 84 | ) 85 | 86 | # Create and update a parser with the current settings 87 | parser = SnakefaceParser() 88 | parser.load(workflow.data) 89 | return edit_or_update_workflow(request, workflow=workflow, parser=parser) 90 | 91 | 92 | @login_is_required 93 | @ratelimit(key="ip", rate=rl_rate, block=rl_block) 94 | def new_workflow(request): 95 | parser = SnakefaceParser() 96 | if request.user.is_authenticated: 97 | return edit_or_update_workflow(request, parser=parser) 98 | return HttpResponseForbidden() 99 | 100 | 101 | def edit_or_update_workflow(request, parser, workflow=None): 102 | """A shared function to edit or update an existing workflow.""" 103 | 104 | # Ensure the user has permission to update 105 | if workflow: 106 | existed = True 107 | action = "update" 108 | if request.user not in workflow.owners.all(): 109 | return HttpResponseForbidden() 110 | else: 111 | workflow = Workflow() 112 | action = "create" 113 | existed = False 114 | 115 | form = WorkflowForm(request.POST or None, instance=workflow) 116 | 117 | # Case 1: parse a provided form to update current data 118 | if request.method == "POST" and form.is_valid(): 119 | 120 | for arg, setting in request.POST.items(): 121 | parser.set(arg, setting) 122 | 123 | # Has the user gone over the workflow number limit? 124 | if ( 125 | Workflow.objects.filter(owners=request.user).count() 126 | >= cfg.USER_WORKFLOW_LIMIT 127 | ): 128 | messages.info( 129 | request, "You are at the workflow limit of %s" % cfg.USER_WORKFLOW_LIMIT 130 | ) 131 | elif not parser.validate(): 132 | messages.info(request, parser.errors) 133 | else: 134 | print("Creating workflow") 135 | workflow = form.save() 136 | workflow.data = parser.to_dict() 137 | workflow.snakefile = parser.snakefile 138 | workflow.workdir = request.POST.get("workdirs", cfg.WORKDIR) 139 | workflow.private = ( 140 | True if cfg.PRIVATE_ONLY else request.POST.get("private", 1) == 1 141 | ) 142 | workflow.owners.add(request.user) 143 | # Save updates the dag and command 144 | workflow.save() 145 | return run_workflow(request=request, wid=workflow.id, uid=request.user.id) 146 | 147 | # Case 2: no snakefiles: 148 | if not parser.snakefiles: 149 | message = ( 150 | "No Snakefiles were found in any path under %s." 151 | " You must have one to %s a workflow." % (cfg.WORKDIR, action) 152 | ) 153 | messages.info(request, message) 154 | return redirect("main:dashboard") 155 | 156 | # Case 3: Render an empty form with current working directory 157 | if existed: 158 | form.fields["workdirs"].initial = workflow.workdir 159 | return render( 160 | request, 161 | "workflows/new.html", 162 | { 163 | "groups": parser.groups, 164 | "page_title": "%s Workflow" % action.capitalize(), 165 | "form": form, 166 | "workflow_id": getattr(workflow, "id", None), 167 | }, 168 | ) 169 | 170 | 171 | @login_is_required 172 | @ratelimit(key="ip", rate=rl_rate, block=rl_block) 173 | def workflow_command(request): 174 | """is called from the browser via POST to update the command""" 175 | parser = SnakefaceParser() 176 | if request.method == "POST": 177 | for arg, setting in request.POST.items(): 178 | parser.set(arg, setting) 179 | return JsonResponse({"command": parser.command}) 180 | 181 | 182 | @login_is_required 183 | def workflow_statuses(request, wid): 184 | """return serialized workflow statuses for the details view.""" 185 | workflow = get_object_or_404(Workflow, pk=wid) 186 | return JsonResponse({"data": serialize_workflow_statuses(workflow)}) 187 | 188 | 189 | @login_is_required 190 | @ratelimit(key="ip", rate=rl_rate, block=rl_block) 191 | def view_workflow(request, wid): 192 | 193 | workflow = get_object_or_404(Workflow, pk=wid) 194 | return render( 195 | request, 196 | "workflows/detail.html", 197 | { 198 | "workflow": workflow, 199 | "page_title": "%s: %s" % (workflow.name or "Workflow", workflow.id), 200 | }, 201 | ) 202 | 203 | 204 | def view_workflow_report(request, wid): 205 | """If a workflow generated a report and the report exists, render it to a page""" 206 | workflow = get_object_or_404(Workflow, pk=wid) 207 | report = workflow.get_report() 208 | if not report: 209 | messages.info(request, "This workflow does not have a report file.") 210 | redirect("main:view_workflow", wid=workflow.id) 211 | return render( 212 | request, 213 | "workflows/report.html", 214 | {"workflow": workflow, "page_title": "Report", "report": report}, 215 | ) 216 | -------------------------------------------------------------------------------- /snakeface/apps/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snakemake/snakeface/0928c8607608c0518d6cb279a4a506c5e1f85431/snakeface/apps/users/__init__.py -------------------------------------------------------------------------------- /snakeface/apps/users/admin.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | from django.contrib.auth.admin import UserAdmin 6 | from django.contrib import admin 7 | from snakeface.apps.users.models import User 8 | 9 | admin.site.register(User, UserAdmin) 10 | -------------------------------------------------------------------------------- /snakeface/apps/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = "users" 6 | -------------------------------------------------------------------------------- /snakeface/apps/users/decorators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __author__ = "Vanessa Sochat" 4 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 5 | __license__ = "MPL 2.0" 6 | 7 | from django.contrib.auth import REDIRECT_FIELD_NAME 8 | from django.shortcuts import resolve_url, redirect 9 | from snakeface.settings import cfg 10 | from snakeface import settings 11 | from urllib.parse import urlparse 12 | 13 | 14 | def login_is_required( 15 | function=None, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME 16 | ): 17 | """ 18 | Decorator to extend login required to also check if a notebook auth is 19 | desired first. 20 | """ 21 | 22 | def wrap(request, *args, **kwargs): 23 | 24 | # If we are using a notebook, the user is required to login with a token 25 | if (cfg.NOTEBOOK or cfg.NOTEBOOK_ONLY) and not request.user.is_authenticated: 26 | return redirect("users:notebook_login") 27 | 28 | # If we have the token in the session 29 | elif ( 30 | cfg.NOTEBOOK 31 | or cfg.NOTEBOOK_ONLY 32 | and request.session.get("notebook_auth") 33 | == request.session.get("notebook_token") 34 | ): 35 | return function(request, *args, **kwargs) 36 | 37 | # If the user is authenticated, return the view right away 38 | elif request.user.is_authenticated: 39 | return function(request, *args, **kwargs) 40 | 41 | # Otherwise, prepare login url (from django user_passes_test) 42 | # https://github.com/django/django/blob/master/django/contrib/auth/decorators.py#L10 43 | path = request.build_absolute_uri() 44 | resolved_login_url = resolve_url(login_url or settings.LOGIN_URL) 45 | login_scheme, login_netloc = urlparse(resolved_login_url)[:2] 46 | current_scheme, current_netloc = urlparse(path)[:2] 47 | if (not login_scheme or login_scheme == current_scheme) and ( 48 | not login_netloc or login_netloc == current_netloc 49 | ): 50 | path = request.get_full_path() 51 | from django.contrib.auth.views import redirect_to_login 52 | 53 | return redirect_to_login(path, resolved_login_url, redirect_field_name) 54 | 55 | wrap.__doc__ = function.__doc__ 56 | wrap.__name__ = function.__name__ 57 | return wrap 58 | -------------------------------------------------------------------------------- /snakeface/apps/users/forms.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | from django import forms 6 | 7 | 8 | class TokenForm(forms.Form): 9 | token = forms.CharField(label="Notebook token", max_length=100) 10 | -------------------------------------------------------------------------------- /snakeface/apps/users/models.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | from django.conf import settings 6 | from django.contrib.auth.models import AbstractUser, BaseUserManager 7 | from django.db.models.signals import post_save 8 | from django.dispatch import receiver 9 | 10 | from django.db import models 11 | from rest_framework.authtoken.models import Token 12 | 13 | 14 | class CustomUserManager(BaseUserManager): 15 | def _create_user( 16 | self, username, email, password, is_staff, is_superuser, **extra_fields 17 | ): 18 | """ 19 | Creates and saves a User with the given username, email and password. 20 | """ 21 | if not username: 22 | raise ValueError("The given username must be set") 23 | 24 | email = self.normalize_email(email) 25 | user = self.model( 26 | username=username, 27 | email=email, 28 | is_staff=is_staff, 29 | is_active=True, 30 | **extra_fields 31 | ) 32 | user.set_password(password) 33 | user.save(using=self._db) 34 | return user 35 | 36 | def create_user(self, username, email=None, password=None, **extra_fields): 37 | return self._create_user( 38 | username, email, password, False, False, **extra_fields 39 | ) 40 | 41 | def create_superuser(self, username, email, password, **extra_fields): 42 | return self._create_user(username, email, password, True, True, **extra_fields) 43 | 44 | def add_superuser(self, user): 45 | """Intended for existing user""" 46 | user.is_superuser = True 47 | user.save(using=self._db) 48 | return user 49 | 50 | def add_staff(self, user): 51 | """Intended for existing user""" 52 | user.is_staff = True 53 | user.save(using=self._db) 54 | return user 55 | 56 | 57 | class User(AbstractUser): 58 | active = models.BooleanField(default=True) 59 | 60 | # has the user agreed to terms? 61 | agree_terms = models.BooleanField(default=False) 62 | agree_terms_date = models.DateTimeField(blank=True, default=None, null=True) 63 | 64 | # Notebook token (only for terminal notebook) 65 | notebook_token = models.CharField( 66 | max_length=36, default=None, null=True, blank=True 67 | ) 68 | 69 | # Ensure that we can add staff / superuser and retain on logout 70 | objects = CustomUserManager() 71 | 72 | class Meta: 73 | app_label = "users" 74 | 75 | @property 76 | def token(self): 77 | """The user token is for interaction with creating and updating workflows""" 78 | return str(Token.objects.get(user=self)) 79 | 80 | def has_create_permission(self): 81 | """has create permission determines if the user (globally) can create 82 | new collections. By default, superusers and admin can, along with 83 | regular users if USER_COLLECTIONS is True. Otherwise, not. 84 | """ 85 | if self.is_superuser is True or self.is_staff is True: 86 | return True 87 | if settings.USER_COLLECTIONS is True: 88 | return True 89 | return False 90 | 91 | def get_credentials(self, provider): 92 | """return one or more credentials, or None""" 93 | if self.is_anonymous is False: 94 | try: 95 | # Case 1: one credential 96 | credential = self.social_auth.get(provider=provider) 97 | return credential 98 | except: 99 | # Case 2: more than one credential for the provider 100 | credential = self.social_auth.filter(provider=provider) 101 | if credential: 102 | return credential.last() 103 | 104 | def get_providers(self): 105 | """return a list of providers that the user has credentials for.""" 106 | return [x.provider for x in self.social_auth.all()] 107 | 108 | def get_label(self): 109 | return "users" 110 | 111 | 112 | @receiver(post_save, sender=settings.AUTH_USER_MODEL) 113 | def create_auth_token(sender, instance=None, created=False, **kwargs): 114 | """Create a token for the user when the user is created (with oAuth2) 115 | 116 | 1. Assign user a token 117 | 2. Assign user to default group 118 | 119 | Create a Profile instance for all newly created User instances. We only 120 | run on user creation to avoid having to check for existence on each call 121 | to User.save. 122 | 123 | """ 124 | # This auth token is intended for APIs 125 | if created and "api" in settings.PLUGINS_ENABLED: 126 | Token.objects.create(user=instance) 127 | -------------------------------------------------------------------------------- /snakeface/apps/users/templates/login/notebook.html: -------------------------------------------------------------------------------- 1 | {% extends "base/base.html" %} 2 | {% load static %} 3 | {% block content %} 4 |
5 |
6 |
7 |
8 |
9 | 10 |
11 | {% csrf_token %} 12 |
{% for messages in messages %}{{ message }}{% endfor %} 13 |

Welcome to Snakeface!

14 | 15 | The notebook token should be printed in the console where you ran the application. 16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | {% endblock %} 25 | {% block scripts %} 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /snakeface/apps/users/urls.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | from django.conf.urls import url, include 6 | from snakeface.apps.users import views 7 | from social_django import urls as social_urls 8 | 9 | urlpatterns = [ 10 | # url(r"^login/$", views.login, name="login"), 11 | url(r"^login/notebook/$", views.notebook_login, name="notebook_login"), 12 | # url(r"^accounts/login/$", views.login), 13 | url(r"^logout/$", views.logout, name="logout"), 14 | url("", include(social_urls, namespace="social")), 15 | ] 16 | 17 | app_name = "users" 18 | -------------------------------------------------------------------------------- /snakeface/apps/users/utils.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | import uuid 6 | import os 7 | 8 | 9 | def get_notebook_token(request, verbose=True): 10 | """If a notebook token isn't defined, generate it (and print to the console) 11 | The token is used to generate a user to log the user in. 12 | """ 13 | # If the user has already logged in, return current token 14 | token = request.session.get("notebook_token") 15 | 16 | # The user session has ended, but has authenticated before 17 | user = get_notebook_user() 18 | if request.user.is_authenticated: 19 | token = request.user.notebook_token 20 | 21 | # Second attempt, see if user has logged in before and retrieve token 22 | elif user: 23 | token = user.notebook_token 24 | 25 | if not token: 26 | token = str(uuid.uuid4()) 27 | user = get_or_create_notebook_user(token) 28 | request.session["notebook_token"] = token 29 | request.session.modified = True 30 | if verbose: 31 | print("Enter token: %s" % token) 32 | return token 33 | 34 | 35 | def get_username(): 36 | """get the username based on the effective uid. This is for a notebook 37 | execution, and doesn't add any additional security, but rather is used for 38 | personalization and being able to create an associated django user. 39 | """ 40 | try: 41 | import pwd 42 | 43 | return pwd.getpwuid(os.getuid())[0] 44 | except: 45 | return "snakeface-user" 46 | 47 | 48 | def get_notebook_user(): 49 | """Get the notebook user, if they have logged in before.""" 50 | from snakeface.apps.users.models import User 51 | from snakeface.settings import cfg 52 | 53 | try: 54 | return User.objects.get(username=cfg.USERNAME) 55 | except: 56 | return None 57 | 58 | 59 | def get_or_create_notebook_user(token): 60 | """Get or create the notebook user. Imports are done in the function because 61 | Django startup (settings.py) uses these functions. 62 | """ 63 | from snakeface.apps.users.models import User 64 | from snakeface.settings import cfg 65 | 66 | try: 67 | user = User.objects.get(username=cfg.USERNAME) 68 | except: 69 | user = User.objects.create_user(cfg.USERNAME, None, token) 70 | user.notebook_token = token 71 | user.save() 72 | return user 73 | -------------------------------------------------------------------------------- /snakeface/apps/users/views.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | 6 | from django.contrib.auth import logout as auth_logout, authenticate, login 7 | from django.contrib import messages 8 | from django.shortcuts import render, redirect 9 | from ratelimit.decorators import ratelimit 10 | from snakeface.apps.users.forms import TokenForm 11 | from snakeface.apps.users.decorators import login_is_required 12 | 13 | from snakeface.apps.users.utils import get_notebook_token 14 | from snakeface.settings import ( 15 | VIEW_RATE_LIMIT as rl_rate, 16 | VIEW_RATE_LIMIT_BLOCK as rl_block, 17 | cfg, 18 | ) 19 | 20 | 21 | @login_is_required 22 | def logout(request): 23 | """log the user out, either from the notebook or traditional Django auth""" 24 | auth_logout(request) 25 | 26 | # Notebook: delete both tokens to ensure we generate a new one on logout 27 | if cfg.NOTEBOOK or cfg.NOTEBOOK_ONLY: 28 | return redirect("users:notebook_login") 29 | 30 | # A traditional Django authentication is here 31 | return redirect("/") 32 | 33 | 34 | @ratelimit(key="ip", rate=rl_rate, block=rl_block) 35 | def notebook_login(request): 36 | """Given the user doesn't have a token in the request session, ask for it.""" 37 | # If they came to page directly, we need to generate the token 38 | valid_token = get_notebook_token(request) 39 | form = TokenForm() 40 | 41 | # If the user is submitting the form, validate it 42 | if request.method == "POST": 43 | form = TokenForm(request.POST) 44 | if form.is_valid(): 45 | 46 | # If the form is valid, get/create the user and log in 47 | if form.cleaned_data["token"] == valid_token: 48 | user = authenticate(username=cfg.USERNAME, password=valid_token) 49 | if not user: 50 | messages.warning(request, "That token is not valid.") 51 | else: 52 | login(request, user) 53 | return redirect("main:dashboard") 54 | else: 55 | messages.warning(request, "That token is not valid.") 56 | else: 57 | return render(request, "login/notebook.html", {"form": form}) 58 | 59 | # If a user is already authenticated, redirect to dashboard 60 | if request.user.is_authenticated: 61 | return redirect("main:dashboard") 62 | 63 | # If the token isn't provided, they need to provide it 64 | return render(request, "login/notebook.html", {"form": form}) 65 | -------------------------------------------------------------------------------- /snakeface/argparser.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | from snakemake import get_argument_parser 6 | from snakeface.settings import cfg 7 | from snakeface import settings 8 | from snakeface.apps.main.utils import get_snakefile_choices 9 | from jinja2 import Template 10 | import logging 11 | import os 12 | import json 13 | 14 | logger = logging.getLogger("argparser") 15 | 16 | # Prepare path to templates 17 | here = os.path.abspath(os.path.dirname(__file__)) 18 | templates = os.path.join(here, "apps", "main", "templates", "forms") 19 | 20 | 21 | class SnakefaceParser: 22 | """A Snakeface Parser is a wrapper to an argparse.Parser, and aims 23 | to make it easy to loop over arguments and options, and generate various 24 | representations (e.g., an input field) for the interface. The point is 25 | not to use it to parse arguments and validate, but to output all 26 | fields to a front end form. 27 | """ 28 | 29 | # Update the listing of snakefiles on the parser init 30 | snakefiles = get_snakefile_choices() 31 | 32 | def __init__(self): 33 | """load the parser, optionally specifying a profile""" 34 | self.parser = get_argument_parser() 35 | self._groups = {} 36 | self._args = {} 37 | self.groups 38 | self._errors = [] 39 | 40 | # A profile can further customize job submission 41 | if cfg.PROFILE and os.path.exists(cfg.PROFILE): 42 | print("Loading profile %s" % cfg.PROFILE) 43 | self.parser = get_argument_parser(cfg.PROFILE) 44 | 45 | def __str__(self): 46 | return "[snakeface-parser]" 47 | 48 | def __repr__(self): 49 | return self.__str__() 50 | 51 | @property 52 | def errors(self): 53 | return " ".join(self._errors) 54 | 55 | def get(self, name, default=None): 56 | """A general get function to return an argument that might be nested under 57 | a group. These objects are the same as linked in _groups. 58 | """ 59 | return self._args.get(name, default) 60 | 61 | def load(self, argdict): 62 | """Load is a wrapper around set - we loop through a dictionary and set all 63 | arguments. 64 | """ 65 | if isinstance(argdict, str): 66 | argdict = json.loads(argdict) 67 | for key, value in argdict.items(): 68 | arg = self._args.get(key) 69 | if arg: 70 | arg.value = value 71 | 72 | def set(self, name, value): 73 | """Set a value for an argument. This is typically what the user has selected.""" 74 | arg = self._args.get(name) 75 | if arg: 76 | arg.value = value 77 | 78 | def to_dict(self): 79 | """the opposite of load, this function exports an argument""" 80 | return {name: arg.value for name, arg in self._args.items()} 81 | 82 | @property 83 | def snakefile(self): 84 | snakefile = self._args.get("snakefile") 85 | if snakefile: 86 | return snakefile.value 87 | 88 | def validate(self): 89 | """ensure that all required args are defined""" 90 | valid = True 91 | for key in self.required: 92 | if not self._args.get(key): 93 | self._errors.append("The %s is required." % key) 94 | valid = False 95 | return valid 96 | 97 | @property 98 | def required(self): 99 | return ["cores", "snakefile"] 100 | 101 | @property 102 | def command(self): 103 | """Given a loaded set of arguments, generate the command.""" 104 | command = "snakemake" 105 | 106 | for name, arg in self._args.items(): 107 | if arg.value: 108 | 109 | # If the value is set to the default, ignore it 110 | if arg.value == arg.action["default"] and name not in self.required: 111 | continue 112 | 113 | flag = "" 114 | if arg.action["option_strings"]: 115 | flag = arg.action["option_strings"][0] 116 | 117 | # Assemble argument based on type 118 | if arg.is_boolean: 119 | command += " %s" % flag 120 | else: 121 | command += " %s %s" % (flag, arg.value) 122 | 123 | return command 124 | 125 | @property 126 | def groups(self): 127 | """yield arguments organized by groups, with the intention to easily map 128 | into a form on the front end. The groups seem to have ALL arguments each, 129 | so we have to artificially separate them. 130 | """ 131 | if self._groups: 132 | return self._groups 133 | 134 | # Generate an argument lookup based on dest 135 | lookup = {} 136 | for action in self.parser._actions: 137 | lookup[action.dest] = SnakefaceArgument( 138 | action, action.dest in self.required 139 | ) 140 | 141 | # Define choices 142 | if action.dest == "snakefile": 143 | lookup[action.dest].update_choice_fields({"snakefile": self.snakefiles}) 144 | 145 | # Set the wms monitor to be this server 146 | if action.dest == "wms_monitor": 147 | lookup[action.dest].value = settings.DOMAIN_NAME 148 | 149 | # This top level organizes into groups 150 | for group in self.parser._action_groups: 151 | group_dict = { 152 | a.dest: lookup.get(a.dest) 153 | for a in group._group_actions 154 | if self.include_argument(a.dest, group.title) 155 | } 156 | 157 | # Store a flattened representation to manipulate later 158 | self._args.update(group_dict) 159 | 160 | # Don't add empty groups 161 | if group_dict: 162 | self._groups[group.title] = group_dict 163 | return self._groups 164 | 165 | def include_argument(self, name, group): 166 | """Given an argument name, and a group name, skip if settings disable 167 | it 168 | """ 169 | # Never include these named arguments 170 | if name in ["help", "version"]: 171 | return False 172 | 173 | # Skip groups based on specific configuration settings 174 | if not cfg.EXECUTOR_CLUSTER and group == "CLUSTER": 175 | return False 176 | if not cfg.EXECUTOR_GOOGLE_LIFE_SCIENCES and group == "GOOGLE_LIFE_SCIENCE": 177 | return False 178 | if not cfg.EXECUTOR_KUBERNETES and group == "KUBERNETES": 179 | return False 180 | if not cfg.EXECUTOR_TIBANNA and group == "TIBANNA": 181 | return False 182 | if not cfg.EXECUTOR_TIBANNA and group == "TIBANNA": 183 | return False 184 | if not cfg.EXECUTOR_GA4GH_TES and group == "TES": 185 | return False 186 | if cfg.DISABLE_SINGULARITY and group == "SINGULARITY": 187 | return False 188 | if cfg.DISABLE_CONDA and group == "CONDA": 189 | return False 190 | if cfg.DISABLE_NOTEBOOKS and group == "NOTEBOOKS": 191 | return False 192 | return True 193 | 194 | 195 | class SnakefaceArgument: 196 | """A Snakeface argument takes an action from a parser, and is able to 197 | easily generate front end views (e.g., a form element) for it 198 | """ 199 | 200 | def __init__(self, action, required=False): 201 | self.action = action.__dict__ 202 | self.boolean_template = "" 203 | self.text_template = "" 204 | self.choice_template = "" 205 | self.choice_fields = {} 206 | self.value = "" 207 | self.required = required 208 | 209 | def __str__(self): 210 | return self.action["dest"] 211 | 212 | def __repr__(self): 213 | return self.__str__() 214 | 215 | def update_choice_fields(self, updates): 216 | self.choice_fields.update(updates) 217 | 218 | @property 219 | def field_name(self): 220 | return " ".join([x.capitalize() for x in self.action["dest"].split("_")]) 221 | 222 | @property 223 | def is_boolean(self): 224 | return self.action["nargs"] == 0 and self.action["const"] 225 | 226 | def field(self): 227 | """generate a form field for the argument""" 228 | if self.action["dest"] in self.choice_fields: 229 | return self.choice_field() 230 | if self.is_boolean: 231 | return self.boolean_field() 232 | return self.text_field() 233 | 234 | def load_template(self, path): 235 | """Given a path to a template file, load the template with jinja2""" 236 | if os.path.exists(path): 237 | with open(path, "r") as fd: 238 | template = Template(fd.read()) 239 | return template 240 | logging.warning("%s does not exist, no template loaded.") 241 | return "" 242 | 243 | def boolean_field(self): 244 | """generate a boolean field (radio button) via a jinja2 template""" 245 | # Ensure that we only load/read the file once 246 | if not self.boolean_template: 247 | self.boolean_template = self.load_template( 248 | os.path.join(templates, "boolean_field.html") 249 | ) 250 | checked = "checked" if self.action["default"] == True else "" 251 | return self.boolean_template.render( 252 | label=self.field_name, 253 | help=self.action["help"], 254 | name=self.action["dest"], 255 | checked=checked, 256 | required="required" if self.required else "", 257 | ) 258 | 259 | def text_field(self): 260 | """generate a text field for using a pre-loaded jinja2 template""" 261 | if not self.text_template: 262 | self.text_template = self.load_template( 263 | os.path.join(templates, "text_field.html") 264 | ) 265 | 266 | return self.text_template.render( 267 | name=self.action["dest"], 268 | default=self.action["default"] or self.value, 269 | label=self.field_name, 270 | help=self.action["help"], 271 | required="required" if self.required else "", 272 | ) 273 | 274 | def choice_field(self): 275 | """generate a choice field for using a pre-loaded jinja2 template""" 276 | if not self.choice_template: 277 | self.choice_template = self.load_template( 278 | os.path.join(templates, "choice_field.html") 279 | ) 280 | 281 | return self.choice_template.render( 282 | name=self.action["dest"], 283 | label=self.field_name, 284 | help=self.action["help"], 285 | required="required" if self.required else "", 286 | choices=self.choice_fields.get(self.action["dest"]), 287 | ) 288 | -------------------------------------------------------------------------------- /snakeface/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from channels.auth import AuthMiddlewareStack 4 | from channels.routing import ProtocolTypeRouter, URLRouter 5 | from django.core.asgi import get_asgi_application 6 | from snakeface.apps.main import routing 7 | 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "snakeface.settings") 9 | 10 | application = ProtocolTypeRouter( 11 | { 12 | "http": get_asgi_application(), 13 | "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), 14 | } 15 | ) 16 | -------------------------------------------------------------------------------- /snakeface/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __author__ = "Vanessa Sochat" 4 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 5 | __license__ = "MPL 2.0" 6 | 7 | from snakeface.logger import setup_logger 8 | from django.core.wsgi import get_wsgi_application 9 | from django.core import management 10 | from snakeface.version import __version__ 11 | import argparse 12 | import sys 13 | import os 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "snakeface.settings") 16 | 17 | 18 | def get_parser(): 19 | parser = argparse.ArgumentParser(description="Snakeface: interface to snakemake.") 20 | 21 | parser.add_argument( 22 | "--version", 23 | dest="version", 24 | help="print the version and exit.", 25 | default=False, 26 | action="store_true", 27 | ) 28 | 29 | parser.add_argument( 30 | "--noreload", 31 | dest="noreload", 32 | help="Tells Django to NOT use the auto-reloader.", 33 | default=False, 34 | action="store_true", 35 | ) 36 | 37 | parser.add_argument( 38 | "--verbosity", 39 | dest="verbosity", 40 | help="Verbosity (0, 1, 2, 3).", 41 | choices=list(range(0, 4)), 42 | default=0, 43 | ) 44 | 45 | parser.add_argument( 46 | "--workdir", dest="workdir", help="Specify the working directory.", nargs="?" 47 | ) 48 | 49 | deploy_group = parser.add_argument_group("SETTINGS") 50 | deploy_group.add_argument( 51 | "--auth", 52 | dest="auth", 53 | help="Authentication type to create for the interface, defaults to token.", 54 | choices=["token"], 55 | default="token", 56 | ) 57 | 58 | network_group = parser.add_argument_group("NETWORKING") 59 | network_group.add_argument( 60 | "--port", dest="port", help="Port to serve application on.", default=5000 61 | ) 62 | 63 | # Logging 64 | logging_group = parser.add_argument_group("LOGGING") 65 | 66 | logging_group.add_argument( 67 | "--quiet", 68 | dest="quiet", 69 | help="suppress logging.", 70 | default=False, 71 | action="store_true", 72 | ) 73 | 74 | logging_group.add_argument( 75 | "--verbose", 76 | dest="verbose", 77 | help="verbose output for logging.", 78 | default=False, 79 | action="store_true", 80 | ) 81 | 82 | logging_group.add_argument( 83 | "--log-disable-color", 84 | dest="disable_color", 85 | default=False, 86 | help="Disable color for snakeface logging.", 87 | action="store_true", 88 | ) 89 | 90 | logging_group.add_argument( 91 | "--log-use-threads", 92 | dest="use_threads", 93 | action="store_true", 94 | help="Force threads rather than processes.", 95 | ) 96 | 97 | description = "subparsers for Snakeface" 98 | subparsers = parser.add_subparsers( 99 | help="snakeface actions", 100 | title="actions", 101 | description=description, 102 | dest="command", 103 | ) 104 | 105 | # print version and exit 106 | subparsers.add_parser("notebook", help="run a snakeface notebook") 107 | 108 | return parser 109 | 110 | 111 | def main(): 112 | """main entrypoint for snakeface""" 113 | parser = get_parser() 114 | 115 | def help(return_code=0): 116 | """print help, including the software version and active client 117 | and exit with return code. 118 | """ 119 | print("\nSnakemake Interface v%s" % __version__) 120 | parser.print_help() 121 | sys.exit(return_code) 122 | 123 | # If an error occurs while parsing the arguments, the interpreter will exit with value 2 124 | args, extra = parser.parse_known_args() 125 | 126 | # Show the version and exit 127 | if args.version: 128 | print(__version__) 129 | sys.exit(0) 130 | 131 | # Do we want a notebook? 132 | notebook = args.command == "notebook" 133 | if notebook: 134 | os.environ["SNAKEFACE_NOTEBOOK"] = "yes" 135 | os.putenv("SNAKEFACE_NOTEBOOK", "yes") 136 | 137 | # If a working directory is set 138 | if not args.workdir or args.workdir == ".": 139 | args.workdir = os.getcwd() 140 | if args.workdir: 141 | os.environ["SNAKEFACE_WORKDIR"] = args.workdir 142 | os.putenv("SNAKEFACE_WORKDIR", args.workdir) 143 | 144 | application = get_wsgi_application() 145 | 146 | # customize django logging 147 | setup_logger( 148 | quiet=args.quiet, 149 | nocolor=args.disable_color, 150 | debug=args.verbose, 151 | use_threads=args.use_threads, 152 | ) 153 | 154 | # Migrations 155 | management.call_command("makemigrations", verbosity=args.verbosity) 156 | for app in ["users", "main", "base"]: 157 | management.call_command("makemigrations", app, verbosity=args.verbosity) 158 | management.call_command("migrate", verbosity=args.verbosity) 159 | 160 | # management.call_command("qcluster", verbosity=args.verbosity) 161 | management.call_command( 162 | "collectstatic", verbosity=args.verbosity, interactive=False 163 | ) 164 | management.call_command( 165 | "runserver", args.port, verbosity=args.verbosity, noreload=not args.noreload 166 | ) 167 | sys.exit(0) 168 | 169 | 170 | if __name__ == "__main__": 171 | main() 172 | -------------------------------------------------------------------------------- /snakeface/context_processors.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | from django.contrib.sites.shortcuts import get_current_site 6 | from snakeface import settings 7 | 8 | 9 | def globals(request): 10 | """Returns a dict of defaults to be used by templates, if configured 11 | correcty in the settings.py file.""" 12 | return { 13 | "DOMAIN": settings.DOMAIN_NAME, 14 | "WORKFLOW_UPDATE_SECONDS": settings.cfg.WORKFLOW_UPDATE_SECONDS, 15 | "NOTEBOOK": settings.cfg.NOTEBOOK, 16 | "TWITTER_USERNAME": settings.cfg.TWITTER_USERNAME, 17 | "GITHUB_REPOSITORY": settings.cfg.GITHUB_REPOSITORY, 18 | "GITHUB_DOCUMENTATION": settings.cfg.GITHUB_DOCUMENTATION, 19 | "SITE_NAME": get_current_site(request).name, 20 | "GOOGLE_ANALYTICS_ID": settings.cfg.GOOGLE_ANALYTICS_ID, 21 | "GOOGLE_ANALYTICS_SITE": settings.cfg.GOOGLE_ANALYTICS_SITE, 22 | } 23 | -------------------------------------------------------------------------------- /snakeface/logger.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | import logging as _logging 6 | import platform 7 | import sys 8 | import os 9 | import threading 10 | import inspect 11 | 12 | 13 | class ColorizingStreamHandler(_logging.StreamHandler): 14 | 15 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) 16 | RESET_SEQ = "\033[0m" 17 | COLOR_SEQ = "\033[%dm" 18 | BOLD_SEQ = "\033[1m" 19 | 20 | colors = { 21 | "WARNING": YELLOW, 22 | "INFO": GREEN, 23 | "DEBUG": BLUE, 24 | "CRITICAL": RED, 25 | "ERROR": RED, 26 | } 27 | 28 | def __init__(self, nocolor=False, stream=sys.stderr, use_threads=False): 29 | super().__init__(stream=stream) 30 | self._output_lock = threading.Lock() 31 | self.nocolor = nocolor or not self.can_color_tty() 32 | 33 | def can_color_tty(self): 34 | if "TERM" in os.environ and os.environ["TERM"] == "dumb": 35 | return False 36 | return self.is_tty and not platform.system() == "Windows" 37 | 38 | @property 39 | def is_tty(self): 40 | isatty = getattr(self.stream, "isatty", None) 41 | return isatty and isatty() 42 | 43 | def emit(self, record): 44 | with self._output_lock: 45 | try: 46 | self.format(record) # add the message to the record 47 | self.stream.write(self.decorate(record)) 48 | self.stream.write(getattr(self, "terminator", "\n")) 49 | self.flush() 50 | except BrokenPipeError as e: 51 | raise e 52 | except (KeyboardInterrupt, SystemExit): 53 | # ignore any exceptions in these cases as any relevant messages have been printed before 54 | pass 55 | except Exception: 56 | self.handleError(record) 57 | 58 | def decorate(self, record): 59 | message = record.message 60 | message = [message] 61 | if not self.nocolor and record.levelname in self.colors: 62 | message.insert(0, self.COLOR_SEQ % (30 + self.colors[record.levelname])) 63 | message.append(self.RESET_SEQ) 64 | return "".join(message) 65 | 66 | 67 | class Logger: 68 | def __init__(self): 69 | self.logger = _logging.getLogger(__name__) 70 | self.log_handler = [self.text_handler] 71 | self.stream_handler = None 72 | self.printshellcmds = False 73 | self.quiet = False 74 | self.logfile = None 75 | self.last_msg_was_job_info = False 76 | self.logfile_handler = None 77 | 78 | def cleanup(self): 79 | if self.logfile_handler is not None: 80 | self.logger.removeHandler(self.logfile_handler) 81 | self.logfile_handler.close() 82 | self.log_handler = [self.text_handler] 83 | 84 | def handler(self, msg): 85 | for handler in self.log_handler: 86 | handler(msg) 87 | 88 | def set_stream_handler(self, stream_handler): 89 | if self.stream_handler is not None: 90 | self.logger.removeHandler(self.stream_handler) 91 | self.stream_handler = stream_handler 92 | self.logger.addHandler(stream_handler) 93 | 94 | def set_level(self, level): 95 | self.logger.setLevel(level) 96 | 97 | def location(self, msg): 98 | callerframerecord = inspect.stack()[1] 99 | frame = callerframerecord[0] 100 | info = inspect.getframeinfo(frame) 101 | self.debug( 102 | "{}: {info.filename}, {info.function}, {info.lineno}".format(msg, info=info) 103 | ) 104 | 105 | def info(self, msg): 106 | self.handler(dict(level="info", msg=msg)) 107 | 108 | def warning(self, msg): 109 | self.handler(dict(level="warning", msg=msg)) 110 | 111 | def debug(self, msg): 112 | self.handler(dict(level="debug", msg=msg)) 113 | 114 | def error(self, msg): 115 | self.handler(dict(level="error", msg=msg)) 116 | 117 | def exit(self, msg, return_code=1): 118 | self.handler(dict(level="error", msg=msg)) 119 | sys.exit(return_code) 120 | 121 | def progress(self, done=None, total=None): 122 | self.handler(dict(level="progress", done=done, total=total)) 123 | 124 | def shellcmd(self, msg): 125 | if msg is not None: 126 | msg = dict(level="shellcmd", msg=msg) 127 | self.handler(msg) 128 | 129 | def text_handler(self, msg): 130 | """The default snakemake log handler. 131 | Prints the output to the console. 132 | Args: 133 | msg (dict): the log message dictionary 134 | """ 135 | level = msg["level"] 136 | if level == "info" and not self.quiet: 137 | self.logger.warning(msg["msg"]) 138 | if level == "warning": 139 | self.logger.warning(msg["msg"]) 140 | elif level == "error": 141 | self.logger.error(msg["msg"]) 142 | elif level == "debug": 143 | self.logger.debug(msg["msg"]) 144 | elif level == "progress" and not self.quiet: 145 | done = msg["done"] 146 | total = msg["total"] 147 | p = done / total 148 | percent_fmt = ("{:.2%}" if p < 0.01 else "{:.0%}").format(p) 149 | self.logger.info( 150 | "{} of {} steps ({}) done".format(done, total, percent_fmt) 151 | ) 152 | elif level == "shellcmd": 153 | if self.printshellcmds: 154 | self.logger.warning(msg["msg"]) 155 | 156 | 157 | logger = Logger() 158 | 159 | 160 | def setup_logger( 161 | quiet=False, 162 | printshellcmds=False, 163 | nocolor=False, 164 | stdout=False, 165 | debug=False, 166 | use_threads=False, 167 | wms_monitor=None, 168 | ): 169 | # console output only if no custom logger was specified 170 | stream_handler = ColorizingStreamHandler( 171 | nocolor=nocolor, 172 | stream=sys.stdout if stdout else sys.stderr, 173 | use_threads=use_threads, 174 | ) 175 | logger.set_stream_handler(stream_handler) 176 | logger.set_level(_logging.DEBUG if debug else _logging.INFO) 177 | logger.quiet = quiet 178 | logger.printshellcmds = printshellcmds 179 | -------------------------------------------------------------------------------- /snakeface/settings.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | import os 6 | import tempfile 7 | import yaml 8 | import sys 9 | 10 | from django.core.management.utils import get_random_secret_key 11 | from snakeface.apps.users.utils import get_username 12 | from datetime import datetime 13 | from importlib import import_module 14 | 15 | # Build paths inside the project with the base directory 16 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 17 | 18 | # The snakeface global conflict contains all settings. 19 | SETTINGS_FILE = os.path.join(BASE_DIR, "settings.yml") 20 | if not os.path.exists(SETTINGS_FILE): 21 | sys.exit("Global settings file settings.yml is missing in the install directory.") 22 | 23 | 24 | # Read in the settings file to get settings 25 | class Settings: 26 | """convert a dictionary of settings (from yaml) into a class""" 27 | 28 | def __init__(self, dictionary): 29 | for key, value in dictionary.items(): 30 | setattr(self, key, value) 31 | setattr(self, "UPDATED_AT", datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")) 32 | 33 | def __str__(self): 34 | return "[snakeface-settings]" 35 | 36 | def __repr__(self): 37 | return self.__str__() 38 | 39 | def __iter__(self): 40 | for key, value in self.__dict__.items(): 41 | yield key, value 42 | 43 | 44 | with open(SETTINGS_FILE, "r") as fd: 45 | cfg = Settings(yaml.load(fd.read(), Loader=yaml.FullLoader)) 46 | 47 | # For each setting, if it's defined in the environment with SNAKEFACE_ prefix, override 48 | for key, value in cfg: 49 | envar = os.getenv("SNAKEFACE_%s" % key) 50 | if envar: 51 | setattr(cfg, key, envar) 52 | 53 | # Secret Key 54 | 55 | 56 | def generate_secret_key(filename): 57 | """A helper function to write a randomly generated secret key to file""" 58 | key = get_random_secret_key() 59 | with open(filename, "w") as fd: 60 | fd.writelines("SECRET_KEY = '%s'" % key) 61 | 62 | 63 | # Generate secret key if doesn't exist, and not defined in environment 64 | SECRET_KEY = os.environ.get("SECRET_KEY") 65 | if not SECRET_KEY: 66 | try: 67 | from .secret_key import SECRET_KEY 68 | except ImportError: 69 | SETTINGS_DIR = os.path.abspath(os.path.dirname(__file__)) 70 | generate_secret_key(os.path.join(SETTINGS_DIR, "secret_key.py")) 71 | from .secret_key import SECRET_KEY 72 | 73 | 74 | # Private only should be a boolean 75 | cfg.PRIVATE_ONLY = cfg.PRIVATE_ONLY is not None 76 | 77 | # Set the domain name 78 | DOMAIN_NAME = cfg.DOMAIN_NAME 79 | if cfg.DOMAIN_PORT: 80 | DOMAIN_NAME = "%s:%s" % (DOMAIN_NAME, cfg.DOMAIN_PORT) 81 | 82 | # SECURITY WARNING: don't run with debug turned on in production! 83 | DEBUG = True if os.getenv("DEBUG") != "false" else False 84 | 85 | # Derive list of plugins enabled from the environment 86 | PLUGINS_LOOKUP = { 87 | "ldap_auth": False, 88 | "pam_auth": False, 89 | "saml_auth": False, 90 | } 91 | PLUGINS_ENABLED = [] 92 | using_auth_backend = False 93 | for key, enabled in PLUGINS_LOOKUP.items(): 94 | plugin_key = "PLUGIN_%s_ENABLED" % key.upper() 95 | if hasattr(cfg, plugin_key) and getattr(cfg, plugin_key) is not None: 96 | 97 | # Don't enable auth backends if we are using a notebook 98 | if cfg.NOTEBOOK_ONLY or cfg.NOTEBOOK and "AUTH" in plugin_key: 99 | continue 100 | 101 | if "AUTH" in plugin_key: 102 | using_auth_backend = True 103 | PLUGINS_ENABLED.append(key) 104 | 105 | # Does the user want a notebook? Default to this if no auth setup 106 | if not hasattr(cfg, "NOTEBOOK"): 107 | cfg.NOTEBOOK = True if not using_auth_backend else None 108 | 109 | # If the working directory isn't defined, set to pwd 110 | if not hasattr(cfg, "WORKDIR") or not cfg.WORKDIR: 111 | cfg.WORKDIR = os.getcwd() 112 | 113 | # SECURITY WARNING: App Engine's security features ensure that it is safe to 114 | # have ALLOWED_HOSTS = ['*'] when the app is deployed. If you deploy a Django 115 | # app not on App Engine, make sure to set an appropriate host here. 116 | # See https://docs.djangoproject.com/en/2.1/ref/settings/ 117 | ALLOWED_HOSTS = ["*"] 118 | 119 | # Application definition 120 | 121 | INSTALLED_APPS = [ 122 | "channels", 123 | "snakeface.apps.base", 124 | "snakeface.apps.api", 125 | "snakeface.apps.main", 126 | "snakeface.apps.users", 127 | "django.contrib.admin", 128 | "django.contrib.auth", 129 | "django.contrib.humanize", 130 | "django.contrib.contenttypes", 131 | "django.contrib.sessions", 132 | "django.contrib.messages", 133 | "django.contrib.staticfiles", 134 | "django_extensions", 135 | "crispy_forms", 136 | "social_django", 137 | "django_q", 138 | "rest_framework", 139 | "rest_framework.authtoken", 140 | ] 141 | 142 | 143 | CRISPY_TEMPLATE_PACK = "bootstrap4" 144 | 145 | MIDDLEWARE = [ 146 | "django.middleware.security.SecurityMiddleware", 147 | "django.contrib.sessions.middleware.SessionMiddleware", 148 | "django.middleware.common.CommonMiddleware", 149 | "django.middleware.csrf.CsrfViewMiddleware", 150 | "django.contrib.auth.middleware.AuthenticationMiddleware", 151 | "django.contrib.messages.middleware.MessageMiddleware", 152 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 153 | ] 154 | 155 | # Do we want to enable the cache? 156 | 157 | if cfg.ENABLE_CACHE: 158 | MIDDLEWARE += [ 159 | "django.middleware.cache.UpdateCacheMiddleware", 160 | "django.middleware.common.CommonMiddleware", 161 | "django.middleware.cache.FetchFromCacheMiddleware", 162 | ] 163 | 164 | CACHE_MIDDLEWARE_ALIAS = "default" 165 | CACHE_MIDDLEWARE_SECONDS = 86400 # one day 166 | 167 | 168 | # If we are using a notebook, use an in memory channel layer 169 | if cfg.NOTEBOOK: 170 | CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} 171 | 172 | 173 | ROOT_URLCONF = "snakeface.urls" 174 | 175 | TEMPLATES = [ 176 | { 177 | "BACKEND": "django.template.backends.django.DjangoTemplates", 178 | "DIRS": [], 179 | "APP_DIRS": True, 180 | "OPTIONS": { 181 | "context_processors": [ 182 | "django.template.context_processors.debug", 183 | "django.template.context_processors.request", 184 | "django.contrib.auth.context_processors.auth", 185 | "django.contrib.messages.context_processors.messages", 186 | "snakeface.context_processors.globals", 187 | ], 188 | }, 189 | }, 190 | ] 191 | 192 | TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG 193 | WSGI_APPLICATION = "snakeface.wsgi.application" 194 | ASGI_APPLICATION = "snakeface.asgi.application" 195 | 196 | AUTH_USER_MODEL = "users.User" 197 | SOCIAL_AUTH_USER_MODEL = "users.User" 198 | GRAVATAR_DEFAULT_IMAGE = "retro" 199 | 200 | 201 | # Cache to tmp 202 | CACHE_LOCATION = os.path.join(tempfile.gettempdir(), "snakeface-cache") 203 | CACHES = { 204 | "default": { 205 | "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", 206 | "LOCATION": CACHE_LOCATION, 207 | } 208 | } 209 | 210 | if not os.path.exists(CACHE_LOCATION): 211 | os.mkdir(CACHE_LOCATION) 212 | 213 | 214 | # Database 215 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 216 | 217 | # Case 1: we are running locally but want to do migration, etc. (set False to True) 218 | if True and os.getenv("APP_ENGINE_HOST") != None: 219 | print("Warning: connecting to production database.") 220 | 221 | # Running in development, but want to access the Google Cloud SQL instance in production. 222 | DATABASES = { 223 | "default": { 224 | "ENGINE": "django.db.backends.postgresql", 225 | "USER": os.getenv("APP_ENGINE_USERNAME"), 226 | "PASSWORD": os.getenv("APP_ENGINE_PASSWORD"), 227 | "NAME": os.getenv("APP_ENGINE_DATABASE"), 228 | "HOST": os.getenv("APP_ENGINE_HOST"), # Set to IP address 229 | "PORT": "", # empty string for default. 230 | } 231 | } 232 | 233 | # Case 2: we are running on app engine 234 | elif os.getenv("APP_ENGINE_CONNECTION_NAME") != None: 235 | 236 | # Ensure debug is absolutely off 237 | TEMPLATES[0]["OPTIONS"]["debug"] = False 238 | DEBUG = False 239 | 240 | # Running on production App Engine, so connect to Google Cloud SQL using 241 | # the unix socket at /cloudsql/ 242 | DATABASES = { 243 | "default": { 244 | "ENGINE": "django.db.backends.postgresql", 245 | "HOST": "/cloudsql/%s" % os.getenv("APP_ENGINE_CONNECTION_NAME"), 246 | "USER": os.getenv("APP_ENGINE_USERNAME"), 247 | "PASSWORD": os.getenv("APP_ENGINE_PASSWORD"), 248 | "NAME": os.getenv("APP_ENGINE_DATABASE"), 249 | } 250 | } 251 | 252 | # Case 3: Database local development uses DATABASE_* variables 253 | elif os.getenv("DATABASE_HOST") is not None: 254 | # Make sure to export all of these in your .env file 255 | DATABASES = { 256 | "default": { 257 | "ENGINE": os.environ.get("DATABASE_ENGINE", "django.db.backends.mysql"), 258 | "HOST": os.environ.get("DATABASE_HOST"), 259 | "USER": os.environ.get("DATABASE_USER"), 260 | "PASSWORD": os.environ.get("DATABASE_PASSWORD"), 261 | "NAME": os.environ.get("DATABASE_NAME"), 262 | } 263 | } 264 | else: 265 | # Use sqlite when testing locally 266 | DATABASES = { 267 | "default": { 268 | "ENGINE": "django.db.backends.sqlite3", 269 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 270 | } 271 | } 272 | 273 | 274 | # Password validation 275 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 276 | 277 | AUTH_PASSWORD_VALIDATORS = [ 278 | { 279 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: 501 280 | }, 281 | { 282 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa: 501 283 | }, 284 | { 285 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa: 501 286 | }, 287 | { 288 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa: 501 289 | }, 290 | ] 291 | 292 | # Django Q 293 | # workers defaults to multiprocessing CPU count, can be set if neede 294 | # This can be sped up running with another database 295 | 296 | Q_CLUSTER = { 297 | "name": "snakecluster", 298 | "timeout": 90, 299 | "retry": 120, 300 | "queue_limit": 50, 301 | "bulk": 10, 302 | "orm": "default", 303 | } 304 | 305 | 306 | # Internationalization 307 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 308 | 309 | LANGUAGE_CODE = "en-us" 310 | 311 | TIME_ZONE = "UTC" 312 | 313 | USE_I18N = True 314 | 315 | USE_L10N = True 316 | 317 | USE_TZ = True 318 | 319 | 320 | # Static files (CSS, JavaScript, Images) 321 | # TODO: we probably want to put these in one spot relative to user home 322 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 323 | 324 | STATIC_ROOT = "static" 325 | STATIC_URL = "/static/" 326 | MEDIA_ROOT = "data" 327 | MEDIA_URL = "/data/" 328 | 329 | # Rate Limiting 330 | 331 | VIEW_RATE_LIMIT = "1000/1d" # The rate limit for each view, django-ratelimit, "50 per day per ipaddress) 332 | VIEW_RATE_LIMIT_BLOCK = ( 333 | True # Given that someone goes over, are they blocked for the period? 334 | ) 335 | 336 | # On any admin or plugin login redirect to standard social-auth entry point for agreement to terms 337 | LOGIN_REDIRECT_URL = "/login" 338 | LOGIN_URL = "/login" 339 | 340 | # If we are using a notebook, grab the user that started 341 | cfg.USERNAME = None 342 | if cfg.NOTEBOOK or cfg.NOTEBOOK_ONLY: 343 | cfg.USERNAME = get_username() 344 | 345 | AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"] 346 | 347 | ## PLUGINS ##################################################################### 348 | 349 | # Apply any plugin settings 350 | for plugin in PLUGINS_ENABLED: 351 | 352 | plugin_module = "snakeface.plugins." + plugin 353 | plugin = import_module(plugin_module) 354 | 355 | # Add the plugin to INSTALLED APPS 356 | INSTALLED_APPS.append(plugin_module) 357 | 358 | # Add AUTHENTICATION_BACKENDS if defined, for authentication plugins 359 | if hasattr(plugin, "AUTHENTICATION_BACKENDS"): 360 | AUTHENTICATION_BACKENDS = ( 361 | AUTHENTICATION_BACKENDS + plugin.AUTHENTICATION_BACKENDS 362 | ) 363 | 364 | # Add custom context processors, if defines for plugin 365 | if hasattr(plugin, "CONTEXT_PROCESSORS"): 366 | for context_processor in plugin.CONTEXT_PROCESSORS: 367 | TEMPLATES[0]["OPTIONS"]["context_processors"].append(context_processor) 368 | -------------------------------------------------------------------------------- /snakeface/settings.yml: -------------------------------------------------------------------------------- 1 | # The snakeface settings.yml file is kept alongside the install location, 2 | # meaning that it should only be editable by an owner or system administrator 3 | 4 | # How to fill in fields? ------------------------------------------------------- 5 | # For fields that you want to leave undefined, leave them as null 6 | # All fields get read in to a global settings.cfg.SETTING_NAME object. 7 | 8 | # How to set secrets? ---------------------------------------------------------- 9 | # For each setting below, a value found in the environment prefixed with 10 | # SNAKEFACE_ will override it. This means that you should generally leave 11 | # secrets unset (e.g., SENDGRID keys). and insteaed explort them in the 12 | # environment (e.g., SNAKEFACE_SENDGRID_API_KEY). 13 | 14 | # Social Networks 15 | GOOGLE_ANALYTICS_SITE: null 16 | GOOGLE_ANALYTICS_ID: null 17 | TWITTER_USERNAME: johanneskoester 18 | 19 | # Repository and Documentation 20 | GITHUB_REPOSITORY: https://github.com/snakemake/snakeface 21 | GITHUB_DOCUMENTATION: https://snakemake.github.io/snakeface 22 | 23 | # Workflow running limits 24 | USER_WORKFLOW_LIMIT: 50 25 | USER_WORKFLOW_RUNS_LIMIT: 50 26 | USER_WORKFLOW_GLOBAL_RUNS_LIMIT: 1000 27 | 28 | # Only allow notebooks, set to non null (disables all other auth) 29 | NOTEBOOK_ONLY: null 30 | 31 | # Maximum number of jobs to allow running at once 32 | MAXIMUM_NOTEBOOK_JOBS: 2 33 | 34 | # How often to refresh statuses on a workflow details page 35 | WORKFLOW_UPDATE_SECONDS: 10 36 | 37 | # Executors (set to non null to enable, use profile if needed), local set by default 38 | EXECUTOR_CLUSTER: null 39 | EXECUTOR_GOOGLE_LIFE_SCIENCES: null 40 | EXECUTOR_KUBERNETES: null 41 | EXECUTOR_TIBANNA: null 42 | EXECUTOR_GA4GH_TES: null 43 | 44 | # Disable other argument groups by setting these to non null 45 | DISABLE_SINGULARITY: null 46 | DISABLE_CONDA: null 47 | DISABLE_NOTEBOOKS: true 48 | 49 | # Global server settings 50 | ENVIRONMENT: test 51 | HELP_CONTACT_URL: https://github.com/snakemake/snakeface/issues 52 | SENDGRID_API_KEY: null 53 | SENDGRID_SENDER_EMAIL: null 54 | DOMAIN_NAME: http://127.0.0.1 55 | DOMAIN_PORT: 5000 56 | REQUIRE_AUTH: true 57 | 58 | # Set a default profile (see https://github.com/snakemake-profiles) 59 | PROFILE: null 60 | 61 | # Workflow collection settings 62 | PRIVATE_ONLY: null 63 | 64 | # Caching 65 | ENABLE_CACHE: false 66 | 67 | # Default working directory (overridden by client and environment) 68 | WORKDIR: null 69 | 70 | # Plugins (set to non null value to enable) 71 | PLUGIN_LDAP_AUTH_ENABLED: null 72 | PLUGIN_PAM_AUTH_ENABLED: null 73 | PLUGIN_SAML_AUTH_ENABLED: null 74 | -------------------------------------------------------------------------------- /snakeface/urls.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | from django.conf.urls import include, url 6 | from django.contrib import admin 7 | from django.views.generic.base import TemplateView 8 | 9 | from snakeface.apps.base import urls as base_urls 10 | from snakeface.apps.main import urls as main_urls 11 | from snakeface.apps.users import urls as user_urls 12 | from snakeface.apps.api import urls as api_urls 13 | 14 | admin.site.site_header = "Snakeface Admin" 15 | admin.site.site_title = "Snakeface Admin" 16 | admin.site.index_title = "Snakeface Admin" 17 | 18 | admin.autodiscover() 19 | 20 | urlpatterns = [ 21 | url(r"^admin/", admin.site.urls), 22 | url(r"^", include(base_urls, namespace="base")), 23 | url(r"^", include(api_urls, namespace="api")), 24 | url(r"^", include(main_urls, namespace="main")), 25 | url(r"^", include(user_urls, namespace="users")), 26 | url( 27 | r"^robots\.txt?/$", 28 | TemplateView.as_view( 29 | template_name="base/robots.txt", content_type="text/plain" 30 | ), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /snakeface/version.py: -------------------------------------------------------------------------------- 1 | __author__ = "Vanessa Sochat" 2 | __copyright__ = "Copyright 2020-2021, Vanessa Sochat" 3 | __license__ = "MPL 2.0" 4 | 5 | __version__ = "0.0.18" 6 | AUTHOR = "Vanessa Sochat" 7 | AUTHOR_EMAIL = "vsochat@stanford.edu" 8 | NAME = "snakeface" 9 | PACKAGE_URL = "https://github.com/snakemake/snakeface" 10 | KEYWORDS = "snakemake,workflow management,pipeline,interface, workflows" 11 | DESCRIPTION = "Snakemake Interface" 12 | LICENSE = "LICENSE" 13 | 14 | ################################################################################ 15 | # Global requirements 16 | 17 | 18 | INSTALL_REQUIRES = ( 19 | ("snakedeploy", {"min_version": None}), 20 | ("snakemake", {"min_version": None}), 21 | ("pyaml", {"min_version": "20.4.0"}), 22 | ("Jinja2", {"min_version": "2.11.2"}), 23 | ("Django", {"exact_version": "3.0.8"}), 24 | ("django-q", {"min_version": "1.3.4"}), 25 | ("django-crispy-forms", {"min_version": "1.10.0"}), 26 | ("django-taggit", {"min_version": "1.3.0"}), 27 | ("django-gravatar", {"min_version": None}), 28 | ("django-ratelimit", {"min_version": "3.0.0"}), 29 | ("django-extensions", {"min_version": "3.0.2"}), 30 | ("djangorestframework", {"exact_version": "3.11.1"}), 31 | ("drf-yasg", {"exact_version": "1.20.0"}), 32 | ("channels", {"exact_version": "3.0.3"}), 33 | ("social-auth-app-django", {"min_version": "4.0.0"}), 34 | ("social-auth-core", {"min_version": "3.3.3"}), 35 | ("psycopg2-binary", {"min_version": "2.8.5"}), 36 | ) 37 | 38 | # Dependencies provided by snakemake: pyYaml, jinja2 39 | 40 | EMAIL_REQUIRES = (("sendgrid", {"min_version": "6.4.3"}),) 41 | 42 | TESTS_REQUIRES = (("pytest", {"min_version": "4.6.2"}),) 43 | 44 | ALL_REQUIRES = INSTALL_REQUIRES + EMAIL_REQUIRES 45 | -------------------------------------------------------------------------------- /snakeface/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "snakeface.settings") 4 | 5 | from django.core.wsgi import get_wsgi_application 6 | 7 | application = get_wsgi_application() 8 | --------------------------------------------------------------------------------