├── .gitignore ├── .gitmodules ├── LICENSE.txt ├── Makefile ├── README.md ├── backend_cell.py ├── config_default.py ├── contrib ├── dokuwiki │ ├── README │ ├── action.php │ ├── conf │ │ ├── default.php │ │ └── metadata.php │ ├── plugin.info.txt │ └── syntax.php ├── drupalmodule │ ├── sagecell.info │ ├── sagecell.install │ ├── sagecell.module │ └── sagecell_makesagecell.inc ├── moinmoin │ └── sagecell.py ├── sagecell-client │ ├── sagecell-client.py │ └── sagecell-service.py ├── sphinx │ └── sagecellext.py ├── sphinx2 │ ├── README.rst │ ├── icsecontrib │ │ ├── __init__.py │ │ └── sagecellserver.py │ ├── layout.html │ └── setup.py ├── stats │ └── sagecell-stats.ipynb └── vm │ ├── README.md │ ├── build_host.sh │ ├── compute_node │ ├── config.py │ ├── etc │ │ ├── apt │ │ │ └── apt.conf.d │ │ │ │ └── 20auto-upgrades │ │ ├── cron.d │ │ │ ├── sagecell-cleantmp │ │ │ └── sagecell-monitor │ │ ├── logrotate.d │ │ │ └── 0-sagecell │ │ ├── nginx │ │ │ └── conf.d │ │ │ │ └── sagecell.conf │ │ ├── rsyslog.d │ │ │ └── sagecell.conf │ │ ├── security │ │ │ └── limits.d │ │ │ │ └── sagecell.conf │ │ └── systemd │ │ │ └── system │ │ │ └── sagecell.service │ └── root │ │ ├── check_sagecell │ │ └── firewall │ ├── container_manager.py │ ├── permalink_database │ └── etc │ │ └── systemd │ │ └── system │ │ └── permalink_database.service │ └── preseed.host ├── db.py ├── db_sqlalchemy.py ├── db_web.py ├── doc ├── README.md ├── embedding.rst ├── future.rst ├── interact_protocol.rst ├── js.rst ├── messages.md ├── sep_interacts.md ├── session.md ├── timing.rst └── todo.rst ├── dynamic.py ├── exercise.py ├── fetch_vendor_js.mjs ├── handlers.py ├── interact_compatibility.py ├── interact_sagecell.py ├── js ├── cell.js ├── cell_body.html ├── console.js ├── css.js ├── editor.js ├── interact_cell.js ├── interact_controls.js ├── interact_data.js ├── jquery-global.js ├── main.js ├── multisockjs.js ├── sagecell.js ├── session.js ├── urls.js ├── utils.js └── widgets.js ├── kernel_dealer.py ├── kernel_init.py ├── kernel_provider.py ├── log.py ├── misc.py ├── namespace.py ├── package-lock.json ├── package.json ├── permalink.py ├── permalink_server.py ├── static ├── about.html ├── cocalc-logo-horizontal.png ├── colorpicker │ ├── css │ │ ├── colorpicker.css │ │ └── layout.css │ ├── images │ │ ├── Thumbs.db │ │ ├── blank.gif │ │ ├── colorpicker_background.png │ │ ├── colorpicker_hex.png │ │ ├── colorpicker_hsb_b.png │ │ ├── colorpicker_hsb_h.png │ │ ├── colorpicker_hsb_s.png │ │ ├── colorpicker_indic.gif │ │ ├── colorpicker_overlay.png │ │ ├── colorpicker_rgb_b.png │ │ ├── colorpicker_rgb_g.png │ │ ├── colorpicker_rgb_r.png │ │ ├── colorpicker_select.gif │ │ ├── colorpicker_submit.png │ │ ├── custom_background.png │ │ ├── custom_hex.png │ │ ├── custom_hsb_b.png │ │ ├── custom_hsb_h.png │ │ ├── custom_hsb_s.png │ │ ├── custom_indic.gif │ │ ├── custom_rgb_b.png │ │ ├── custom_rgb_g.png │ │ ├── custom_rgb_r.png │ │ ├── custom_submit.png │ │ ├── select.png │ │ ├── select2.png │ │ └── slider.png │ ├── index.html │ └── js │ │ ├── colorpicker.js │ │ ├── colorpicker.min.js │ │ ├── eye.js │ │ ├── jquery.js │ │ ├── layout.js │ │ └── utils.js ├── favicon.ico ├── fontawesome-webfont.afm ├── fontawesome-webfont.eot ├── fontawesome-webfont.ttf ├── fontawesome-webfont.woff ├── fontawesome.css ├── jquery-1.5.min.js ├── logo_sagemath_cell.png ├── logo_sagemath_cell.svg ├── main.css ├── robots.txt ├── root.css ├── sagecell.css ├── sagemathcell-logo.png ├── sagemathcell-logo.svg ├── spinner.gif └── test │ ├── interact.html │ └── linked.html ├── templates ├── help.html ├── info.html ├── provider.html ├── root.html └── tos_default.html ├── tests ├── forking_kernel_manager_tests.py ├── multimechanize │ ├── config.cfg │ └── test_scripts │ │ ├── client.py │ │ ├── interact_session.py │ │ └── simple_session.py ├── trusted_kernel_manager_tests.py └── untrusted_kernel_manager_tests.py ├── timing ├── __init__.py ├── config.cfg └── test_scripts │ ├── MultipartPostHandler.py │ ├── __init__.py │ ├── sagecell.py │ ├── simple_computation.py │ ├── simple_upload_modify_download.py │ └── timing_util.py ├── web_server.py └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | 4 | /build*/ 5 | /doc/_build/* 6 | /static/SageMenu.mnu 7 | /static/all.min.css 8 | /static/embedded_sagecell.js 9 | /static/embedded_sagecell.js.map 10 | /static/embedded_sagecell.js.LICENSE.txt 11 | /static/images/ 12 | /static/jquery.min.js 13 | /static/jquery-ui.min.css 14 | /static/jsmol 15 | /static/sagecell_embed.css 16 | /static/tos.html 17 | /templates/tos.html 18 | /tests/multimechanize/results/ 19 | /config.py 20 | /sqlite.db 21 | node_modules 22 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/.gitmodules -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Most of the files in this repository are individually licensed with 2 | the modified BSD license: 3 | 4 | Copyright (c) 2011, Jason Grout, Ira Hanson, Alex Kramer, William Stein 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are 9 | met: 10 | 11 | a. Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | 14 | b. Redistributions in binary form must reproduce the above copyright 15 | notice, this list of conditions and the following disclaimer in 16 | the documentation and/or other materials provided with the 17 | distribution. 18 | 19 | c. Neither the name of the Sage Cell project nor the names of its 20 | contributors may be used to endorse or promote products derived 21 | from this software without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 29 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | 36 | Some files (like interact_compatibility.py and interact_sagecell.py) 37 | are licensed GPLv2+ for the sole reason that they import Sage GPLv2+ 38 | code (see the header for those files). If those imports are removed, 39 | the files may be licensed with the modified BSD license. 40 | 41 | Since this package includes GPLv2+ code (namely those files above), 42 | the repository as a whole is licensed GPLv2+. 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | sage-root := $(shell [ -n "$$SAGE_ROOT" ] && echo "$$SAGE_ROOT" || sage --root || echo "\$$SAGE_ROOT") 2 | all-min-js = static/embedded_sagecell.js 3 | all-min-js-map = static/embedded_sagecell.js.map 4 | all-min-js-license = static/embedded_sagecell.js.LICENSE.txt 5 | 6 | sagecell-css = static/sagecell.css 7 | embed-css = static/sagecell_embed.css 8 | 9 | tos-default = templates/tos_default.html 10 | tos = templates/tos.html 11 | tos-static = static/tos.html 12 | 13 | all: $(all-min-js) $(embed-css) $(tos-static) 14 | 15 | .PHONY: $(tos-static) 16 | 17 | build: 18 | npm install 19 | -rm -r build 20 | npm run build:deps 21 | ln -sfn $(SAGE_VENV)/share/jupyter/nbextensions/jupyter-jsmol/jsmol static/jsmol 22 | ln -sfn $(sage-root)/local/share/threejs-sage/r122 static/threejs 23 | ln -sf $(sage-root)/local/share/jmol/appletweb/SageMenu.mnu static/SageMenu.mnu 24 | cp static/jsmol/JSmol.min.nojq.js build/vendor/JSmol.js 25 | 26 | $(all-min-js): build js/* 27 | npm run build 28 | cp build/embedded_sagecell.js $(all-min-js) 29 | cp build/embedded_sagecell.js.map $(all-min-js-map) 30 | cp build/embedded_sagecell.js.LICENSE.txt $(all-min-js-license) 31 | # Host standalone jquery for compatibility with old instructions 32 | cp build/vendor/jquery*.min.js static/jquery.min.js 33 | 34 | $(embed-css): $(sagecell-css) 35 | sed -e 's/;/ !important;/g' < $(sagecell-css) > $(embed-css) 36 | 37 | $(tos-static): $(tos-default) 38 | @[ -e $(tos) ] && cp $(tos) $(tos-static) || cp $(tos-default) $(tos-static) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is SageMathCell - a Sage computation web service. 2 | 3 | Our mailing list is https://groups.google.com/forum/#!forum/sage-cell 4 | 5 | # Security Warning 6 | 7 | If you are going to run a world accessible SageMathCell server, you must understand security implications and should be able to implement reasonable precautions. 8 | 9 | The worker account (which is your own one by default) will be able to execute arbitrary code, which may be malicious. Make **sure** that you are securing the account properly. Working with a professional IT person is a very good idea here. Since the untrusted accounts can be on any computer, one way to isolate these accounts is to host them in a virtual machine that can be reset if the machine is compromised. 10 | 11 | 12 | # Simple Installation 13 | 14 | We assume that you have access to the Internet and can install any needed dependencies. If you need to know more precisely what tools are needed, please consult the scripts for building virtual machine images in [contrib/vm](contrib/vm). 15 | In particular, system packages installed in the base container are listed [here](https://github.com/sagemath/sagecell/blob/master/contrib/vm/container_manager.py#L58). 16 | 17 | 18 | 1. Install requirejs: 19 | 20 | ```bash 21 | sudo apt-get install npm 22 | # On Debian based systems we need to make an alias 23 | sudo ln -s /usr/bin/nodejs /usr/bin/node 24 | ``` 25 | 26 | 2. Get and build Sage (`export MAKE="make -j8"` or something similar can speed things up): 27 | 28 | ```bash 29 | git clone https://github.com/sagemath/sage.git 30 | pushd sage 31 | ./bootstrap 32 | ./configure --enable-download-from-upstream-url 33 | # read messages at the end, follow instructions given there. 34 | # possibly install more system packages (using apt-get, if on Debian/Ubuntu) 35 | make 36 | popd 37 | ``` 38 | 39 | 3. Prepare Sage for SageMathCell: 40 | 41 | ```bash 42 | sage/sage -pip install lockfile 43 | sage/sage -pip install paramiko 44 | sage/sage -pip install sockjs-tornado 45 | sage/sage -pip install sqlalchemy 46 | ``` 47 | 48 | 4. Build SageMathCell: 49 | 50 | ```bash 51 | git clone https://github.com/sagemath/sagecell.git 52 | pushd sagecell 53 | ../sage/sage -sh -c make 54 | ``` 55 | 56 | To build just the Javascript components, from the `sagecell` directory run 57 | 58 | ```bash 59 | make static/embedded_sagecell.js 60 | ``` 61 | 62 | 63 | # Configuration 64 | 65 | 1. Go into the `sagecell` directory (you are there in the end of the above instructions). 66 | 2. Copy `config_default.py` to `config.py`. (Or fill `config.py` only with entries that you wish to change from default values.) 67 | 3. Edit `config.py` according to your needs. Of particular interest are `host` and `username` entries of the `provider_info` dictionary: you should be able to SSH to `username@host` *without typing in a password*. For example, by default, it assumes you can do `ssh localhost` without typing in a password. Unless you are running a private and **firewalled** server for youself, you’ll want to change this to a more restrictive account; otherwise **anyone will be able to execute any code under your username**. You can set up a passwordless account using SSH: type “ssh passwordless login” into Google to find lots of guides for doing this, like http://www.debian-administration.org/articles/152. You may also wish to adjust `db_config["uri"]` (make the database files readable *only* by the trusted account). 68 | 4. You may want to adjust `log.py` to suit your needs and/or adjust system configuration. By default logging is done via syslog which handles multiple processes better than plain files. 69 | 5. Start the server via 70 | 71 | ```bash 72 | ../sage/sage web_server.py [-p ] 73 | ``` 74 | 75 | where the default `` is `8888` and go to `http://localhost:` to use the Sage Cell server. 76 | 77 | When you want to shut down the server, press `Ctrl-C` in the same terminal. 78 | 79 | 80 | # Javascript Development 81 | 82 | Javascript source files are compiled using [Webpack](https://webpack.js.org/). Sagecell depends on source files copied 83 | from the Jupyter notebook project. To start development navigate to the `sagecell` source directory and run 84 | 85 | ```bash 86 | npm install 87 | npm run build:deps 88 | ``` 89 | 90 | After this, all dependencies will be located in the `build/vendor` directory. You can now run 91 | 92 | ```bash 93 | npm run build 94 | ``` 95 | 96 | to build `build/embedded_sagecell.js` 97 | 98 | or 99 | 100 | ```bash 101 | npm run watch 102 | ``` 103 | 104 | to build `build/embedded_sagecell.js` and watch files for changes. If a file is changed, `embedded_sagecell.js` will be automatically 105 | rebuilt. 106 | 107 | # License 108 | 109 | See the [LICENSE.txt](LICENSE.txt) file for terms and conditions for usage and a 110 | DISCLAIMER OF ALL WARRANTIES. 111 | 112 | # Browser Compatibility 113 | 114 | SageMathCell is designed to be compatible with recent versions of: 115 | 116 | * Chrome 117 | * Firefox 118 | * Internet Explorer 119 | * Opera 120 | * Safari 121 | 122 | If you notice issues with any of these browsers, please let us know. 123 | -------------------------------------------------------------------------------- /backend_cell.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | SageMathCell Backend for the Sage Rich Output System 4 | 5 | This module defines the SageMathCell backends for 6 | :mod:`sage.repl.rich_output`. 7 | """ 8 | 9 | #***************************************************************************** 10 | # Copyright (C) 2015 Andrey Novoseltsev 11 | # 12 | # Distributed under the terms of the GNU General Public License (GPL) 13 | # as published by the Free Software Foundation; either version 2 of 14 | # the License, or (at your option) any later version. 15 | # http://www.gnu.org/licenses/ 16 | #***************************************************************************** 17 | 18 | import os 19 | import stat 20 | import sys 21 | import tempfile 22 | 23 | 24 | from sage.repl.rich_output.backend_ipython import BackendIPython 25 | from sage.repl.rich_output.output_catalog import * 26 | 27 | 28 | from misc import display_file, display_html, display_message 29 | 30 | 31 | class BackendCell(BackendIPython): 32 | """ 33 | Backend for SageMathCell 34 | 35 | EXAMPLES:: 36 | 37 | sage: from sage.repl.rich_output.backend_cell import BackendCell 38 | sage: BackendCell() 39 | SageMathCell 40 | """ 41 | 42 | def _repr_(self): 43 | """ 44 | Return a string representation 45 | 46 | OUTPUT: 47 | 48 | String. 49 | 50 | EXAMPLES:: 51 | 52 | sage: from sage.repl.rich_output.backend_cell import BackendCell 53 | sage: backend = BackendCell() 54 | sage: backend._repr_() 55 | 'SageMathCell' 56 | """ 57 | return 'SageMathCell' 58 | 59 | def display_immediately(self, plain_text, rich_output): 60 | """ 61 | Show output immediately. 62 | 63 | This method is similar to the rich output :meth:`displayhook`, 64 | except that it can be invoked at any time. 65 | 66 | INPUT: 67 | 68 | Same as :meth:`displayhook`. 69 | 70 | EXAMPLES:: 71 | 72 | sage: from sage.repl.rich_output.output_basic import OutputPlainText 73 | sage: plain_text = OutputPlainText.example() 74 | sage: from sage.repl.rich_output.backend_cell import BackendCell 75 | sage: backend = BackendCell() 76 | sage: _ = backend.display_immediately(plain_text, plain_text) 77 | Example plain text output 78 | """ 79 | if isinstance(rich_output, OutputPlainText): 80 | return {u'text/plain': rich_output.text.get_str()}, {} 81 | if isinstance(rich_output, OutputAsciiArt): 82 | return {u'text/plain': rich_output.ascii_art.get_str()}, {} 83 | 84 | if isinstance(rich_output, OutputLatex): 85 | display_html(rich_output.latex.get_str()) 86 | elif isinstance(rich_output, OutputHtml): 87 | display_html(rich_output.html.get_str()) 88 | 89 | elif isinstance(rich_output, OutputImageGif): 90 | display_file(rich_output.gif.filename(), 'text/image-filename') 91 | elif isinstance(rich_output, OutputImageJpg): 92 | display_file(rich_output.jpg.filename(), 'text/image-filename') 93 | elif isinstance(rich_output, OutputImagePdf): 94 | display_file(rich_output.pdf.filename(), 'text/image-filename') 95 | elif isinstance(rich_output, OutputImagePng): 96 | display_file(rich_output.png.filename(), 'text/image-filename') 97 | elif isinstance(rich_output, OutputImageSvg): 98 | display_file(rich_output.svg.filename(), 'text/image-filename') 99 | 100 | elif isinstance(rich_output, OutputSceneJmol): 101 | path = tempfile.mkdtemp(suffix=".jmol", dir=".") 102 | os.chmod(path, stat.S_IRWXU + stat.S_IXGRP + stat.S_IXOTH) 103 | rich_output.scene_zip.save_as(os.path.join(path, 'scene.zip')) 104 | rich_output.preview_png.save_as(os.path.join(path, 'preview.png')) 105 | display_message({'text/plain': 'application/x-jmol file', 106 | 'application/x-jmol': path}) 107 | elif isinstance(rich_output, OutputSceneThreejs): 108 | path = tempfile.mkstemp(suffix='.html', dir='.')[1] 109 | path = os.path.relpath(path) 110 | rich_output.html.save_as(path) 111 | os.chmod(path, stat.S_IRUSR + stat.S_IRGRP + stat.S_IROTH) 112 | display_html(""" 113 | 124 | """.format(path)) 125 | sys._sage_.sent_files[path] = os.path.getmtime(path) 126 | 127 | else: 128 | raise TypeError('rich_output type not supported, got {0}'.format(rich_output)) 129 | return {u'text/plain': None}, {} 130 | 131 | displayhook = display_immediately 132 | 133 | def supported_output(self): 134 | """ 135 | Return the outputs that are supported by SageMathCell backend. 136 | 137 | OUTPUT: 138 | 139 | Iterable of output container classes, that is, subclass of 140 | :class:`~sage.repl.rich_output.output_basic.OutputBase`). 141 | The order is ignored. 142 | 143 | EXAMPLES:: 144 | 145 | sage: from sage.repl.rich_output.backend_cell import BackendCell 146 | sage: backend = BackendCell() 147 | sage: supp = backend.supported_output(); supp # random output 148 | set([, 149 | ..., 150 | ]) 151 | sage: from sage.repl.rich_output.output_basic import OutputLatex 152 | sage: OutputLatex in supp 153 | True 154 | """ 155 | return set([ 156 | OutputPlainText, 157 | OutputAsciiArt, 158 | OutputLatex, 159 | OutputHtml, 160 | 161 | OutputImageGif, 162 | OutputImageJpg, 163 | OutputImagePdf, 164 | OutputImagePng, 165 | OutputImageSvg, 166 | 167 | OutputSceneJmol, 168 | OutputSceneThreejs, 169 | #OutputSceneWavefront, 170 | ]) 171 | 172 | def threejs_offline_scripts(self): 173 | """ 174 | Return script tags for ``viewer=threejs`` with ``online=False``. 175 | 176 | OUTPUT: 177 | 178 | - a string 179 | 180 | EXAMPLES:: 181 | 182 | sage: from sage.repl.rich_output.backend_cell import BackendCell 183 | sage: backend = BackendCell() 184 | sage: backend.threejs_offline_scripts() 185 | '... 189 | """ 190 | -------------------------------------------------------------------------------- /config_default.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | 4 | # Location of the Sage executable 5 | if 'SAGE_ROOT' in os.environ: 6 | # Assume that the worker should run the same Sage 7 | # that is used to run the web server 8 | sage = os.path.join(os.environ["SAGE_ROOT"], "sage") 9 | else: 10 | # Assume both the web server and the worker have Sage in their paths 11 | sage = "sage" 12 | 13 | # Require the user to accept terms of service before evaluation 14 | requires_tos = True 15 | 16 | db = "sqlalchemy" 17 | db_config = {"uri": "sqlite:///sqlite.db"} 18 | 19 | # db = "web" 20 | # db_config = {"uri": "http://localhost:8889"} 21 | 22 | permalink_server = { 23 | 'db': 'sqlalchemy', 24 | 'db_config': {'uri': 'sqlite:///sqlite.db'} 25 | } 26 | 27 | pid_file = 'sagecell.pid' 28 | permalink_pid_file = 'sagecell_permalink_server.pid' 29 | 30 | dir = "/tmp/sagecell" 31 | 32 | # Parameters for heartbeat channels checking whether a given kernel is alive. 33 | # Setting first_beat lower than 1.0 may cause JavaScript errors. 34 | beat_interval = 0.5 35 | first_beat = 1.0 36 | 37 | # Allowed idling between interactions with a kernel 38 | max_timeout = 60 * 15 39 | # Even an actively used kernel will be killed after this time 40 | max_lifespan = 60 * 30 41 | 42 | # Recommended settings for kernel providers 43 | provider_settings = { 44 | "max_kernels": 10, 45 | "max_preforked": 1, 46 | # The keys to resource_limits can be any available resources 47 | # for the resource module. See http://docs.python.org/library/resource.html 48 | # for more information (section 35.13.1) 49 | # RLIMIT_AS is more of a suggestion than a hard limit in Mac OS X 50 | # Also, Sage may allocate huge AS, making this limit pointless: 51 | # https://groups.google.com/d/topic/sage-devel/1MM7UPcrW18/discussion 52 | "preforked_rlimits": { 53 | "RLIMIT_CPU": 30, # CPU time in seconds 54 | }, 55 | } 56 | 57 | # Location information for kernel providers 58 | provider_info = { 59 | "host": "localhost", 60 | "username": None, 61 | "python": sage + " -python", 62 | "location": os.path.dirname(os.path.abspath(__file__)) 63 | } 64 | 65 | providers = [provider_info] 66 | -------------------------------------------------------------------------------- /contrib/dokuwiki/README: -------------------------------------------------------------------------------- 1 | sagecell Plugin for DokuWiki 2 | 3 | Embed a Sage Cell into your page 4 | 5 | All documentation for this plugin can be found at 6 | https://github.com/sagemath/sagecell 7 | 8 | If you install this plugin manually, make sure it is installed in 9 | lib/plugins/sagecell/ - if the folder is called different it 10 | will not work! 11 | 12 | Please refer to http://www.dokuwiki.org/plugins for additional info 13 | on how to install plugins in DokuWiki. 14 | 15 | ---- 16 | Copyright (C) Jason Grout 17 | 18 | This program is free software; you can redistribute it and/or modify 19 | it under the terms of the GNU General Public License as published by 20 | the Free Software Foundation; version 2 of the License 21 | 22 | This program is distributed in the hope that it will be useful, 23 | but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 | GNU General Public License for more details. 26 | 27 | See the COPYING file in your DokuWiki folder for details 28 | -------------------------------------------------------------------------------- /contrib/dokuwiki/action.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | // must be run within Dokuwiki 10 | if (!defined('DOKU_INC')) die(); 11 | 12 | if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); 13 | if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); 14 | if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 15 | 16 | require_once DOKU_PLUGIN.'action.php'; 17 | 18 | class action_plugin_sagecell extends DokuWiki_Action_Plugin { 19 | 20 | public function register(Doku_Event_Handler &$controller) { 21 | 22 | $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'handle_tpl_metaheader_output'); 23 | 24 | } 25 | 26 | public function handle_tpl_metaheader_output(Doku_Event &$event, $param) { 27 | $url = rtrim($this->getConf('url'), '/'); 28 | // Adding js 29 | $event->data["script"][] = array ( 30 | "type" => "text/javascript", 31 | "src" => $url . "/static/jquery.min.js", 32 | "_data" => "", 33 | ); 34 | $event->data["script"][] = array ( 35 | "type" => "text/javascript", 36 | "src" => $url . "/static/embedded_sagecell.js", 37 | "_data" => "", 38 | ); 39 | // Initializing cells 40 | $event->data["script"][] = array ( 41 | "type" => "text/javascript", 42 | "charset" => "utf-8", 43 | "_data" => "sagecell.makeSagecell({inputLocation: '.sage'});", 44 | ); 45 | // Adding stylesheet 46 | $event->data["link"][] = array ( 47 | "type" => "text/css", 48 | "rel" => "stylesheet", 49 | "href" => $url . "/static/sagecell_embed.css", 50 | ); 51 | $event->data['style'][] = array('type' => 'text/css', 52 | '_data' => $this->getConf('style')); 53 | } 54 | 55 | } 56 | 57 | // vim:ts=4:sw=4:et: 58 | -------------------------------------------------------------------------------- /contrib/dokuwiki/conf/default.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | $conf['url'] = 'https://sagecell.sagemath.org'; 9 | $conf['style'] = '.sagecell .CodeMirror pre { 10 | padding: 0 4px !important; 11 | border: 0px !important; 12 | margin: 0 !important;} 13 | .sagecell .CodeMirror { 14 | height: auto; 15 | } 16 | .sagecell .CodeMirror-scroll { 17 | overflow-y: auto; 18 | overflow-x: auto; 19 | max-height: 200px; 20 | } 21 | '; 22 | -------------------------------------------------------------------------------- /contrib/dokuwiki/conf/metadata.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | 9 | $meta['url'] = array('string'); 10 | $meta['style'] = array(''); 11 | 12 | -------------------------------------------------------------------------------- /contrib/dokuwiki/plugin.info.txt: -------------------------------------------------------------------------------- 1 | base sagecell 2 | author Jason Grout 3 | email jason.grout@drake.edu 4 | date 2013-03-30 5 | name sagecell plugin 6 | desc Embed a Sage Cell into your page 7 | url https://github.com/sagemath/sagecell 8 | -------------------------------------------------------------------------------- /contrib/dokuwiki/syntax.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | // must be run within Dokuwiki 10 | if (!defined('DOKU_INC')) die(); 11 | 12 | if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); 13 | if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); 14 | if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 15 | 16 | require_once DOKU_PLUGIN.'syntax.php'; 17 | 18 | class syntax_plugin_sagecell extends DokuWiki_Syntax_Plugin { 19 | public function getType() { 20 | return 'protected'; 21 | } 22 | 23 | public function getPType() { 24 | return 'normal'; 25 | } 26 | 27 | public function getSort() { 28 | return 65; 29 | } 30 | 31 | 32 | public function connectTo($mode) { 33 | $this->Lexer->addSpecialPattern('.*?', $mode, 'plugin_sagecell'); 34 | } 35 | 36 | public function handle($match, $state, $pos, &$handler){ 37 | $data = array("code" => str_replace('', '<\/script>', substr($match,10,-11))); 38 | return $data; 39 | } 40 | 41 | public function render($mode, &$renderer, $data) { 42 | if($mode != 'xhtml') return false; 43 | $renderer->doc .= "
"; 44 | return true; 45 | } 46 | } 47 | 48 | // vim:ts=4:sw=4:et: 49 | -------------------------------------------------------------------------------- /contrib/drupalmodule/sagecell.info: -------------------------------------------------------------------------------- 1 | name = Sage Cell field 2 | description = A module that adds the embeddable Sage Cell content field 3 | package = Fields 4 | core = 7.x 5 | dependencies[] = field 6 | version = "7.x-1.1" 7 | -------------------------------------------------------------------------------- /contrib/drupalmodule/sagecell.install: -------------------------------------------------------------------------------- 1 | 'text', // $schema['columns']['code$ 11 | 'size' => 'medium', 12 | 'not null' => TRUE, 13 | //'length' => 100000, 14 | ); 15 | //$schema['indexes'] = array( 16 | // 'code' => array(array('code',30)), // $schema['indexes']['code'] = ('c$ 17 | // ); 18 | 19 | } 20 | return $schema; 21 | } 22 | -------------------------------------------------------------------------------- /contrib/drupalmodule/sagecell.module: -------------------------------------------------------------------------------- 1 | array( 19 | 'label' => t('Sage Cell'), 20 | 'description' => t('A field to store Sage code to be presented in a Sage Cell instance.'), 21 | 'default_widget' => 'sagecell_code', // see hook_field_widget_info 22 | 'default_formatter' => 'sagecell_default', // see hook_field_formatter_info 23 | ), 24 | ); 25 | } 26 | 27 | /** 28 | * Implementation of hook_field_is_empty 29 | * @'code' 30 | * Our field is empty only if the 'code' item is empty (PHP defines empty to include an empty string) 31 | */ 32 | function sagecell_field_is_empty($item, $field) { 33 | if ($field['type'] == 'sagecell') { 34 | if (empty($item['code'])) { 35 | return TRUE; 36 | } 37 | } 38 | return FALSE; 39 | } 40 | 41 | /** 42 | * Implementation of hook_field_validate 43 | * 44 | * - MAY NOT NEED THIS FUNCTION - 45 | * 46 | * @ inside of '; 183 | $element[$delta] = array('#markup' => $output); 184 | } 185 | break; 186 | 187 | // Plain-text output 188 | case 'sagecell_plain': 189 | foreach ($items as $delta => $item) { 190 | $output = '
' . $item['code'] . '
'; 191 | $element[$delta] = array('#markup' => $output); 192 | } 193 | break; 194 | } 195 | return $element; 196 | } 197 | -------------------------------------------------------------------------------- /contrib/drupalmodule/sagecell_makesagecell.inc: -------------------------------------------------------------------------------- 1 | 'header', 'every_page' => TRUE)); 4 | drupal_add_js('http://aleph.sagemath.org/embedded_sagecell.js', array('scope' => 'header', 'every_page' => TRUE)); 5 | 6 | // Add the inline JS to create the cell, performed after the page is loaded. 7 | // The long callback function changes the sagecell's buttons "type's" to "button" so they don't default to "submit" and submit the page when pressed 8 | drupal_add_js("sagecell.jQuery(function () { sagecell.makeSagecell({inputLocation: '#editcelltextarea', hide: ['files', 'sageMode'], evalButtonText: 'Evaluate', callback: function () {sagecell.jQuery('.sagecell button').each(function (i) {this.setAttribute('type', 'button');});}});});", array('type' => 'inline', 'defer' => TRUE, 'every_page' => TRUE)); 9 | 10 | -------------------------------------------------------------------------------- /contrib/moinmoin/sagecell.py: -------------------------------------------------------------------------------- 1 | """ 2 | MoinMoin - Sage Cell Parser 3 | 4 | @copyright: 2012 Jason Grout 5 | @license: Modified BSD 6 | 7 | Usage:: 8 | 9 | {{{#!sagecell 10 | 1+1 11 | }}} 12 | 13 | Installation 14 | 15 | Put this file in ``data/plugin/parser/``. 16 | 17 | You must also something like these lines in your wikiconfig:: 18 | 19 | html_head = '' 20 | html_head += '' 21 | 22 | 23 | """ 24 | from MoinMoin.parser._ParserBase import ParserBase 25 | from uuid import uuid4 26 | 27 | Dependencies = ['user'] 28 | 29 | template=""" 30 |
31 | 34 | """ 35 | 36 | class Parser(ParserBase): 37 | 38 | parsername = "sagecell" 39 | Dependencies = [] 40 | 41 | def __init__(self, code, request, **kw): 42 | self.code = self.sanitize(code) 43 | self.request = request 44 | 45 | def sanitize(self, code): 46 | """ 47 | Sanitize the code, for example, escape any instances of 48 | """ 49 | sanitized=code.replace("", "<\/script>") 50 | return sanitized 51 | 52 | def format(self, formatter): 53 | self.request.write(formatter.rawHTML(template%{'random': uuid4(), 'code': self.code})) 54 | -------------------------------------------------------------------------------- /contrib/sagecell-client/sagecell-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | A small client illustrating how to interact with the Sage Cell Server, version 2 4 | 5 | Requires the websocket-client package: http://pypi.python.org/pypi/websocket-client 6 | """ 7 | 8 | import websocket 9 | import json 10 | import requests 11 | 12 | 13 | class SageCell(object): 14 | 15 | def __init__(self, url, timeout=10): 16 | if not url.endswith('/'): 17 | url += '/' 18 | # POST or GET /kernel 19 | # if there is a terms of service agreement, you need to 20 | # indicate acceptance in the data parameter below (see the API docs) 21 | response = requests.post( 22 | url + 'kernel', 23 | data={'accepted_tos': 'true'}, 24 | headers={'Accept': 'application/json'}).json() 25 | # RESPONSE: {"id": "ce20fada-f757-45e5-92fa-05e952dd9c87", "ws_url": "ws://localhost:8888/"} 26 | # construct the websocket channel url from that 27 | self.kernel_url = '{ws_url}kernel/{id}/'.format(**response) 28 | print(self.kernel_url) 29 | websocket.setdefaulttimeout(timeout) 30 | self._ws = websocket.create_connection( 31 | self.kernel_url + 'channels', 32 | header={'Jupyter-Kernel-ID': response['id']}) 33 | # initialize our list of messages 34 | self.shell_messages = [] 35 | self.iopub_messages = [] 36 | 37 | def execute_request(self, code): 38 | # zero out our list of messages, in case this is not the first request 39 | self.shell_messages = [] 40 | self.iopub_messages = [] 41 | 42 | # Send the JSON execute_request message string down the shell channel 43 | msg = self._make_execute_request(code) 44 | self._ws.send(msg) 45 | 46 | # Wait until we get both a kernel status idle message and an execute_reply message 47 | got_execute_reply = False 48 | got_idle_status = False 49 | while not (got_execute_reply and got_idle_status): 50 | msg = json.loads(self._ws.recv()) 51 | if msg['channel'] == 'shell': 52 | self.shell_messages.append(msg) 53 | # an execute_reply message signifies the computation is done 54 | if msg['header']['msg_type'] == 'execute_reply': 55 | got_execute_reply = True 56 | elif msg['channel'] == 'iopub': 57 | self.iopub_messages.append(msg) 58 | # the kernel status idle message signifies the kernel is done 59 | if (msg['header']['msg_type'] == 'status' and 60 | msg['content']['execution_state'] == 'idle'): 61 | got_idle_status = True 62 | 63 | return {'shell': self.shell_messages, 'iopub': self.iopub_messages} 64 | 65 | def _make_execute_request(self, code): 66 | from uuid import uuid4 67 | session = str(uuid4()) 68 | 69 | # Here is the general form for an execute_request message 70 | execute_request = { 71 | 'channel': 'shell', 72 | 'header': { 73 | 'msg_type': 'execute_request', 74 | 'msg_id': str(uuid4()), 75 | 'username': '', 'session': session, 76 | }, 77 | 'parent_header':{}, 78 | 'metadata': {}, 79 | 'content': { 80 | 'code': code, 81 | 'silent': False, 82 | 'user_expressions': { 83 | '_sagecell_files': 'sys._sage_.new_files()', 84 | }, 85 | 'allow_stdin': False, 86 | } 87 | } 88 | return json.dumps(execute_request) 89 | 90 | def close(self): 91 | # If we define this, we can use the closing() context manager to automatically close the channels 92 | self._ws.close() 93 | 94 | if __name__ == "__main__": 95 | import sys 96 | if len(sys.argv) >= 2: 97 | # argv[1] is the web address 98 | url = sys.argv[1] 99 | else: 100 | url = 'https://sagecell.sagemath.org' 101 | a = SageCell(url) 102 | import pprint 103 | pprint.pprint(a.execute_request('factorial(2020)')) 104 | -------------------------------------------------------------------------------- /contrib/sagecell-client/sagecell-service.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | from datetime import datetime 4 | import random 5 | import requests 6 | import sys 7 | import time 8 | 9 | 10 | retries = 3 11 | 12 | def message(s): 13 | print('{}: {} attempts left. {}'.format(datetime.now(), retries, s)) 14 | 15 | while retries: 16 | retries -= 1 17 | a, b = random.randint(-2**31, 2**31), random.randint(-2**31, 2**31) 18 | # The handling of temporary files in Sage 9.7 does not allow SageMathCell to 19 | # function properly if there are no regular requests producing temporary 20 | # files. To fight it, we'll generate one during health checks. See 21 | # https://groups.google.com/g/sage-devel/c/jpwUb8OCVVc/m/R4r5bnOkBQAJ 22 | code = 'show(plot(sin)); print({} + {})'.format(a, b) 23 | try: 24 | r = requests.post(sys.argv[1] + '/service', 25 | data={"code": code, "accepted_tos": "true"}, 26 | timeout=5) 27 | reply = r.json() 28 | # Every few hours we have a request that comes back as executed, but the 29 | # stdout is not in the dictionary. It seems that the compute message 30 | # never actually gets sent to the kernel and it appears the problem is 31 | # in the zmq connection between the webserver and the kernel. 32 | # 33 | # Also sometimes reply is unsuccessful, yet the server keeps running 34 | # and other requests are serviced. Since a restart breaks all active 35 | # interacts, better not to restart the server that "mostly works" and 36 | # instead we'll just accumulate statistics on these random errors to 37 | # help resolve them. 38 | if (reply['success'] 39 | and 'stdout' in reply 40 | and int(reply['stdout'].strip()) == a + b): 41 | exit(0) 42 | message(reply) 43 | except Exception as e: 44 | message(e) 45 | time.sleep(0.5) 46 | message('The server is not working!') 47 | exit(1) 48 | -------------------------------------------------------------------------------- /contrib/sphinx/sagecellext.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sphinx extension to insert sagecell in sphinx docs. 3 | 4 | Add the following lines to your layout.html file (e.g., in source/_templates) 5 | 6 | ######### BEGIN layout.html ############### 7 | {% extends "!layout.html" %} 8 | 9 | {%- block extrahead %} 10 | 11 | 12 | 13 | {% endblock %} 14 | 15 | ############ END ######################## 16 | 17 | Add the directory of this file to the path in conf.py 18 | 19 | import sys, os 20 | sys.path.append(os.path.abspath('path/to/file')) 21 | 22 | Add sagecellext to the list of extensions in conf.py. 23 | 24 | extensions = ['sphinx.ext.mathjax', 'sphinx.ext.graphviz', 'sagecellext'] 25 | 26 | 27 | USAGE: 28 | .. sagecell:: 29 | 30 | 1+1 31 | print("hello world") 32 | 33 | 34 | """ 35 | from docutils import nodes, utils 36 | from docutils.nodes import Body, Element 37 | from docutils.parsers.rst import directives 38 | 39 | from sphinx.util.nodes import set_source_info 40 | from sphinx.util.compat import Directive 41 | 42 | class sagecell(Body, Element): 43 | pass 44 | 45 | 46 | class Sagecell(Directive): 47 | 48 | has_content = True 49 | required_arguments = 0 50 | optional_arguments = 0 51 | final_argument_whitespace = False 52 | 53 | 54 | def run(self): 55 | node = sagecell() 56 | node['code'] = u'\n'.join(self.content) 57 | return [node] 58 | 59 | def html_sagecell(self, node): 60 | """ 61 | Convert block to the script here. 62 | """ 63 | from uuid import uuid4 64 | 65 | template= """ 66 |
67 | 72 | """ 73 | self.body.append(template%{'random': uuid4(), 'code': node['code']}) 74 | raise nodes.SkipNode 75 | 76 | 77 | def setup(app): 78 | app.add_node(sagecell, 79 | html=(html_sagecell, None)) 80 | 81 | app.add_directive('sagecell', Sagecell) 82 | -------------------------------------------------------------------------------- /contrib/sphinx2/README.rst: -------------------------------------------------------------------------------- 1 | This extension defines a directive 'sagecellserver' which allows to embedd sage cell inside sphinx doc. To learn more about sage cell server visit: http://aleph.sagemath.org/static/about.html 2 | 3 | 4 | Installation 5 | ========= 6 | 1. Install this extension: 'python setup.py install --user' 7 | 2. Move 'layout.html' to your '_templates' directory. Change sagecell paths if necessary 8 | 3. Add 'icsecontrib.sagecellserver' to your extensions in 'conf.py' 9 | 10 | 11 | How to use it 12 | =========== 13 | 14 | Example of usage:: 15 | 16 | .. sagecellserver:: 17 | 18 | sage: A = matrix([[1,1],[-1,1]]) 19 | sage: D = [vector([0,0]), vector([1,0])] 20 | sage: @interact 21 | sage: def f(A = matrix([[1,1],[-1,1]]), D = '[[0,0],[1,0]]', k=(3..17)): 22 | ... print("Det = {}".format(A.det())) 23 | ... D = matrix(eval(D)).rows() 24 | ... def Dn(k): 25 | ... ans = [] 26 | ... for d in Tuples(D, k): 27 | ... s = sum(A**n * d[n] for n in range(k)) 28 | ... ans.append(s) 29 | ... return ans 30 | ... G = points([v.list() for v in Dn(k)], size=50) 31 | ... show(G, frame=True, axes=False) 32 | 33 | 34 | .. end of output 35 | 36 | Options 37 | ====== 38 | 39 | The sage prompts can be removed by adding setting 'prompt_tag' option to False:: 40 | 41 | .. sagecellserver:: 42 | :prompt_tag: False 43 | 44 | Setting 'prompt_tag' to True has same effect as removing ':prompt_tag:'. 45 | 46 | During latex/pdf generation sagecell code can be displayed inside '\begin{verbatim}' and '\end{verbatim}' tags or as a single \textbf '***SAGE CELL***' message. This message is a reminder of sage cell exsistence. For example later this text can be manually replaced by screenshoot of sagcell example (mostly @interact example). 47 | 48 | This option is controlled using 'is_verbatim' option. Default is 'True'.:: 49 | 50 | .. sagecellserver:: 51 | :is_verbatim: True 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /contrib/sphinx2/icsecontrib/__init__.py: -------------------------------------------------------------------------------- 1 | __import__('pkg_resources').declare_namespace(__name__) 2 | 3 | 4 | -------------------------------------------------------------------------------- /contrib/sphinx2/icsecontrib/sagecellserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from docutils import nodes 5 | from docutils.parsers.rst import directives 6 | from sphinx.util.compat import Directive 7 | 8 | 9 | 10 | class sagecellserver(nodes.General, nodes.Element): 11 | pass 12 | 13 | 14 | def html_visit_sagecellserver_node(self, node): 15 | self.body.append("
") 16 | self.body.append("") 19 | self.body.append("
") 20 | 21 | 22 | def html_depart_sagecellserver_node(self, node): 23 | pass 24 | 25 | 26 | def latex_visit_sagecellserver_node(self, node): 27 | if node["is_verbatim"] == "True": 28 | self.body.append("\n\n") 29 | self.body.append("\\begin{verbatim}\n") 30 | self.body.append(node['python_code']) 31 | self.body.append("\n\end{verbatim}") 32 | self.body.append("\n\n") 33 | else: 34 | self.body.append("\n\\textbf{***SAGE CELL***}\n") 35 | 36 | 37 | def latex_depart_sagecellserver_node(self, node): 38 | pass 39 | 40 | 41 | class SageCellServer(Directive): 42 | has_content = True 43 | required_arguments = 0 44 | optional_arguments = 2 45 | option_spec = { 46 | "prompt_tag": directives.unchanged, 47 | "is_verbatim": directives.unchanged, 48 | } 49 | 50 | def run(self): 51 | if "prompt_tag" in self.options: 52 | annotation = self.options.get("prompt_tag") 53 | else: 54 | annotation = "False" 55 | 56 | if "is_verbatim" in self.options: 57 | is_verbatim = self.options.get("is_verbatim") 58 | else: 59 | is_verbatim = "True" 60 | 61 | content_list = self.content 62 | 63 | if annotation == "False": 64 | content_list = map(lambda x: x.replace("sage: ", "").replace("... ", ""), content_list) 65 | 66 | node = sagecellserver() 67 | node['is_verbatim'] = is_verbatim 68 | node['python_code'] = '\n'.join(content_list) 69 | 70 | return [node] 71 | 72 | 73 | def setup(app): 74 | app.add_node(sagecellserver, 75 | html = (html_visit_sagecellserver_node, html_depart_sagecellserver_node), 76 | latex = (latex_visit_sagecellserver_node, latex_depart_sagecellserver_node)) 77 | app.add_directive("sagecellserver", SageCellServer) 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /contrib/sphinx2/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {% block linktags %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | {{ super() }} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /contrib/sphinx2/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | long_desc = ''' 6 | This package contains the sage cell server Sphinx extension. 7 | 8 | The extension defines a directive, "sagecellserver", for embedding sage cell. 9 | ''' 10 | 11 | requires = ['Sphinx>=0.6', 'setuptools'] 12 | 13 | 14 | setup(name='icsecontrib-sagecellserver', 15 | version='1.1', 16 | description='Sphinx sagecellserver extension', 17 | author='Krzysztof Kajda', 18 | author_email='kajda.krzysztof@gmail.com', 19 | packages=find_packages(), 20 | include_package_data=True, 21 | install_requires=requires, 22 | namespace_packages=['icsecontrib'], 23 | ) 24 | -------------------------------------------------------------------------------- /contrib/stats/sagecell-stats.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "code", 12 | "collapsed": false, 13 | "input": [ 14 | "%pylab inline" 15 | ], 16 | "language": "python", 17 | "metadata": {}, 18 | "outputs": [] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "collapsed": false, 23 | "input": [ 24 | "#grep -v '\"::1\", \"\", \"service\",' s.log > s2.log\n", 25 | "#mv s2.log s.log\n", 26 | "\n", 27 | "\n", 28 | "import json\n", 29 | "from datetime import datetime\n", 30 | "def parseline(line):\n", 31 | " a,b=line.split('[', 1)\n", 32 | " c=a.split()\n", 33 | " d = datetime.strptime(c[4]+c[5], \"%Y-%m-%d%H:%M:%S,%f\")\n", 34 | " data = json.loads('['+b)\n", 35 | " return [d, c[3], data[1], data[2], data[3]]\n", 36 | "\n", 37 | "\n", 38 | "lines = []\n", 39 | "i=0\n", 40 | "errors=0\n", 41 | "skipped=0\n", 42 | "import gc\n", 43 | "gc.disable()\n", 44 | "with open('s.log') as f:\n", 45 | " for s in f:\n", 46 | " s=s.rstrip()\n", 47 | " i+=1\n", 48 | " if i%100000==0: \n", 49 | " print 'processing ',i,'lines'\n", 50 | " #if i>10000: break\n", 51 | " if s[-2:]!='\"]':\n", 52 | " # ignore lines that don't end correctly\n", 53 | " skipped+=1\n", 54 | " try:\n", 55 | " lines.append(parseline(s))\n", 56 | " except Exception as E:\n", 57 | " #print(i, E)\n", 58 | " errors+=1\n", 59 | "gc.enable()\n", 60 | "print(\"Errors: \",errors)\n", 61 | "print(\"Skipped: \",skipped)\n", 62 | "print(\"Processed: \",i)" 63 | ], 64 | "language": "python", 65 | "metadata": {}, 66 | "outputs": [] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "collapsed": false, 71 | "input": [ 72 | "len(lines)" 73 | ], 74 | "language": "python", 75 | "metadata": {}, 76 | "outputs": [] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "collapsed": false, 81 | "input": [ 82 | "lines[0]" 83 | ], 84 | "language": "python", 85 | "metadata": {}, 86 | "outputs": [] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "collapsed": false, 91 | "input": [ 92 | "import pandas\n", 93 | "#d=pandas.DataFrame(lines, index=columns=[\"time\", \"server\", \"ip\",\"url\",\"type\"])\n", 94 | "d=pandas.DataFrame.from_items(((l[0],l[1:]) for l in lines), \n", 95 | " columns=[\"server\", \"ip\",\"url\",\"type\"],\n", 96 | " orient='index').sort()\n", 97 | "d" 98 | ], 99 | "language": "python", 100 | "metadata": {}, 101 | "outputs": [] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "collapsed": false, 106 | "input": [ 107 | "from datetime import datetime\n", 108 | "dec10=datetime(2013,12,10)\n", 109 | "dec11=datetime(2013,12,11)\n", 110 | "d_dec10=d.ix[dec10:dec11]" 111 | ], 112 | "language": "python", 113 | "metadata": {}, 114 | "outputs": [] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "collapsed": false, 119 | "input": [ 120 | "d.groupby('ip').count().sort('ip',ascending=False)[:10]" 121 | ], 122 | "language": "python", 123 | "metadata": {}, 124 | "outputs": [] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "collapsed": false, 129 | "input": [ 130 | "d.groupby('server').count()" 131 | ], 132 | "language": "python", 133 | "metadata": {}, 134 | "outputs": [] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "collapsed": false, 139 | "input": [ 140 | "d.groupby('type').count()" 141 | ], 142 | "language": "python", 143 | "metadata": {}, 144 | "outputs": [] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "collapsed": false, 149 | "input": [ 150 | "d[d.url.str.contains('^(http|https)://[^.]*.ups.edu')]" 151 | ], 152 | "language": "python", 153 | "metadata": {}, 154 | "outputs": [] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "collapsed": false, 159 | "input": [ 160 | "notsagecell=d[~d.url.str.contains('^(http|https)://[^.]*.sagemath.org')]\n", 161 | "print(len(notsagecell))\n", 162 | "c=notsagecell.groupby('url').count().sort('url',ascending=False).take([0],axis=1)\n", 163 | "print(len(c))\n", 164 | "c[:500]" 165 | ], 166 | "language": "python", 167 | "metadata": {}, 168 | "outputs": [] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "collapsed": false, 173 | "input": [ 174 | "d.groupby('url').count().sort('url', ascending=False).take([0],axis=1)[:100]" 175 | ], 176 | "language": "python", 177 | "metadata": {}, 178 | "outputs": [] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "collapsed": false, 183 | "input": [ 184 | "daily=d['type'].resample('1D',how='count')\n", 185 | "print(daily.describe())\n", 186 | "daily.plot(kind='kde')" 187 | ], 188 | "language": "python", 189 | "metadata": {}, 190 | "outputs": [] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "collapsed": false, 195 | "input": [ 196 | "daily.plot()" 197 | ], 198 | "language": "python", 199 | "metadata": {}, 200 | "outputs": [] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "collapsed": false, 205 | "input": [ 206 | "pandas.set_option('display.max_rows', 500)" 207 | ], 208 | "language": "python", 209 | "metadata": {}, 210 | "outputs": [] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "collapsed": false, 215 | "input": [], 216 | "language": "python", 217 | "metadata": {}, 218 | "outputs": [] 219 | } 220 | ], 221 | "metadata": {} 222 | } 223 | ] 224 | } 225 | -------------------------------------------------------------------------------- /contrib/vm/README.md: -------------------------------------------------------------------------------- 1 | # Advanced Installation 2 | 3 | Here we describe how to setup a "production" instance of SageMathCell server. 4 | 5 | ## Create the "Enveloping Virtual Machine" (EVM). 6 | 7 | This is optional, if you are willing to dedicate a physical machine to SageMathCell. In any case **/var/lib/lxc must be a BTRFS system on an SSD.** 8 | 9 | It does not really matter how you create it. It is up to you also what resources you allocate to it. But something like 4 CPU cores, 32 GB RAM, and 200 GB SSD is a good starting point. 10 | 11 | **The automating scripts are NOT TESTED SINCE 2018.** Adjust them to current versions and your needs or use them just as a guidance, if desired. 12 | 13 | 1. Configure a package proxy, e.g. Apt-Cacher NG, for the machine that will host EVM. 14 | 2. Install KVM and configure it for your account, consult your OS documentation as necessary. 15 | 3. Download `build_host.sh` and `preseed.host` to some directory, that will be used also for the login script and SSH keys. 16 | 17 | ```bash 18 | mkdir ~/scevm 19 | cd ~/scevm 20 | wget https://raw.githubusercontent.com/sagemath/sagecell/master/contrib/vm/build_host.sh 21 | wget https://raw.githubusercontent.com/sagemath/sagecell/master/contrib/vm/preseed.host 22 | chmod a+x build_host.sh 23 | ``` 24 | 25 | 4. Make adjustments to `build_host.sh`, in particular the proxy and EVM resources. 26 | 5. Make adjustments to `preseed.host` if desired, e.g. change time zone and locale settings. 27 | 6. Run `build_host.sh` and wait: the installation make take half an hour or even longer with a slow Internet connection. Your terminal settings may get messed up in the process since we do not suppress the installation console, it will not happen after installation. 28 | 29 | ```bash 30 | ./build_host.sh 31 | ``` 32 | 33 | (If your `virt-install` does not understand `OSVARIANT=ubuntu18.04`, you may try a different version of Ubuntu here, while keeping the same `LOCATION`, `osinfo-query os` may be useful. Using a different base OS is probably possible, but will likely require further changes to build commands.) 34 | 35 | 7. You should get `ssh_host.sh` script that allows you to SSH as root to EVM. If it does not work, you probably need to adjust the IP address in it manually. Note that root password is disabled, you must use the SSH key generated during installation. 36 | 37 | ```bash 38 | ./ssh_host.sh 39 | ``` 40 | 41 | ## Inside of EVM 42 | 43 | 1. Download `container_manager.py` and make it executable (or run it with python3). 44 | 45 | ```bash 46 | wget https://raw.githubusercontent.com/sagemath/sagecell/master/contrib/vm/container_manager.py 47 | chmod a+x container_manager.py 48 | ``` 49 | 50 | 2. Adjust it if necessary. In particular, note that the default permalink database server is the public one. 51 | 3. Run it. The first time it adjusts system configuration and asks you to finish your session and start a new one. Then the base OS and the master SageMathCell container will be created. Expect it to take at least an hour. 52 | 53 | ```bash 54 | ./container_manager.py 55 | exit 56 | ./ssh_host.sh 57 | ./container_manager.py 58 | ``` 59 | 60 | 4. To create several compute nodes behind a load balancer, run 61 | 62 | ```bash 63 | ./container_manager.py --deploy 64 | ``` 65 | 66 | ## Outside of EVM 67 | 68 | 1. Configure HTTP and/or HTTPS access to EVM:80. HTTPS has to be decrypted before EVM, but it is recommended to avoid certain connection problems. If you are using HA-Proxy, you can add the following sections to `/etc/haproxy/haproxy.conf`: 69 | 70 | ``` 71 | frontend http 72 | bind *:80 73 | bind *:443 ssl crt /etc/haproxy/cert/your_cerificate.pem 74 | option forwardfor 75 | http-request add-header X-Proto https if { ssl_fc } 76 | use_backend sagemathcell 77 | 78 | backend sagemathcell 79 | server sagemathcell_evm sagemathcell_evm_ip_address:80 check 80 | ``` 81 | 82 | If you are using Apache, a possible proxy configuration (with `proxy_wstunnel` module enabled) is 83 | 84 | ``` 85 | ProxyPass /sockjs/info http://sagemathcell_evm_ip_address:80/sockjs/info 86 | ProxyPass /sockjs/ ws://sagemathcell_evm_ip_address:80/sockjs/ 87 | ProxyPass / http://sagemathcell_evm_ip_address:80/ 88 | ProxyPassReverse / http://sagemathcell_evm_ip_address:80/ 89 | ProxyPreserveHost On 90 | ``` 91 | 92 | 2. Configure (restricted) access to EVM:8888 for testing newer versions of SageMathCell. 93 | 3. Configure (restricted) access to EVM:9999 for HA-Proxy statistics page. 94 | 4. If you are going to run multiple EVMs, consider adjusting `/etc/rsyslog.d/sagecell.conf` in them to collect all logs on a single server. 95 | 96 | ## Maintenance Notes 97 | 98 | 1. Used GitHub repositories are cached in `/root/github/` to reduce download size for upgrades. 99 | 2. EVM is configured to install security updates automatically. 100 | 3. Master containers are always fully updated before cloning deployment nodes. 101 | 4. Deployment nodes are configured to install security updates automatically. 102 | 5. EVM, deployment, and test containers should start automatically after reboot of the host machine. 103 | 104 | ## Upgrading 105 | 106 | 1. Check if you have any custom changes to `container_manager.py` and save them in a patch: 107 | 108 | ```bash 109 | diff -u github/sagecell/contrib/vm/container_manager.py container_manager.py 110 | diff -u github/sagecell/contrib/vm/container_manager.py container_manager.py > local.patch 111 | ``` 112 | 113 | 2. Pull the latest branch to your saved repository and look over changes: 114 | 115 | ```bash 116 | cd github/sagecell/ && git pull && cd && diff -u container_manager.py github/sagecell/contrib/vm/container_manager.py 117 | ``` 118 | 119 | 3. If everything looks OK to you, apply your patch over new version of the script: 120 | 121 | ```bash 122 | cp github/sagecell/contrib/vm/container_manager.py . && patch container_manager.py local.patch 123 | ``` 124 | 125 | 4. Build new versions of all containers and deploy: 126 | 127 | ```bash 128 | ./container_manager.py -b -s -p -m --deploy 129 | ``` 130 | 131 | Note that after the new version is started, the script will wait for a couple hours to make sure that users in the middle of interacting with the old one have finished their work. 132 | 5. If you want to first test the new version while keeping the old one in production, run instead 133 | 134 | ```bash 135 | ./container_manager.py -b -s -p -m -t 136 | ``` 137 | 138 | and once you are satisfied with it 139 | 140 | ```bash 141 | ./container_manager.py --deploy 142 | ``` 143 | 144 | 6. If you know that only some changes to SageMathCell source code were made, you can skip building Sage and its packages from scratch: `./container_manager.py -m` 145 | 7. For some other options check the built-in help: `./container_manager.py -h` 146 | 147 | **If these instructions are unclear or do not work, please let us know!** 148 | -------------------------------------------------------------------------------- /contrib/vm/build_host.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit 3 | set -o nounset 4 | set -o xtrace 5 | 6 | APTPROXY="http://your-proxy-here-or-delete-proxy-from-preseed/" 7 | MAC=52:54:00:5a:9e:c1 # Must start with 52:54:00 8 | 9 | VM_NAME=schostvm 10 | OSVARIANT=ubuntu18.04 11 | LOCATION="http://archive.ubuntu.com/ubuntu/dists/bionic/main/installer-amd64/" 12 | 13 | sed "s|APTPROXY|$APTPROXY|" preseed.host > preseed.cfg 14 | 15 | if ! [ -f ${VM_NAME}_rsa ]; then 16 | ssh-keygen -q -N "" -C "sc_admin" -f ${VM_NAME}_rsa 17 | fi 18 | SSHKEY=`cat ${VM_NAME}_rsa.pub` 19 | sed -i "s|SSHKEY|$SSHKEY|" preseed.cfg 20 | 21 | virt-install \ 22 | --connect qemu:///system \ 23 | --name $VM_NAME \ 24 | --description "Host for SageCell instances." \ 25 | --ram=32768 \ 26 | --cpu host \ 27 | --vcpus=12 \ 28 | --location $LOCATION \ 29 | --initrd-inject=preseed.cfg \ 30 | --extra-args="console=ttyS0" \ 31 | --os-type=linux \ 32 | --os-variant=$OSVARIANT \ 33 | --disk pool=default,size=100,device=disk,bus=virtio,format=qcow2,cache=writeback \ 34 | --network network=default,model=virtio,mac=$MAC \ 35 | --graphics none \ 36 | --autostart \ 37 | --check-cpu \ 38 | --noreboot \ 39 | 40 | # For a dedicated partition use 41 | #--disk path=/dev/???,bus=virtio \ 42 | 43 | # Make a script to SSH inside, assuming that IP will not change 44 | virsh --connect qemu:///system start $VM_NAME 45 | sleep 30 46 | IP=`ip n|grep $MAC|grep -Eo "^[0-9.]{7,15}"` 47 | cat < ssh_host.sh 48 | #!/usr/bin/env bash 49 | 50 | if [[ \`virsh --connect qemu:///system domstate $VM_NAME\` != "running" ]]; then 51 | if ! virsh --connect qemu:///system start $VM_NAME; then 52 | echo "Failed to start $VM_NAME" 53 | exit 1 54 | fi 55 | sleep 30 56 | fi 57 | 58 | ssh -i ${VM_NAME}_rsa root@$IP 59 | EOF 60 | chmod u+x ssh_host.sh 61 | # Power cycle VM, otherwise bash completion does not work for LXC. 62 | virsh --connect qemu:///system shutdown $VM_NAME 63 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/config.py: -------------------------------------------------------------------------------- 1 | import config_default 2 | 3 | 4 | # Global database running on Google Compute Engine with a static IP 5 | db = "web" 6 | db_config = {"uri": "http://130.211.113.153"} 7 | 8 | requires_tos = False 9 | 10 | pid_file = '/home/{server}/sagecell.pid' 11 | 12 | config_default.provider_settings.update({ 13 | "max_kernels": 80, 14 | "max_preforked": 10, 15 | }) 16 | 17 | config_default.provider_info.update({ 18 | "username": "{worker}", 19 | }) 20 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/etc/apt/apt.conf.d/20auto-upgrades: -------------------------------------------------------------------------------- 1 | APT::Periodic::Update-Package-Lists "1"; 2 | APT::Periodic::Unattended-Upgrade "1"; 3 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/etc/cron.d/sagecell-cleantmp: -------------------------------------------------------------------------------- 1 | */30 * * * * root /usr/sbin/tmpreaper --mtime-dir 2h /tmp 2 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/etc/cron.d/sagecell-monitor: -------------------------------------------------------------------------------- 1 | */2 * * * * root /root/check_sagecell http://localhost:8888 >> /root/check_sagecell.log 2>&1 2 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/etc/logrotate.d/0-sagecell: -------------------------------------------------------------------------------- 1 | /var/log/syslog 2 | /var/log/sagecell.log 3 | { 4 | size 1G 5 | rotate 5 6 | missingok 7 | notifempty 8 | compress 9 | delaycompress 10 | postrotate 11 | /usr/lib/rsyslog/rsyslog-rotate 12 | endscript 13 | } 14 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/etc/nginx/conf.d/sagecell.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8889; 3 | gzip on; 4 | gzip_types *; 5 | root /home/{server}/sagecell; 6 | location /static/ { 7 | add_header Access-Control-Allow-Origin $http_origin; 8 | } 9 | location = /static/jsmol/php/jsmol.php { 10 | # Script adds Access-Control-Allow-Origin * 11 | include snippets/fastcgi-php.conf; 12 | fastcgi_pass unix:/run/php/php7.2-fpm.sock; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/etc/rsyslog.d/sagecell.conf: -------------------------------------------------------------------------------- 1 | global(maxMessageSize="64k") 2 | 3 | module(load="omrelp") 4 | 5 | template(name="sagecell_local" type="list") { 6 | property(name="syslogtag") 7 | property(name="msg" spifno1stsp="on") 8 | property(name="msg" droplastlf="on") 9 | constant(value=" #") 10 | property(name="syslogseverity-text" caseconversion="upper") 11 | constant(value="\n") 12 | } 13 | 14 | if $syslogfacility-text == "local3" then 15 | { 16 | action(type="omfile" 17 | file="/var/log/sagecell.log" 18 | template="sagecell_local") 19 | if $syslogseverity-text != "debug" and $msg contains " sagecell.stats " then 20 | action(type="omrelp" 21 | target="10.0.3.1" 22 | port="12514" 23 | action.resumeRetryCount="-1" 24 | queue.type="linkedList" 25 | queue.filename="sagecell" 26 | queue.maxDiskSpace="1g" 27 | queue.saveOnShutdown="on") 28 | stop 29 | } 30 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/etc/security/limits.d/sagecell.conf: -------------------------------------------------------------------------------- 1 | {worker} hard fsize 1048576 2 | {worker} hard as 8388608 3 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/etc/systemd/system/sagecell.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SageMathCell computation server 3 | 4 | 5 | [Service] 6 | Type=notify 7 | NotifyAccess=all 8 | Restart=always 9 | SyslogIdentifier=SageMathCell 10 | 11 | ExecStartPre=/root/firewall 12 | ExecStartPre=\ 13 | -/usr/bin/pkill -u {worker} ;\ 14 | /bin/rm -rf /tmp/sagecell ;\ 15 | /bin/mkdir /tmp/sagecell ;\ 16 | /bin/chown {server}:{group} /tmp/sagecell ;\ 17 | /bin/chmod g=wxs,o= /tmp/sagecell 18 | 19 | PermissionsStartOnly=true 20 | WorkingDirectory=/home/{server}/sagecell 21 | User={server} 22 | ExecStart=/home/{server}/sage/sage web_server.py 23 | 24 | 25 | [Install] 26 | WantedBy=multi-user.target 27 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/root/check_sagecell: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | systemctl --quiet is-active sagecell 4 | if [ $? -ne 0 ] 5 | then 6 | echo "`date` Service is not active, skipping check" 7 | exit 0 8 | fi 9 | 10 | /home/{server}/sagecell/contrib/sagecell-client/sagecell-service.py $1 11 | if [ $? -ne 0 ] 12 | then 13 | echo "`date` Error in server. Restarting..." 14 | systemctl restart sagecell 15 | echo "`date` Restarted." 16 | echo "************" 17 | exit 1 18 | fi 19 | -------------------------------------------------------------------------------- /contrib/vm/compute_node/root/firewall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ipset create allowed hash:net 4 | 5 | ipset add allowed 130.211.113.153 # Permalinks 6 | ipset add allowed 91.189.88.0/24 # Canonical Group Limited, archive.ubuntu.com 7 | ipset add allowed 185.125.188.0/22 8 | ipset add allowed 128.174.0.0/16 # University of Illinois, Macaulay2 9 | 10 | ipset add allowed 160.79.104.10 # api.anthropic.com 11 | ipset add allowed 104.18.26.90 # api.deepseek.com.cdn.cloudflare.net 12 | ipset add allowed 104.18.27.90 13 | ipset add allowed 162.159.140.245 # api.openai.com 14 | ipset add allowed 172.66.0.243 15 | ipset add allowed 104.18.18.80 # api.x.ai 16 | ipset add allowed 104.18.19.80 17 | 18 | ipset add allowed 217.160.0.231 # codetables.de 19 | ipset add allowed 162.255.119.162 # data.astropy.org 20 | ipset add allowed 94.136.40.82 # designtheory.org 21 | ipset add allowed 141.38.0.0/16 # Deutscher Wetterdienst 22 | ipset add allowed 162.125.0.0/16 # Dropbox 23 | ipset add allowed 193.144.0.0/14 # ESA - Villafranca Satellite Tracking Station 24 | ipset add allowed 88.131.0.0/16 # European Centre for Disease Prevention and Control 25 | ipset add allowed 151.101.0.0/16 # Fastly, PyPi.org 26 | 27 | ipset add allowed 140.82.112.0/20 # GitHub 28 | ipset add allowed 185.199.108.0/22 29 | 30 | ipset add allowed 64.233.160.0/19 # Google services 31 | ipset add allowed 74.125.0.0/16 32 | ipset add allowed 108.177.0.0/17 33 | ipset add allowed 142.250.0.0/15 34 | ipset add allowed 172.217.0.0/16 35 | ipset add allowed 173.194.0.0/16 36 | ipset add allowed 192.178.0.0/15 37 | ipset add allowed 209.85.128.0/17 38 | ipset add allowed 216.58.192.0/19 39 | 40 | ipset add allowed 89.107.186.22 # graphclasses.org 41 | ipset add allowed 131.142.0.0/16 # Harvard-Smithsonian Center for Astrophysics 42 | 43 | ipset add allowed 108.128.53.182 # elb.koboroute.org 44 | ipset add allowed 52.214.202.32 45 | ipset add allowed 54.77.106.83 46 | ipset add allowed 34.207.37.27 # elb.kobotoolbox.org 47 | ipset add allowed 34.232.35.237 48 | 49 | ipset add allowed 35.155.106.45 # ljcr.dmgordon.org/mathtrek.eu 50 | ipset add allowed 35.166.140.139 51 | 52 | ipset add allowed 82.165.72.196 # mathtrek.eu 53 | 54 | ipset add allowed 104.18.22.152 # api.mistral.ai 55 | ipset add allowed 104.18.23.152 56 | 57 | ipset add allowed 129.164.0.0/16 # NASA 58 | ipset add allowed 137.78.0.0/16 59 | 60 | ipset add allowed 206.188.192.120 # neilsloane.com 61 | ipset add allowed 104.239.138.29 # oeis.org 62 | ipset add allowed 216.105.38.13 # sourceforge.net 63 | ipset add allowed 108.167.142.230 # tssfl.com 64 | ipset add allowed 104.244.40.0/21 # Twitter 65 | ipset add allowed 130.88.0.0/16 # University of Manchester 66 | ipset add allowed 130.79.0.0/16 # Universite de Strasbourg 67 | ipset add allowed 137.208.0.0/16 # Vienna University of Economics and Business, CRAN 68 | ipset add allowed 208.80.152.0/22 # Wikimedia 69 | ipset add allowed 134.147.222.194 # www.findstat.org 70 | ipset add allowed 35.244.233.98 # www.kaggle.com 71 | 72 | ipset add allowed 69.147.64.0/18 # Yahoo 73 | ipset add allowed 188.125.95.0/24 74 | 75 | 76 | iptables --flush 77 | # Loopback traffic 78 | iptables -A INPUT -i lo -j ACCEPT 79 | iptables -A OUTPUT -o lo -j ACCEPT 80 | # Established connections 81 | iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 82 | iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 83 | 84 | # Outbound DHCP request 85 | iptables -A OUTPUT -p udp --dport 67:68 --sport 67:68 -j ACCEPT 86 | # Outbound DNS lookups 87 | iptables -A OUTPUT -p udp -m udp --dport 53 -j ACCEPT 88 | # Outbound Network Time Protocol (NTP) requests 89 | iptables -A OUTPUT -p udp --dport 123 --sport 123 -j ACCEPT 90 | 91 | # Incoming connections to SageCell server for computations 92 | iptables -A INPUT -p tcp --dport 8888 -j ACCEPT 93 | # Incoming connections to nginx for static content 94 | iptables -A INPUT -p tcp --dport 8889 -j ACCEPT 95 | 96 | # Outbound HTTP to allowed hosts 97 | iptables -A OUTPUT -p tcp -m multiport --dport http,https -m state --state NEW -m set --match-set allowed dst -j ACCEPT 98 | 99 | # And nothing else 100 | iptables -P INPUT DROP 101 | iptables -P FORWARD DROP 102 | iptables -P OUTPUT DROP 103 | -------------------------------------------------------------------------------- /contrib/vm/permalink_database/etc/systemd/system/permalink_database.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SageMathCell Permalink Server 3 | 4 | 5 | [Service] 6 | Type=notify 7 | NotifyAccess=all 8 | Restart=always 9 | SyslogIdentifier=SageMathCell 10 | 11 | ExecStartPre=\ 12 | /sbin/iptables -t nat --flush PREROUTING ;\ 13 | /sbin/iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 14 | 15 | 16 | PermissionsStartOnly=true 17 | WorkingDirectory=/home/sc_data 18 | User=sc_data 19 | ExecStart=/usr/bin/python3 sagecell/permalink_server.py 20 | 21 | 22 | [Install] 23 | WantedBy=multi-user.target 24 | -------------------------------------------------------------------------------- /contrib/vm/preseed.host: -------------------------------------------------------------------------------- 1 | ### Localization 2 | d-i debian-installer/language string en 3 | d-i debian-installer/country string US 4 | d-i debian-installer/locale string en_US.UTF-8 5 | 6 | # Keyboard selection. 7 | d-i console-setup/ask_detect boolean false 8 | d-i keyboard-configuration/layoutcode string us 9 | 10 | ### Network configuration 11 | d-i netcfg/choose_interface select auto 12 | 13 | # If you have a slow dhcp server and the installer times out waiting for 14 | # it, this might be useful. 15 | #d-i netcfg/dhcp_timeout string 60 16 | 17 | # Any hostname and domain names assigned from dhcp take precedence over 18 | # values set here. However, setting the values still prevents the questions 19 | # from being shown, even if values come from dhcp. 20 | d-i netcfg/get_hostname string schostvm 21 | d-i netcfg/get_domain string unassigned-domain 22 | 23 | ### Mirror settings 24 | d-i mirror/country string manual 25 | d-i mirror/http/hostname string archive.ubuntu.com 26 | d-i mirror/http/directory string /ubuntu 27 | d-i mirror/http/proxy string APTPROXY 28 | 29 | ### Clock and time zone setup 30 | # Controls whether or not the hardware clock is set to UTC. 31 | d-i clock-setup/utc boolean true 32 | 33 | # You may set this to any valid setting for $TZ; see the contents of 34 | # /usr/share/zoneinfo/ for valid values. 35 | d-i time/zone string US/Pacific 36 | 37 | # Controls whether to use NTP to set the clock during the install 38 | d-i clock-setup/ntp boolean true 39 | # NTP server to use. The default is almost always fine here. 40 | #d-i clock-setup/ntp-server string ntp.example.com 41 | 42 | ### Partitioning 43 | d-i partman-auto/method string regular 44 | d-i partman-lvm/device_remove_lvm boolean true 45 | d-i partman-md/device_remove_md boolean true 46 | d-i partman-lvm/confirm boolean true 47 | #d-i partman-auto/choose_recipe select atomic 48 | #d-i partman/default_filesystem string btrfs 49 | 50 | d-i partman-auto/expert_recipe string small-swap : \ 51 | 16384 65536 -1 btrfs \ 52 | $primary{ } $bootable{ } \ 53 | method{ format } format{ } \ 54 | use_filesystem{ } filesystem{ btrfs } \ 55 | options/compress{ } \ 56 | mountpoint{ / } . \ 57 | 1024 4096 50% linux-swap \ 58 | method{ swap } format{ } . 59 | 60 | # This makes partman automatically partition without confirmation, provided 61 | # that you told it what to do using one of the methods above. 62 | d-i partman-partitioning/confirm_write_new_label boolean true 63 | d-i partman/choose_partition select finish 64 | d-i partman/confirm boolean true 65 | d-i partman/confirm_nooverwrite boolean true 66 | 67 | ### Account setup 68 | d-i passwd/root-login boolean true 69 | # Root password, either in clear text 70 | d-i passwd/root-password password disabled 71 | d-i passwd/root-password-again password disabled 72 | # or encrypted using an MD5 hash. 73 | #d-i passwd/root-password-crypted password [MD5 hash] 74 | d-i passwd/make-user boolean false 75 | 76 | ### Package selection 77 | tasksel tasksel/first multiselect openssh-server, server, standard 78 | 79 | # Individual additional packages to install 80 | d-i pkgsel/include string haproxy lxc python3-lxc python3-psutil rsyslog-relp 81 | 82 | # Whether to upgrade packages after debootstrap. 83 | # Allowed values: none, safe-upgrade, full-upgrade 84 | d-i pkgsel/upgrade select safe-upgrade 85 | 86 | # Policy for applying updates. May be "none" (no automatic updates), 87 | # "unattended-upgrades" (install security updates automatically), or 88 | # "landscape" (manage system with Landscape). 89 | d-i pkgsel/update-policy select unattended-upgrades 90 | 91 | # By default, the system's locate database will be updated after the 92 | # installer has finished installing most packages. This may take a while, so 93 | # if you don't want it, you can set this to "false" to turn it off. 94 | d-i pkgsel/updatedb boolean false 95 | 96 | ### Boot loader installation 97 | # This is fairly safe to set, it makes grub install automatically to the MBR 98 | # if no other operating system is detected on the machine. 99 | d-i grub-installer/only_debian boolean true 100 | # This one makes grub-installer install to the MBR if it also finds some other 101 | # OS, which is less safe as it might not be able to boot that other OS. 102 | d-i grub-installer/with_other_os boolean true 103 | 104 | ### Finishing up the installation 105 | # During installations from serial console, the regular virtual consoles 106 | # (VT1-VT6) are normally disabled in /etc/inittab. Uncomment the next 107 | # line to prevent this. 108 | #d-i finish-install/keep-consoles boolean true 109 | 110 | # Avoid that last message about the install being complete. 111 | d-i finish-install/reboot_in_progress note 112 | 113 | #### Advanced options 114 | ### Running custom commands during the installation 115 | # This command is run just before the install finishes, but when there is 116 | # still a usable /target directory. You can chroot to /target and use it 117 | # directly, or use the apt-install and in-target commands to easily install 118 | # packages and run commands in the target system. 119 | #d-i preseed/late_command string apt-install zsh; in-target chsh -s /bin/zsh 120 | 121 | d-i preseed/late_command string \ 122 | cd /target/root; \ 123 | mkdir .ssh; \ 124 | chmod 0700 .ssh; \ 125 | echo 'SSHKEY' > .ssh/authorized_keys; \ 126 | chmod 0600 .ssh/authorized_keys; \ 127 | in-target /usr/bin/passwd -l root 128 | -------------------------------------------------------------------------------- /db.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generic Database Adapter 3 | 4 | The database is used for storing execute requests and 5 | permalinks, as well as any extra logging required by 6 | the web server and/or backend. 7 | """ 8 | 9 | 10 | class DB(object): 11 | """ 12 | Abstract base class for database adaptors. 13 | """ 14 | 15 | async def add(self, code, language, interacts): 16 | """ 17 | Add an entry to the database. 18 | 19 | INPUT: 20 | 21 | - ``code`` -- a string 22 | 23 | - ``language`` -- a string 24 | 25 | - ``interacts`` -- a string 26 | 27 | OUTPUT: 28 | 29 | - a string -- the identifier key for the entry 30 | """ 31 | raise NotImplementedError 32 | 33 | async def get(self, key): 34 | """ 35 | Retrieve the entry from the database matching ``key``. 36 | 37 | INPUT: 38 | 39 | - ``key`` -- a string 40 | 41 | OUTPUT: 42 | 43 | - a tuple of three strings: the code, the language, the interact state. 44 | """ 45 | raise NotImplementedError 46 | -------------------------------------------------------------------------------- /db_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | """ 2 | SQLAlchemy Database Adapter 3 | --------------------------- 4 | """ 5 | 6 | from datetime import datetime 7 | import random 8 | import string 9 | 10 | from sqlalchemy import create_engine, Column, Integer, String, DateTime 11 | from sqlalchemy.orm import declarative_base, sessionmaker 12 | from sqlalchemy.exc import IntegrityError 13 | 14 | import db 15 | 16 | 17 | Base = declarative_base() 18 | 19 | 20 | class ExecMessage(Base): 21 | """ 22 | Table of input messages in JSON form. 23 | """ 24 | __tablename__ = "permalinks" 25 | ident = Column(String, primary_key=True, index=True) 26 | code = Column(String) 27 | language = Column(String) 28 | interacts = Column(String) 29 | created = Column(DateTime, default=datetime.utcnow) 30 | last_accessed = Column( 31 | DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 32 | requested = Column(Integer, default=0) 33 | 34 | def __repr__(self): 35 | return """\ 36 | ident: {} 37 | Code: 38 | {} 39 | Interacts: 40 | {} 41 | Language: {} 42 | Created: {} 43 | Last accessed: {} 44 | Requested: {}""".format( 45 | self.ident, 46 | self.code, 47 | self.interacts, 48 | self.language, 49 | self.created, 50 | self.last_accessed, 51 | self.requested) 52 | 53 | 54 | class DB(db.DB): 55 | """ 56 | SQLAlchemy database adapter 57 | 58 | :arg db_file str: the SQLAlchemy URI for a database file 59 | """ 60 | 61 | def __init__(self, db_file): 62 | self.engine = create_engine(db_file) 63 | self.SQLSession = sessionmaker(bind=self.engine) 64 | Base.metadata.create_all(self.engine) 65 | self.dbsession = self.SQLSession() 66 | 67 | async def add(self, code, language, interacts): 68 | """ 69 | See :meth:`db.DB.add` 70 | """ 71 | while True: 72 | ident = "".join( 73 | random.choice(string.ascii_lowercase) for _ in range(6)) 74 | message = ExecMessage( 75 | ident=ident, 76 | code=code, 77 | language=language, 78 | interacts=interacts) 79 | try: 80 | self.dbsession.add(message) 81 | self.dbsession.commit() 82 | except IntegrityError: 83 | # ident was used before 84 | self.dbsession.rollback() 85 | else: 86 | break 87 | return ident 88 | 89 | async def get(self, key): 90 | """ 91 | See :meth:`db.DB.get` 92 | """ 93 | msg = self.dbsession.query(ExecMessage).filter_by(ident=key).first() 94 | if msg is None: 95 | raise LookupError 96 | msg.requested = ExecMessage.requested + 1 97 | self.dbsession.commit() 98 | return (msg.code, msg.language, msg.interacts) 99 | -------------------------------------------------------------------------------- /db_web.py: -------------------------------------------------------------------------------- 1 | """ 2 | Web Database Adapter 3 | """ 4 | 5 | import json 6 | import urllib 7 | 8 | 9 | import tornado.httpclient 10 | 11 | 12 | import db 13 | 14 | 15 | class DB(db.DB): 16 | """ 17 | :arg URL str: the URL for the key-value store 18 | """ 19 | 20 | def __init__(self, url): 21 | self.url = url 22 | 23 | async def add(self, code, language, interacts): 24 | """ 25 | See :meth:`db.DB.add` 26 | """ 27 | body = urllib.parse.urlencode({ 28 | "code": code.encode("utf8"), 29 | "language": language.encode("utf8"), 30 | "interacts": interacts.encode("utf8")}) 31 | http_client = tornado.httpclient.AsyncHTTPClient() 32 | response = await http_client.fetch( 33 | self.url, method="POST", body=body, 34 | headers={"Accept": "application/json"}) 35 | return json.loads(response.body)["query"] 36 | 37 | async def get(self, key): 38 | """ 39 | See :meth:`db.DB.get` 40 | """ 41 | http_client = tornado.httpclient.AsyncHTTPClient() 42 | response = await http_client.fetch( 43 | "{}?q={}".format(self.url, key), method="GET", 44 | headers={"Accept": "application/json"}) 45 | return json.loads(response.body) 46 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | ../README.md 2 | -------------------------------------------------------------------------------- /doc/future.rst: -------------------------------------------------------------------------------- 1 | ====================================== 2 | Notes for Possible Future Directions 3 | ====================================== 4 | 5 | Goals 6 | ----- 7 | 8 | * More scalable service that takes advantage of recent work by the iPython team to increase performance and vastly reduce code complexity 9 | 10 | * Overall consistency with the iPython messaging scheme 11 | 12 | Overall Design 13 | -------------- 14 | 15 | .. image:: ./_static/rewrite_design.png 16 | :align: center 17 | 18 | Typical computation request:: 19 | 20 | USER writes code to be executed 21 | CLIENT formats code into iPython execute_request message 22 | CLIENT sends request message to SERVER 23 | SERVER stores request message in DB and sends identifying permalinking back to client 24 | SERVER starts KERNEL session 25 | SERVER opens websocket/ZMQ connection linking CLIENT to KERNEL through SERVER 26 | SERVER sends request message to KERNEL through websocket 27 | KERNEL executes user code and sends output messages to CLIENT through websocket 28 | KERNEL uploads files to DB 29 | KERNEL notifies user of files in DB 30 | KERNEL session ends, websocket closes 31 | 32 | Typical interact request:: 33 | 34 | USER writes code to be executed 35 | CLIENT formats code into iPython execute_request message 36 | CLIENT sends request message to SERVER 37 | SERVER stores request message in DB and sends identifying permalking back to client 38 | SERVER starts KERNEL session 39 | SERVER opens websocket/ZMQ connection linking CLIENT to KERNEL through SERVER 40 | SERVER sends request message to KERNEL through websocket 41 | KERNEL executes user code and sends output messages to CLIENT through websocket 42 | USER sends additional request messages to KERNEL through websocket 43 | KERNEL continues to execute user code and sends output messages to CLIENT 44 | KERNEL uploads files to DB 45 | KERNEL notifies user of files in DB 46 | KERNEL session ends, websocket closes 47 | 48 | Significant Changes 49 | ------------------- 50 | 51 | Server: 52 | 53 | * The biggest change is a preference towards using websockets, rather than a continuing request/reply model managed by the client 54 | 55 | * However, as a fallback, we should still support something like long-polling for a mobile app API or in other scenarios where websockets are not available 56 | 57 | * In terms of software, the plan is to switch to Tornado from Flask, since Tornado is designed for high performance, scalability, and the type of request model we want to support (websockets, long polling, etc). Tornado can also be run behind nginx for more scalability. 58 | 59 | Database: 60 | 61 | * The DB no longer serves as a buffer for messages back to the server, only as a storage location for files and permalinks. 62 | 63 | * Specific software TBD, especially since it plays a much smaller role than before. 64 | 65 | Kernel Backend: 66 | 67 | * Specifics TBD. At minimum, this should have some separation of "trusted" and "untrusted" portions so that arbitrary user code can theoretically be run without any sort of recourse on a separate account and/or machine. 68 | 69 | * Hopefully, we can use and/or extend iPython's built-in parallel architecture along with a websocket/zmq bridge to eliminate most of the current multiprocessing device code. 70 | 71 | Client Javascript: 72 | 73 | * The goal is to use javascript from the iPython notebook as the basis for session management, because it introduces useful features such as an integrated event system built on top of jQuery events as well as a heavier focus on callback functions, etc. 74 | 75 | 76 | Notes on using IPython workers 77 | ------------------------------ 78 | 79 | Issues when migrating to using IPython 0.12+ hub and engines: 80 | 81 | * We'd have to figure out a way to spawn an engine easily by forking a process, with custom initialization and cleanup (like putting files in a directory and cleaning up the files afterwards). 82 | 83 | * We'd need to have a way to "wrap" any messages sent out, to translate them GAE channel messages, for example. The IPython notebook seems to do this already with their websocket/zmq bridge. 84 | 85 | 86 | Roadmap and Future Work 87 | ----------------------- 88 | 89 | Going forward, here are some areas that we need to explore: 90 | 91 | * Implement stats or analytics of some type. Google analytics seems like it won't work very well in the embedding scenario. We can use pyzmq to add a [zmq logger](http://zeromq.github.com/pyzmq/logging.html) (so we get asynchronous logging). Getting nice graphs and things from this is something else. 92 | 93 | * Extensive load-testing. We don't seem to be able to handle huge load yet. We're still rebooting 4-5 times a day because of 30-second timeout errors (with one server) 94 | 95 | * Also, we should figure out the right HAProxy settings to load balance across several tornado servers. Currently, it seems like we have problems with the sessions getting mixed up between backends. There are lots KeyErrors where a session can't find a kernel or something. 96 | 97 | * Get something like nginx to server static files. 98 | 99 | * (Lower priority: ) Move interact improvements back to sage notebook 100 | 101 | * Experiment with better interacts using the global dict that notifies of variable changes. 102 | 103 | Here are a few things to keep our eyes on: 104 | 105 | Secure ZMQ: 106 | 107 | * Salt: https://github.com/thatch45/salt The ZMQ secure pub/sub spec is based on it too: http://www.zeromq.org/topics:pubsub-security. 108 | * tcpcrypt: http://groups.google.com/group/sage-devel/browse_thread/thread/4e64f206fe980ebd 109 | * ssh tunnels 110 | 111 | Interacts: 112 | 113 | * using jsxgraph? See gh-197, http://jsxgraph.uni-bayreuth.de/wiki/index.php/Circles_on_circles, http://sage.cs.drake.edu/home/pub/81/, and http://sage.cs.drake.edu/home/pub/79/ 114 | * jquerymobile is released: http://jquerymobile.com/. We should look at switching to that for interacts instead of jqueryui. See also gh-78. 115 | 116 | Communication: 117 | 118 | * socket.io is a javascript library that allows for a variety of smarter communication that simple polling. See also https://groups.google.com/forum/#!topic/sage-notebook/JbJSULEX3hA 119 | 120 | Files: 121 | 122 | * We could have drag-and-drop uploading: http://blueimp.github.com/jQuery-File-Upload/ 123 | -------------------------------------------------------------------------------- /doc/timing.rst: -------------------------------------------------------------------------------- 1 | Timing 2 | ======= 3 | 4 | .. automodule:: timing 5 | 6 | 7 | Test Scripts 8 | ------------ 9 | 10 | Timing Utilities 11 | ^^^^^^^^^^^^^^^^ 12 | 13 | .. automodule:: timing.test_scripts.timing_util 14 | 15 | Timing Tests 16 | ^^^^^^^^^^^^ 17 | 18 | .. automodule:: timing.test_scripts.simple_computation 19 | 20 | Testing 21 | ------- 22 | 23 | Here are some tests that should be written: 24 | 25 | * an interact (maybe where the user waits a small random amount of time, then "moves the slider", another small random amount of time and "changes an input", etc. 26 | 27 | * upload a file, do some operation on the file, and then get the result (and the resulting file) 28 | 29 | * a longer computation than just summing two numbers. Maybe a for loop that calculates a factorial of a big number or something. 30 | 31 | * generate a file in code (maybe a matplotlib plot) and download the resulting image 32 | 33 | * Exercise the "Sage Mode" --- that should also be an option for all of the above 34 | 35 | * Sage-specific preparser tests. 36 | 37 | * tests exercising memory and cputime limits:: 38 | 39 | import time 40 | a = [] 41 | for i in range(20): 42 | a.append([0] * 50000000) 43 | time.sleep(1) 44 | print(get_memory_usage()) 45 | 46 | or for time limits:: 47 | 48 | factor(2^4994-3^344) 49 | -------------------------------------------------------------------------------- /dynamic.py: -------------------------------------------------------------------------------- 1 | # Modified from sage_salvus.py in the sagemath/cloud github project 2 | # Original version licensed GPLv2+ by William Stein 3 | 4 | #TODO: Need some way of having controls without output and without 5 | #having a 'dirty' indicator. Just javascript controls. This should 6 | #be an argument to interact, like @interact(output=False) or something 7 | 8 | # also, need an easy way to make controls read-only (especially if 9 | # they are just displaying a value) 10 | 11 | import sys 12 | from interact_sagecell import interact 13 | def _dynamic(var, control=None): 14 | if control is None: 15 | control = sys._sage_.namespace.get(var,'') 16 | 17 | # Workaround for not having the nonlocal statement in python 2.x 18 | old_value = [sys._sage_.namespace.get(var,None)] 19 | 20 | @interact(layout=[[(var,12)]], output=False) 21 | def f(self, x=(var,control)): 22 | if x is not old_value[0]: 23 | # avoid infinite recursion: if control is already set, 24 | # leave it alone 25 | sys._sage_.namespace[var]=x 26 | old_value[0] = x 27 | 28 | def g(var,y): 29 | f.x = y 30 | sys._sage_.namespace.on(var,'change', g) 31 | 32 | if var in sys._sage_.namespace: 33 | g(var, sys._sage_.namespace[var]) 34 | 35 | def dynamic(*args, **kwds): 36 | """ 37 | Make variables in the global namespace dynamically linked to a control from the 38 | interact label (see the documentation for interact). 39 | 40 | EXAMPLES: 41 | 42 | Make a control linked to a variable that doesn't yet exist:: 43 | 44 | dynamic('newname') 45 | 46 | Make a slider and a selector, linked to t and x:: 47 | 48 | dynamic(t=(1..10), x=[1,2,3,4]) 49 | t = 5 # this changes the control 50 | """ 51 | for var in args: 52 | if not isinstance(var, str): 53 | i = id(var) 54 | for k,v in sys._sage_.namespace.items(): 55 | if id(v) == i: 56 | _dynamic(k) 57 | return 58 | else: 59 | _dynamic(var) 60 | 61 | for var, control in kwds.items(): 62 | _dynamic(var, control) 63 | 64 | 65 | def dynamic_expression(v, vars): 66 | """ 67 | sage: t=5 68 | sage: dynamic(t) 69 | sage: dynamic_expression('2*t','t') 70 | """ 71 | # control 72 | @interact(output=False, readonly=True) 73 | def f(t=(0,2)): 74 | pass 75 | 76 | # update function 77 | def g(var,val): 78 | f.t = eval(v) 79 | 80 | for vv in vars: 81 | sys._sage_.namespace.on(vv,'change',g) 82 | 83 | imports = {"dynamic": dynamic, 84 | "dynamic_expression": dynamic_expression} 85 | -------------------------------------------------------------------------------- /fetch_vendor_js.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Script to download all required vendor javascript files. 3 | */ 4 | 5 | import fs from "fs"; 6 | import path from "path"; 7 | import fetch from "node-fetch"; 8 | import { fileURLToPath } from "url"; 9 | 10 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 11 | 12 | const TARGET_DIR = path.resolve(__dirname, "build/vendor"); 13 | 14 | const URLS = { 15 | "jquery.min.js": 16 | "https://code.jquery.com/jquery-3.7.1.min.js", 17 | "base/js/utils.js": 18 | "https://raw.githubusercontent.com/jupyter/nbclassic/master/nbclassic/static/base/js/utils.js", 19 | "base/js/namespace.js": 20 | "https://raw.githubusercontent.com/jupyter/nbclassic/master/nbclassic/static/base/js/namespace.js", 21 | "base/js/events.js": 22 | "https://raw.githubusercontent.com/jupyter/nbclassic/master/nbclassic/static/base/js/events.js", 23 | "services/kernels/kernel.js": 24 | "https://raw.githubusercontent.com/jupyter/nbclassic/master/nbclassic/static/services/kernels/kernel.js", 25 | "services/kernels/comm.js": 26 | "https://raw.githubusercontent.com/jupyter/nbclassic/master/nbclassic/static/services/kernels/comm.js", 27 | "services/kernels/serialize.js": 28 | "https://raw.githubusercontent.com/jupyter/nbclassic/master/nbclassic/static/services/kernels/serialize.js", 29 | "mpl.js": 30 | "https://raw.githubusercontent.com/matplotlib/matplotlib/main/lib/matplotlib/backends/web_backend/js/mpl.js", 31 | }; 32 | 33 | async function fetchFile(fileName, url) { 34 | const resp = await fetch(url); 35 | const body = await resp.text(); 36 | 37 | const fullPath = `${TARGET_DIR}/${fileName}`; 38 | const base = path.dirname(fullPath); 39 | fs.mkdirSync(base, { recursive: true }); 40 | 41 | const stream = fs.createWriteStream(fullPath); 42 | stream.once("open", function (fd) { 43 | stream.write(body); 44 | stream.end(); 45 | console.log(`Downloaded ${fileName}`); 46 | }); 47 | } 48 | 49 | // Ensure the target directory has been created 50 | fs.mkdirSync(TARGET_DIR, { recursive: true }); 51 | 52 | for (const [fileName, url] of Object.entries(URLS)) { 53 | fetchFile(fileName, url); 54 | } 55 | -------------------------------------------------------------------------------- /js/cell_body.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 12 |
13 | 14 | 15 |
16 |
17 | 32 |
33 |
34 | 45 |
46 |
47 |
48 |
49 |
50 | Messages 51 |

52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /js/console.js: -------------------------------------------------------------------------------- 1 | import sagecell from "./sagecell"; 2 | 3 | export const origConsole = window.console; 4 | 5 | /** 6 | * A replacement for window.console that suppresses logging based on `sagecell.quietMode` 7 | */ 8 | export const console = { 9 | log(...args) { 10 | if (sagecell.quietMode) { 11 | return; 12 | } 13 | origConsole.log(...args); 14 | }, 15 | info(...args) { 16 | if (sagecell.quietMode) { 17 | return; 18 | } 19 | origConsole.info(...args); 20 | }, 21 | debug(...args) { 22 | if (sagecell.quietMode) { 23 | return; 24 | } 25 | origConsole.debug(...args); 26 | }, 27 | error(...args) { 28 | origConsole.error(...args); 29 | }, 30 | warn(...args) { 31 | origConsole.warn(...args); 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /js/css.js: -------------------------------------------------------------------------------- 1 | /** Statically import CSS files and re-export them as a string */ 2 | 3 | import codemirror from "codemirror/lib/codemirror.css"; 4 | import fullscreen from "codemirror/addon/display/fullscreen.css"; 5 | import foldgutter from "codemirror/addon/fold/foldgutter.css"; 6 | import show_hint from "codemirror/addon/hint/show-hint.css"; 7 | import jquery from "jquery-ui-themes/themes/smoothness/jquery-ui.min.css"; 8 | 9 | import _colorpicker from "colorpicker.css"; 10 | // Fix colorpicker's relative paths 11 | const colorpicker = _colorpicker.replace(/url\(\.\./g, "url(colorpicker"); 12 | import fontawesome from "fontawesome.css"; 13 | import sagecell_css from "sagecell.css"; 14 | 15 | const css = `${codemirror}\n\n${fullscreen}\n\n${foldgutter}\n\n${show_hint}\n\n${jquery}\n\n${colorpicker}\n\n${fontawesome}\n\n${sagecell_css}`; 16 | 17 | export { css }; 18 | -------------------------------------------------------------------------------- /js/interact_controls.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import utils from "./utils"; 3 | 4 | var interact_control_throttle = 100; 5 | 6 | function InteractControl() { 7 | return function (session, control_id) { 8 | this.session = session; 9 | this.control_id = control_id; 10 | }; 11 | } 12 | 13 | /* 14 | // To implement a new control, do something like the following. 15 | // See below for examples. The Checkbox control is particularly simple. 16 | 17 | MyControl = InteractControl(); 18 | MyControl.prototype.create = function(data, block_id) { 19 | // The create message is in `data`, while the block id to use for `this.session.output` is in block_id. 20 | // This method creates the control and registers any change handlers. 21 | // Change handlers should send a `variable_update` message back to the server. This message is handled 22 | // by the control's python variable_update method. 23 | }; 24 | 25 | MyControl.prototype.update = function(namespace, variable, control_id) { 26 | // If a variable in the namespace is updated (i.e., the client receives a variable update message), 27 | // this method is called. The namespace is the UUID of the namespace, the variable is the variable name as a string, 28 | // and the control_id is the UUID of the control. This method should send a message and register a handler for the reply 29 | // from the control's python update_control method. The reply handler should then update the control appropriately. 30 | }; 31 | */ 32 | 33 | var Slider = InteractControl(); 34 | Slider.prototype.create = function (data, block_id) { 35 | var that = this; 36 | this.control = this.session.output( 37 | utils.createElement("div", { id: data.control_id }), 38 | block_id 39 | ); 40 | this.control.slider({ 41 | disabled: !data.enabled, 42 | min: data.min, 43 | max: data.max, 44 | step: data.step, 45 | slide: utils.throttle(function (event, ui) { 46 | if (!event.originalEvent) { 47 | return; 48 | } 49 | that.session.send_message( 50 | "variable_update", 51 | { control_id: data.control_id, value: ui.value }, 52 | { 53 | iopub: { 54 | output: $.proxy( 55 | that.session.handle_output, 56 | that.session 57 | ), 58 | }, 59 | } 60 | ); 61 | }, interact_control_throttle), 62 | }); 63 | }; 64 | 65 | Slider.prototype.update = function (namespace, variable, control_id) { 66 | var that = this; 67 | if (this.control_id !== control_id) { 68 | this.session.send_message( 69 | "control_update", 70 | { 71 | control_id: this.control_id, 72 | namespace: namespace, 73 | variable: variable, 74 | }, 75 | { 76 | iopub: { 77 | output: $.proxy(this.session.handle_output, this.session), 78 | }, 79 | shell: { 80 | control_update_reply: function (content, metadata) { 81 | if (content.status === "ok") { 82 | that.control.slider("value", content.result.value); 83 | } 84 | }, 85 | }, 86 | } 87 | ); 88 | } 89 | }; 90 | 91 | var ExpressionBox = InteractControl(); 92 | ExpressionBox.prototype.create = function (data, block_id) { 93 | var that = this; 94 | this.control = this.session.output( 95 | utils.createElement("input", { 96 | id: data.control_id, 97 | type: "textbox", 98 | }), 99 | block_id 100 | ); 101 | this.control.change(function (event) { 102 | if (!event.originalEvent) { 103 | return; 104 | } 105 | that.session.send_message( 106 | "variable_update", 107 | { control_id: data.control_id, value: $(this).val() }, 108 | { 109 | iopub: { 110 | output: $.proxy(that.session.handle_output, that.session), 111 | }, 112 | } 113 | ); 114 | }); 115 | }; 116 | 117 | ExpressionBox.prototype.update = function (namespace, variable, control_id) { 118 | var that = this; 119 | this.session.send_message( 120 | "control_update", 121 | { 122 | control_id: this.control_id, 123 | namespace: namespace, 124 | variable: variable, 125 | }, 126 | { 127 | iopub: { 128 | output: $.proxy(this.session.handle_output, this.session), 129 | }, 130 | shell: { 131 | control_update_reply: function (content, metadata) { 132 | if (content.status === "ok") { 133 | that.control.val(content.result.value); 134 | } 135 | }, 136 | }, 137 | } 138 | ); 139 | }; 140 | 141 | var Checkbox = InteractControl(); 142 | Checkbox.prototype.create = function (data, block_id) { 143 | var that = this; 144 | this.control = this.session.output( 145 | utils.createElement("input", { 146 | id: data.control_id, 147 | type: "checkbox", 148 | }), 149 | block_id 150 | ); 151 | this.control.change(function (event) { 152 | if (!event.originalEvent) { 153 | return; 154 | } 155 | that.session.send_message( 156 | "variable_update", 157 | { control_id: data.control_id, value: $(this).prop("checked") }, 158 | { 159 | iopub: { 160 | output: $.proxy(that.session.handle_output, that.session), 161 | }, 162 | } 163 | ); 164 | }); 165 | }; 166 | 167 | Checkbox.prototype.update = function (namespace, variable, control_id) { 168 | var that = this; 169 | this.session.send_message( 170 | "control_update", 171 | { 172 | control_id: this.control_id, 173 | namespace: namespace, 174 | variable: variable, 175 | }, 176 | { 177 | iopub: { 178 | output: $.proxy(this.session.handle_output, this.session), 179 | }, 180 | shell: { 181 | control_update_reply: function (content, metadata) { 182 | if (content.status === "ok") { 183 | that.control.prop("checked", content.result.value); 184 | } 185 | }, 186 | }, 187 | } 188 | ); 189 | }; 190 | 191 | var OutputRegion = InteractControl(); 192 | OutputRegion.prototype.create = function (data, block_id) { 193 | var that = this; 194 | this.control = this.session.output( 195 | utils.createElement("div", { id: data.control_id }), 196 | block_id 197 | ); 198 | this.session.output_blocks[this.control_id] = this.control; 199 | this.message_number = 1; 200 | }; 201 | 202 | OutputRegion.prototype.update = function (namespace, variable, control_id) { 203 | var that = this; 204 | this.message_number += 1; 205 | var msg_number = this.message_number; 206 | this.session.send_message( 207 | "control_update", 208 | { 209 | control_id: this.control_id, 210 | namespace: namespace, 211 | variable: variable, 212 | }, 213 | { 214 | iopub: { 215 | output: function (msg) { 216 | if (msg_number === that.message_number) { 217 | $.proxy(that.session.handle_output, that.session)( 218 | msg, 219 | that.control_id 220 | ); 221 | } 222 | }, 223 | }, 224 | } 225 | ); 226 | }; 227 | 228 | export default { 229 | Slider: Slider, 230 | ExpressionBox: ExpressionBox, 231 | Checkbox: Checkbox, 232 | OutputRegion: OutputRegion, 233 | }; 234 | -------------------------------------------------------------------------------- /js/jquery-global.js: -------------------------------------------------------------------------------- 1 | import _jquery from "jquery"; 2 | 3 | // Some users depend on jQuery being globally set by sage_cell. 4 | // We take care to initialize the jQuery global variable only if 5 | // another jQuery is not set. 6 | window.jQuery = window.jQuery || window.$ || _jquery; 7 | window.$ = window.$ || window.jQuery || _jquery; 8 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | import sagecell from "./sagecell"; 2 | import cell from "./cell"; 3 | import "./jquery-global"; 4 | 5 | import { console } from "./console"; 6 | 7 | (function () { 8 | var ga = document.createElement("script"); 9 | ga.type = "text/javascript"; 10 | ga.async = true; 11 | ga.src = 12 | ("https:" == document.location.protocol 13 | ? "https://ssl" 14 | : "http://www") + ".google-analytics.com/ga.js"; 15 | var s = document.getElementsByTagName("script")[0]; 16 | s.parentNode.insertBefore(ga, s); 17 | })(); 18 | 19 | /** 20 | * Creates a promise and hoists its `resolve` method so that 21 | * it can be called externally. 22 | */ 23 | function makeResolvablePromise() { 24 | const ret = { promise: null, resolve: null, state: "pending" }; 25 | ret.promise = new Promise((resolve) => { 26 | ret.resolve = (...args) => { 27 | ret.state = "fulfilled"; 28 | return resolve(...args); 29 | }; 30 | }); 31 | return ret; 32 | } 33 | 34 | // Set up the global sagecell variable. This needs to be done right away because other 35 | // scripts want to access window.sagecell. 36 | Object.assign(sagecell, { 37 | templates: { 38 | minimal: { 39 | // for an evaluate button and nothing else. 40 | editor: "textarea-readonly", 41 | hide: ["editor", "files", "permalink"], 42 | }, 43 | restricted: { 44 | // to display/evaluate code that can't be edited. 45 | editor: "codemirror-readonly", 46 | hide: ["files", "permalink"], 47 | }, 48 | }, 49 | allLanguages: [ 50 | "sage", 51 | "gap", 52 | "gp", 53 | "html", 54 | "macaulay2", 55 | "maxima", 56 | "octave", 57 | "python", 58 | "r", 59 | "singular", 60 | ], 61 | // makeSagecell must be available as soon as the script loads, 62 | // but we may not be ready to process data right away, so we 63 | // provide a wrapper that will poll until sagecell is ready. 64 | makeSagecell: function (args) { 65 | // Clients expect to receive a `cellInfo` object right away. 66 | // However, this object cannot be made available until the page loads. 67 | // If we're not ready, we return a stub object that gets updated with 68 | // the proper data when it becomes available. 69 | if (sagecell._initPromise.state === "pending") { 70 | const ret = {}; 71 | sagecell._initPromise.promise 72 | .then(() => { 73 | const cellInfo = window.sagecell._makeSagecell(args); 74 | Object.assign(ret, cellInfo); 75 | }) 76 | .catch((e) => { 77 | console.warn("Encountered error in makeSagecell", e); 78 | }); 79 | return ret; 80 | } else { 81 | return window.sagecell._makeSagecell(args); 82 | } 83 | }, 84 | _initPromise: makeResolvablePromise(), 85 | quietMode: false, 86 | }); 87 | 88 | // Purely for backwards compatibility 89 | window.singlecell = sagecell; 90 | window.singlecell.makeSinglecell = window.singlecell.makeSagecell; 91 | 92 | /** 93 | * Retrieve the kernel index associated with `key`. If 94 | * needed, this function will push `null` onto the kernel 95 | * stack, providing a space for the kernel to be initialized. 96 | */ 97 | function linkKeyToIndex(key) { 98 | sagecell.linkKeys = sagecell.linkKeys || {}; 99 | if (key in sagecell.linkKeys) { 100 | return sagecell.linkKeys[key]; 101 | } 102 | 103 | sagecell.kernels = sagecell.kernels || []; 104 | // Make sure we have a kernel to share for our new key. 105 | const index = sagecell.kernels.push(null) - 1; 106 | sagecell.linkKeys[key] = index; 107 | return index; 108 | } 109 | 110 | sagecell._makeSagecell = function (args) { 111 | console.info("sagecell.makeSagecell called"); 112 | // If `args.linkKey` is set, we force the `linked` option to be true. 113 | if (args.linkKey) { 114 | args = Object.assign({}, args, { linked: true }); 115 | } 116 | 117 | var cellInfo = {}; 118 | if (args.linked && args.linkKey) { 119 | cell.make(args, cellInfo, linkKeyToIndex(args.linkKey)); 120 | } else { 121 | cell.make(args, cellInfo); 122 | } 123 | console.info("sagecell.makeSagecell finished"); 124 | return cellInfo; 125 | }; 126 | sagecell.deleteSagecell = function (cellInfo) { 127 | cell.delete(cellInfo); 128 | }; 129 | sagecell.moveInputForm = function (cellInfo) { 130 | cell.moveInputForm(cellInfo); 131 | }; 132 | sagecell.restoreInputForm = function (cellInfo) { 133 | cell.restoreInputForm(cellInfo); 134 | }; 135 | 136 | sagecell._initPromise.resolve(); 137 | 138 | export default sagecell; 139 | export { sagecell }; 140 | -------------------------------------------------------------------------------- /js/multisockjs.js: -------------------------------------------------------------------------------- 1 | import { URLs } from "./urls"; 2 | import SockJS from "sockjs-client"; 3 | import utils from "./utils"; 4 | import { console } from "./console"; 5 | 6 | export function MultiSockJS(url, prefix) { 7 | console.debug( 8 | "Starting sockjs connection to " + url + " with prefix " + prefix 9 | ); 10 | if ( 11 | !MultiSockJS.sockjs || 12 | MultiSockJS.sockjs.readyState === SockJS.CLOSING || 13 | MultiSockJS.sockjs.readyState === SockJS.CLOSED 14 | ) { 15 | MultiSockJS.channels = {}; 16 | MultiSockJS.to_init = []; 17 | console.debug("Initializing MultiSockJS to " + URLs.sockjs); 18 | MultiSockJS.sockjs = new SockJS( 19 | URLs.sockjs + "?CellSessionID=" + utils.cellSessionID() 20 | ); 21 | 22 | MultiSockJS.sockjs.onopen = function (e) { 23 | while (MultiSockJS.to_init.length > 0) { 24 | MultiSockJS.to_init.shift().init_socket(e); 25 | } 26 | }; 27 | 28 | MultiSockJS.sockjs.onmessage = function (e) { 29 | var i = e.data.indexOf(","); 30 | var prefix = e.data.substring(0, i); 31 | console.debug("MultiSockJS.sockjs.onmessage prefix: " + prefix); 32 | e.data = e.data.substring(i + 1); 33 | console.debug("other data: " + e.data); 34 | if ( 35 | MultiSockJS.channels[prefix] && 36 | MultiSockJS.channels[prefix].onmessage 37 | ) { 38 | MultiSockJS.channels[prefix].onmessage(e); 39 | } 40 | }; 41 | 42 | MultiSockJS.sockjs.onclose = function (e) { 43 | var readyState = MultiSockJS.sockjs.readyState; 44 | for (var prefix in MultiSockJS.channels) { 45 | MultiSockJS.channels[prefix].readyState = readyState; 46 | if (MultiSockJS.channels[prefix].onclose) { 47 | MultiSockJS.channels[prefix].onclose(e); 48 | } 49 | } 50 | // Maybe we should just remove the sockjs object from MultiSockJS now 51 | }; 52 | } 53 | this.prefix = url 54 | ? url.match(/^\w+:\/\/.*?\/kernel\/(.*\/channels).*$/)[1] 55 | : prefix; 56 | console.debug("this.prefix: " + this.prefix); 57 | this.readyState = MultiSockJS.sockjs.readyState; 58 | MultiSockJS.channels[this.prefix] = this; 59 | this.init_socket(); 60 | } 61 | 62 | MultiSockJS.prototype.init_socket = function (e) { 63 | if (MultiSockJS.sockjs.readyState) { 64 | var that = this; 65 | // Run the onopen function after the current thread has finished, 66 | // so that onopen has a chance to be set. 67 | setTimeout(function () { 68 | that.readyState = MultiSockJS.sockjs.readyState; 69 | if (that.onopen) { 70 | that.onopen(e); 71 | } 72 | }, 0); 73 | } else { 74 | MultiSockJS.to_init.push(this); 75 | } 76 | }; 77 | 78 | MultiSockJS.prototype.send = function (msg) { 79 | MultiSockJS.sockjs.send(this.prefix + "," + msg); 80 | }; 81 | 82 | MultiSockJS.prototype.close = function () { 83 | delete MultiSockJS.channels[this.prefix]; 84 | }; 85 | 86 | export default MultiSockJS; 87 | -------------------------------------------------------------------------------- /js/sagecell.js: -------------------------------------------------------------------------------- 1 | const _sagecell = window.sagecell || {}; 2 | window.sagecell = _sagecell; 3 | 4 | export default _sagecell; 5 | -------------------------------------------------------------------------------- /js/urls.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import sagecell from "./sagecell"; 3 | 4 | export const URLs = {}; 5 | 6 | /** 7 | * Initialize the important URLs. The root URL derived from one of 8 | * the following locations: 9 | * 1. the variable sagecell.root 10 | * 2. a tag of the form 11 | * 3. the root of the URL of the executing script 12 | */ 13 | export function initializeURLs() { 14 | var root; 15 | var el; 16 | if (sagecell.root) { 17 | root = sagecell.root; 18 | } else if ((el = $("link[property=sagecell-root]")).length > 0) { 19 | root = el.last().attr("href"); 20 | } else { 21 | /* get the first part of the last script element's src that loaded something called 'embedded_sagecell.js' 22 | also, strip off the static/ part of the url if the src looked like 'static/embedded_sagecell.js' 23 | modified from MathJax source 24 | We could use the jquery reverse plugin at http://www.mail-archive.com/discuss@jquery.com/msg04272.html 25 | and the jquery .each() to get this as well, but this approach avoids creating a reversed list, etc. */ 26 | var scripts = ( 27 | document.documentElement || document 28 | ).getElementsByTagName("script"); 29 | var namePattern = /^.*?(?=(?:static\/)?embedded_sagecell.js)/; 30 | for (var i = scripts.length - 1; i >= 0; i--) { 31 | var m = (scripts[i].src || "").match(namePattern); 32 | if (m) { 33 | root = m[0]; 34 | break; 35 | } 36 | } 37 | if (!root || root === "/") { 38 | root = window.location.protocol + "//" + window.location.host + "/"; 39 | } 40 | } 41 | if (root.slice(-1) !== "/") { 42 | root += "/"; 43 | } 44 | if (root === "http://sagecell.sagemath.org/") { 45 | root = "https://sagecell.sagemath.org/"; 46 | } 47 | 48 | Object.assign(URLs, { 49 | cell: root + "sagecell.html", 50 | completion: root + "complete", 51 | help: root + "help.html", 52 | kernel: root + "kernel", 53 | permalink: root + "permalink", 54 | root: root, 55 | sockjs: root + "sockjs", 56 | spinner: root + "static/spinner.gif", 57 | terms: root + "tos.html", 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /js/widgets.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import utils from "./utils"; 3 | 4 | // Creates global namespace 5 | import "mpl"; 6 | import { console } from "./console"; 7 | 8 | export const widgets = { 9 | Graphics: function (session) { 10 | return function (comm, msg) { 11 | var callbacks = { 12 | iopub: { output: $.proxy(session.handle_output, session) }, 13 | }; 14 | var filename = msg.content.data.filename; 15 | var filepath = session.kernel.kernel_url + "/files/"; 16 | var img = utils.createElement("img", { 17 | src: filepath + filename, 18 | }); 19 | var block_id = msg.metadata.interact_id || null; 20 | 21 | session.output(img, block_id); 22 | // Handle clicks inside the image 23 | $(img).click(function (e) { 24 | var offset = $(this).offset(); 25 | var x = (e.pageX - offset.left) / img.clientWidth; 26 | var y = (e.pageY - offset.top) / img.clientHeight; 27 | comm.send({ x: x, y: y, eventType: "click" }, callbacks); 28 | }); 29 | // Handle mousemove inside the image 30 | $(img).mousemove(function (e) { 31 | var offset = $(this).offset(); 32 | var x = (e.pageX - offset.left) / img.clientWidth; 33 | var y = (e.pageY - offset.top) / img.clientHeight; 34 | comm.send({ x: x, y: y, eventType: "mousemove" }, callbacks); 35 | }); 36 | 37 | // For messages from Python to javascript; we don't use this in this example 38 | //comm.on_msg(function(msg) {console.log(msg)}); 39 | }; 40 | }, 41 | ThreeJS: function (session) { 42 | return function (comm, msg) { 43 | var that = this; 44 | var callbacks = { 45 | iopub: { output: $.proxy(session.handle_output, session) }, 46 | }; 47 | var div = utils.createElement("div", { 48 | style: "border: 2px solid blue;margin:0;padding:0;", 49 | }); 50 | var block_id = msg.metadata.interact_id || null; 51 | 52 | $(div).salvus_threejs(msg.content.data); 53 | 54 | that.obj = utils.proxy([ 55 | "add_3dgraphics_obj", 56 | "render_scene", 57 | "set_frame", 58 | "animate", 59 | ]); 60 | run_when_defined({ 61 | fn: function () { 62 | return $(div).data("salvus-threejs"); 63 | }, 64 | cb: function (result) { 65 | that.obj._run_callbacks(result); 66 | that.obj = result; 67 | }, 68 | err: function (err) { 69 | comm.close(); 70 | console.log(err); 71 | }, 72 | }); 73 | 74 | session.output(div, block_id); 75 | 76 | comm.on_msg(function (msg) { 77 | var data = msg.content.data; 78 | var type = data.msg_type; 79 | delete data.msg_type; 80 | if (type === "add") { 81 | that.obj.add_3dgraphics_obj(data); 82 | } else if (type === "render") { 83 | that.obj.render_scene(data); 84 | } else if (type === "set_frame") { 85 | that.obj.set_frame(data); 86 | } else if (type === "animate") { 87 | that.obj.animate(data); 88 | } else if (type === "lights") { 89 | that.obj.add_lights(data); 90 | } 91 | }); 92 | }; 93 | }, 94 | MPL: function (session) { 95 | var callbacks = { 96 | iopub: { output: $.proxy(session.handle_output, session) }, 97 | }; 98 | var comm_websocket = function (comm) { 99 | var ws = {}; 100 | // MPL assumes we have a websocket that is not open yet 101 | // so we run the onopen handler after they have a chance 102 | // to set it. 103 | ws.onopen = function () {}; 104 | setTimeout(ws.onopen(), 0); 105 | ws.close = function () { 106 | comm.close(); 107 | }; 108 | ws.send = function (m) { 109 | comm.send(m, callbacks); 110 | console.log("sending", m); 111 | }; 112 | comm.on_msg(function (msg) { 113 | console.log("receiving", msg); 114 | ws.onmessage(msg["content"]["data"]); 115 | }); 116 | return ws; 117 | }; 118 | return function (comm, msg) { 119 | var id = msg.content.data.id; 120 | var div = utils.createElement("div", { 121 | style: "border: 2px solid blue;margin:0;padding:0;", 122 | }); 123 | var block_id = msg.metadata.interact_id || null; 124 | session.output(div, block_id); 125 | var c = comm_websocket(comm); 126 | var m = new mpl.figure( 127 | id, 128 | c, 129 | function () { 130 | console.log("download"); 131 | }, 132 | div 133 | ); 134 | }; 135 | }, 136 | }; 137 | 138 | export default widgets; 139 | -------------------------------------------------------------------------------- /log.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from logging.handlers import SysLogHandler 4 | import sys 5 | 6 | 7 | LOG_LEVEL = logging.DEBUG 8 | LOG_VERSION = 0 9 | 10 | 11 | class StatsMessage(object): 12 | def __init__(self, kernel_id, code, execute_type, remote_ip, referer): 13 | self.msg = [LOG_VERSION, remote_ip, referer, execute_type, kernel_id, code] 14 | def __str__(self): 15 | return json.dumps(self.msg) 16 | 17 | 18 | syslog = SysLogHandler(address="/dev/log", facility=SysLogHandler.LOG_LOCAL3) 19 | syslog.setFormatter(logging.Formatter( 20 | "%(asctime)s %(process)5d %(name)-28s %(message)s")) 21 | 22 | # Default logger for SageCell 23 | logger = logging.getLogger("sagecell") 24 | permalink_logger = logger.getChild("permalink") 25 | stats_logger = logger.getChild("stats") 26 | # Intermediate loggers to be parents for actual receivers and kernels. 27 | kernel_logger = logger.getChild("kernel") 28 | provider_logger = logger.getChild("provider") 29 | 30 | root = logging.getLogger() 31 | root.addHandler(syslog) 32 | root.setLevel(LOG_LEVEL) 33 | 34 | class TornadoFilter(logging.Filter): 35 | """ 36 | Drop HA-Proxy healthchecks. 37 | """ 38 | def filter(self, record): 39 | return len(record.args) != 3 or \ 40 | record.args[:2] != (200, 'OPTIONS / (10.0.3.1)') 41 | 42 | logging.getLogger("tornado.access").addFilter(TornadoFilter()) 43 | 44 | 45 | class StdLog(object): 46 | """ 47 | A file-like object for sending stdout/stderr to a log. 48 | """ 49 | def __init__(self, logger, level): 50 | self.logger = logger 51 | self.level = level 52 | 53 | def fileno(self): 54 | return 1 55 | 56 | def flush(self): 57 | pass 58 | 59 | def write(self, data): 60 | self.logger.log(self.level, data) 61 | 62 | 63 | def std_redirect(logger): 64 | """ 65 | Redirect stdout and stderr to the given logger. 66 | 67 | Also set their underscore versions to make IPython happier. 68 | """ 69 | sys.__stdout__ = sys.stdout = StdLog( 70 | logger.getChild("stdout"), logging.DEBUG) 71 | sys.__stderr__ = sys.stderr = StdLog( 72 | logger.getChild("stderr"), logging.WARNING) 73 | -------------------------------------------------------------------------------- /misc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Misc functions / classes 3 | """ 4 | from binascii import b2a_base64 5 | from contextlib import contextmanager 6 | from datetime import datetime 7 | import os 8 | import shutil 9 | import stat 10 | import sys 11 | 12 | 13 | class Config(object): 14 | """ 15 | Config file wrapper / handler class 16 | 17 | This is designed to make loading and working with an 18 | importable configuration file with options relevant to 19 | multiple classes more convenient. 20 | 21 | Rather than re-importing a configuration module whenever 22 | a specific is attribute is needed, a Config object can 23 | be instantiated by some base application and the relevant 24 | attributes can be passed to whatever classes / functions 25 | are needed. 26 | 27 | This class tracks both the default and user-specified 28 | configuration files 29 | """ 30 | def __init__(self): 31 | import config_default 32 | 33 | self.config = None 34 | self.config_default = config_default 35 | 36 | try: 37 | import config 38 | self.config = config 39 | except ImportError: 40 | pass 41 | 42 | def get(self, attr): 43 | """ 44 | Get a config attribute. If the attribute is defined 45 | in the user-specified file, that is used, otherwise 46 | the default config file attribute is used if 47 | possible. If the attribute is a dictionary, the items 48 | in config and default_config will be merged. 49 | 50 | :arg attr str: the name of the attribute to get 51 | :returns: the value of the named attribute, or 52 | None if the attribute does not exist. 53 | """ 54 | result = self.get_default(attr) 55 | if self.config is not None: 56 | try: 57 | val = getattr(self.config, attr) 58 | if isinstance(val, dict): 59 | result.update(val) 60 | else: 61 | result = val 62 | except AttributeError: 63 | pass 64 | return result 65 | 66 | def get_default(self, attr): 67 | """ 68 | Get a config attribute from the default config file. 69 | 70 | :arg attr str: the name of the attribute toget 71 | :returns: the value of the named attribute, or 72 | None if the attribute does not exist. 73 | """ 74 | config_val = None 75 | 76 | try: 77 | config_val = getattr(self.config_default, attr) 78 | except AttributeError: 79 | pass 80 | 81 | return config_val 82 | 83 | def set(self, attr, value): 84 | """ 85 | Set a config attribute 86 | 87 | :arg attr str: the name of the attribute to set 88 | :arg value: an arbitrary value to set the named 89 | attribute to 90 | """ 91 | setattr(self.config, attr, value) 92 | 93 | def get_attrs(self): 94 | """ 95 | Get a list of all the config object's attributes 96 | 97 | This isn't very useful right now, since it includes 98 | ____ attributes and the like. 99 | 100 | :returns: a list of all attributes belonging to 101 | the imported config module. 102 | :rtype: list 103 | """ 104 | return dir(self.config) 105 | 106 | 107 | @contextmanager 108 | def session_metadata(metadata): 109 | # flush any messages waiting in buffers 110 | sys.stdout.flush() 111 | sys.stderr.flush() 112 | 113 | session = sys.stdout.session 114 | old_metadata = session.metadata 115 | new_metadata = old_metadata.copy() 116 | new_metadata.update(metadata) 117 | session.metadata = new_metadata 118 | yield 119 | sys.stdout.flush() 120 | sys.stderr.flush() 121 | session.metadata = old_metadata 122 | 123 | def display_file(path, mimetype=None): 124 | path = os.path.relpath(path) 125 | if path.startswith("../"): 126 | shutil.copy(path, ".") 127 | path = os.path.basename(path) 128 | os.chmod(path, stat.S_IMODE(os.stat(path).st_mode) | stat.S_IRGRP) 129 | if mimetype is None: 130 | mimetype = 'application/x-file' 131 | mt = os.path.getmtime(path) 132 | display_message({ 133 | 'text/plain': '%s file' % mimetype, 134 | mimetype: path + '?m=%s' % mt}) 135 | sys._sage_.sent_files[path] = mt 136 | 137 | def display_html(s): 138 | display_message({'text/plain': 'html', 'text/html': s}) 139 | 140 | def display_message(data, metadata=None): 141 | sys.stdout.session.send(sys.stdout.pub_thread, 142 | 'display_data', 143 | content={'data': data, 'source': 'sagecell'}, 144 | parent=sys.stdout.parent_header, 145 | metadata=metadata) 146 | 147 | def stream_message(stream, data, metadata=None): 148 | sys.stdout.session.send(sys.stdout.pub_thread, 149 | 'stream', 150 | content={'name': stream, 'data': data}, 151 | parent=sys.stdout.parent_header, 152 | metadata=metadata) 153 | 154 | def reset_kernel_timeout(timeout): 155 | sys.stdout.session.send(sys.stdout.pub_thread, 156 | 'kernel_timeout', 157 | content={'timeout': float(timeout)}, 158 | parent=sys.stdout.parent_header) 159 | 160 | def javascript(code): 161 | sys._sage_.display_message({'application/javascript': code, 'text/plain': 'javascript code'}) 162 | 163 | 164 | def sage_json(obj): 165 | # Similar to json_default in jupyter_client/jsonutil.py 166 | import sage.all 167 | if isinstance(obj, datetime): 168 | return obj.isoformat() 169 | if isinstance(obj, sage.rings.integer.Integer): 170 | return int(obj) 171 | if isinstance(obj, ( 172 | sage.rings.real_mpfr.RealLiteral, 173 | sage.rings.real_mpfr.RealNumber, 174 | sage.rings.real_double.RealDoubleElement)): 175 | return float(obj) 176 | if isinstance(obj, bytes): 177 | return b2a_base64(obj).decode('ascii') 178 | raise TypeError( 179 | "Object of type %s with value of %s is not JSON serializable" 180 | % (type(obj), repr(obj))) 181 | -------------------------------------------------------------------------------- /namespace.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | 4 | class InstrumentedNamespace(dict): 5 | def __init__(self, *args, **kwargs): 6 | """ 7 | Set up a namespace id 8 | """ 9 | dict.__init__(self,*args,**kwargs) 10 | self.events = defaultdict(lambda: defaultdict(list)) 11 | 12 | def on(self, key, event, f): 13 | self.events[key][event].append(f) 14 | 15 | def off(self, key, event=None, f=None): 16 | if event is None: 17 | self.events.pop(key, None) 18 | elif f is None: 19 | self.events[key].pop(event, None) 20 | else: 21 | self.events[key][event].remove(f) 22 | 23 | def trigger(self, key, event, *args, **kwargs): 24 | if key in self.events and event in self.events[key]: 25 | for f in self.events[key][event]: 26 | f(key, *args, **kwargs) 27 | 28 | def __setitem__(self, key, value): 29 | """ 30 | Set a value in the dictionary and run attached notification functions. 31 | """ 32 | if key not in self: 33 | self.trigger(key, 'initialize', value) 34 | dict.__setitem__(self, key, value) 35 | self.trigger(key, 'change', value) 36 | 37 | def __delitem__(self, key): 38 | dict.__delitem__(self, key) 39 | self.off(key) 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "embedded_sagecell.js", 3 | "version": "1.0.0", 4 | "description": "Sage cell that can be embedded into a webpage", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build:copystatic": "mkdir -p build/vendor && cp static/sagecell.css static/fontawesome.css static/colorpicker/js/colorpicker.js static/colorpicker/css/colorpicker.css build/vendor", 9 | "build:deps": "npm run build:copystatic && node fetch_vendor_js.mjs", 10 | "build": "webpack --mode production", 11 | "watch": "webpack --watch --mode development" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/sagemath/" 16 | }, 17 | "keywords": [ 18 | "sage" 19 | ], 20 | "author": "Jason Grout, Andrey Novoseltsev, Ira Hanson, Alex Kramer", 21 | "license": "SEE LICENSE IN LICENSE.txt", 22 | "dependencies": { 23 | "codemirror": "^5.65.1", 24 | "domready": "^1.0.8", 25 | "es6-promise": "^4.2.8", 26 | "jquery": "^3.6.0", 27 | "jquery-ui": "^1.13.2", 28 | "jquery-ui-themes": "^1.12.0", 29 | "jsmol": "^1.0.0", 30 | "moment": "^2.29.4", 31 | "node-fetch": "^3.2.10", 32 | "sockjs": "^0.3.24", 33 | "sockjs-client": "^1.5.2", 34 | "source-map-loader": "^3.0.1", 35 | "ts-loader": "^9.2.6", 36 | "underscore": "^1.13.2", 37 | "webpack-jquery-ui": "^2.0.1" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.17.2", 41 | "@babel/plugin-transform-modules-amd": "^7.16.7", 42 | "@babel/preset-env": "^7.16.11", 43 | "babel-loader": "^8.2.3", 44 | "eslint": "^8.9.0", 45 | "raw-loader": "^4.0.2", 46 | "webpack": "^5.94.0", 47 | "webpack-bundle-analyzer": "^4.5.0", 48 | "webpack-cli": "^4.9.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /permalink.py: -------------------------------------------------------------------------------- 1 | """ 2 | Permalink web server 3 | 4 | This Tornado server provides a permalink service with a convenient 5 | post/get api for storing and retrieving code. 6 | """ 7 | 8 | import base64 9 | import json 10 | import zlib 11 | 12 | import tornado 13 | 14 | from log import permalink_logger as logger 15 | 16 | 17 | class PermalinkHandler(tornado.web.RequestHandler): 18 | """ 19 | Permalink generation request handler. 20 | 21 | This accepts the code and language strings and stores 22 | these in the permalink database. A zip and query string are returned. 23 | 24 | The specified id can be used to generate permalinks 25 | with the format ``?q=``. 26 | """ 27 | 28 | async def post(self): 29 | def encode(s): 30 | return base64.urlsafe_b64encode( 31 | zlib.compress(s.encode("utf8"))).decode("utf8") 32 | 33 | args = self.request.arguments 34 | logger.debug("Storing permalink %s", args) 35 | code = self.get_argument("code") 36 | language = self.get_argument("language", "sage") 37 | interacts = self.get_argument("interacts", "[]") 38 | retval = {} 39 | retval["zip"] = encode(code) 40 | retval["query"] = await self.application.db.add( 41 | code, language, interacts) 42 | retval["interacts"] = encode(interacts) 43 | if "n" in args: 44 | retval["n"] = int(self.get_argument("n")) 45 | if "frame" in args: 46 | retval = ('' 47 | % json.dumps(retval)) 48 | self.set_header("Content-Type", "text/html") 49 | else: 50 | self.set_header("Access-Control-Allow-Origin", 51 | self.request.headers.get("Origin", "*")) 52 | self.set_header("Access-Control-Allow-Credentials", "true") 53 | self.write(retval) 54 | self.finish() 55 | 56 | async def get(self): 57 | q = self.get_argument("q") 58 | try: 59 | logger.debug("Looking up permalink %s", q) 60 | response = await self.application.db.get(q) 61 | except LookupError: 62 | logger.warning("ID not found in permalink database %s", q) 63 | self.set_status(404) 64 | self.finish("ID not found in permalink database") 65 | return 66 | response = json.dumps(response) 67 | if self.get_arguments("callback"): 68 | self.write("%s(%r);" % (self.get_argument("callback"), response)) 69 | self.set_header("Content-Type", "application/javascript") 70 | else: 71 | self.write(response) 72 | self.set_header("Access-Control-Allow-Origin", 73 | self.request.headers.get("Origin", "*")) 74 | self.set_header("Access-Control-Allow-Credentials", "true") 75 | self.set_header("Content-Type", "application/json") 76 | self.finish() 77 | -------------------------------------------------------------------------------- /permalink_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Permalink web server 3 | 4 | This Tornado server provides a permalink service with a convenient 5 | post/get api for storing and retrieving code. 6 | """ 7 | 8 | import os 9 | import signal 10 | 11 | import psutil 12 | import tornado.httpserver 13 | import tornado.ioloop 14 | import tornado.web 15 | 16 | import permalink 17 | from log import permalink_logger as logger 18 | 19 | 20 | PERMALINK_DB = "sqlalchemy" 21 | PERMALINK_URI = "sqlite:///permalinks.db" 22 | PERMALINK_PID_FILE = "permalink.pid" 23 | 24 | 25 | class PermalinkServer(tornado.web.Application): 26 | def __init__(self): 27 | handlers_list = [ 28 | (r"/", permalink.PermalinkHandler), 29 | (r"/permalink", permalink.PermalinkHandler), 30 | ] 31 | db = __import__('db_' + PERMALINK_DB) 32 | self.db = db.DB(PERMALINK_URI) 33 | 34 | #self.ioloop = ioloop.IOLoop.instance() 35 | # to check for blocking when debugging, uncomment the following 36 | # and set the argument to the blocking timeout in seconds 37 | #self.ioloop.set_blocking_log_threshold(.5) 38 | 39 | super(PermalinkServer, self).__init__(handlers_list) 40 | 41 | if __name__ == "__main__": 42 | import argparse 43 | parser = argparse.ArgumentParser( 44 | description='Launch a permalink database web server', 45 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 46 | parser.add_argument('-p', '--port', type=int, default=8080, 47 | help='port to launch the server') 48 | args = parser.parse_args() 49 | 50 | from lockfile.pidlockfile import PIDLockFile 51 | pidfile_path = PERMALINK_PID_FILE 52 | pidlock = PIDLockFile(pidfile_path) 53 | if pidlock.is_locked(): 54 | old_pid = pidlock.read_pid() 55 | if os.getpid() != old_pid: 56 | try: 57 | old = psutil.Process(old_pid) 58 | if os.path.basename(__file__) in old.cmdline(): 59 | try: 60 | old.terminate() 61 | try: 62 | old.wait(10) 63 | except psutil.TimeoutExpired: 64 | old.kill() 65 | except psutil.AccessDenied: 66 | pass 67 | except psutil.NoSuchProcess: 68 | pass 69 | pidlock.break_lock() 70 | 71 | pidlock.acquire(timeout=10) 72 | app = PermalinkServer() 73 | app.listen(port=args.port, xheaders=True) 74 | 75 | def handler(signum, frame): 76 | logger.info("Received %s, shutting down...", signum) 77 | ioloop = tornado.ioloop.IOLoop.current() 78 | ioloop.add_callback_from_signal(ioloop.stop) 79 | 80 | signal.signal(signal.SIGHUP, handler) 81 | signal.signal(signal.SIGINT, handler) 82 | signal.signal(signal.SIGTERM, handler) 83 | 84 | try: 85 | from systemd.daemon import notify 86 | notify('READY=1\nMAINPID={}'.format(os.getpid()), True) 87 | except ImportError: 88 | pass 89 | 90 | tornado.ioloop.IOLoop.current().start() 91 | pidlock.release() 92 | -------------------------------------------------------------------------------- /static/cocalc-logo-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/cocalc-logo-horizontal.png -------------------------------------------------------------------------------- /static/colorpicker/css/colorpicker.css: -------------------------------------------------------------------------------- 1 | .colorpicker { 2 | width: 356px; 3 | height: 176px; 4 | overflow: hidden; 5 | position: absolute; 6 | background: url(../images/colorpicker_background.png); 7 | font-family: Arial, Helvetica, sans-serif; 8 | display: none; 9 | } 10 | .colorpicker_color { 11 | width: 150px; 12 | height: 150px; 13 | left: 14px; 14 | top: 13px; 15 | position: absolute; 16 | background: #f00; 17 | overflow: hidden; 18 | cursor: crosshair; 19 | } 20 | .colorpicker_color div { 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | width: 150px; 25 | height: 150px; 26 | background: url(../images/colorpicker_overlay.png); 27 | } 28 | .colorpicker_color div div { 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | width: 11px; 33 | height: 11px; 34 | overflow: hidden; 35 | background: url(../images/colorpicker_select.gif); 36 | margin: -5px 0 0 -5px; 37 | } 38 | .colorpicker_hue { 39 | position: absolute; 40 | top: 13px; 41 | left: 171px; 42 | width: 35px; 43 | height: 150px; 44 | cursor: n-resize; 45 | } 46 | .colorpicker_hue div { 47 | position: absolute; 48 | width: 35px; 49 | height: 9px; 50 | overflow: hidden; 51 | background: url(../images/colorpicker_indic.gif) left top; 52 | margin: -4px 0 0 0; 53 | left: 0px; 54 | } 55 | .colorpicker_new_color { 56 | position: absolute; 57 | width: 60px; 58 | height: 30px; 59 | left: 213px; 60 | top: 13px; 61 | background: #f00; 62 | } 63 | .colorpicker_current_color { 64 | position: absolute; 65 | width: 60px; 66 | height: 30px; 67 | left: 283px; 68 | top: 13px; 69 | background: #f00; 70 | } 71 | .colorpicker input { 72 | background-color: transparent; 73 | border: 1px solid transparent; 74 | position: absolute; 75 | font-size: 10px; 76 | font-family: Arial, Helvetica, sans-serif; 77 | color: #898989; 78 | top: 4px; 79 | right: 11px; 80 | text-align: right; 81 | margin: 0; 82 | padding: 0; 83 | height: 11px; 84 | } 85 | .colorpicker_hex { 86 | position: absolute; 87 | width: 72px; 88 | height: 22px; 89 | background: url(../images/colorpicker_hex.png) top; 90 | left: 212px; 91 | top: 142px; 92 | } 93 | .colorpicker_hex input { 94 | right: 6px; 95 | } 96 | .colorpicker_field { 97 | height: 22px; 98 | width: 62px; 99 | background-position: top; 100 | position: absolute; 101 | } 102 | .colorpicker_field span { 103 | position: absolute; 104 | width: 12px; 105 | height: 22px; 106 | overflow: hidden; 107 | top: 0; 108 | right: 0; 109 | cursor: n-resize; 110 | } 111 | .colorpicker_rgb_r { 112 | background-image: url(../images/colorpicker_rgb_r.png); 113 | top: 52px; 114 | left: 212px; 115 | } 116 | .colorpicker_rgb_g { 117 | background-image: url(../images/colorpicker_rgb_g.png); 118 | top: 82px; 119 | left: 212px; 120 | } 121 | .colorpicker_rgb_b { 122 | background-image: url(../images/colorpicker_rgb_b.png); 123 | top: 112px; 124 | left: 212px; 125 | } 126 | .colorpicker_hsb_h { 127 | background-image: url(../images/colorpicker_hsb_h.png); 128 | top: 52px; 129 | left: 282px; 130 | } 131 | .colorpicker_hsb_s { 132 | background-image: url(../images/colorpicker_hsb_s.png); 133 | top: 82px; 134 | left: 282px; 135 | } 136 | .colorpicker_hsb_b { 137 | background-image: url(../images/colorpicker_hsb_b.png); 138 | top: 112px; 139 | left: 282px; 140 | } 141 | .colorpicker_submit { 142 | position: absolute; 143 | width: 22px; 144 | height: 22px; 145 | background: url(../images/colorpicker_submit.png) top; 146 | left: 322px; 147 | top: 142px; 148 | overflow: hidden; 149 | } 150 | .colorpicker_focus { 151 | background-position: center; 152 | } 153 | .colorpicker_hex.colorpicker_focus { 154 | background-position: bottom; 155 | } 156 | .colorpicker_submit.colorpicker_focus { 157 | background-position: bottom; 158 | } 159 | .colorpicker_slider { 160 | background-position: bottom; 161 | } 162 | -------------------------------------------------------------------------------- /static/colorpicker/css/layout.css: -------------------------------------------------------------------------------- 1 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td { 2 | margin:0; 3 | padding:0; 4 | } 5 | table { 6 | border-collapse:collapse; 7 | border-spacing:0; 8 | } 9 | fieldset,img { 10 | border:0; 11 | } 12 | address,caption,cite,code,dfn,em,strong,th,var { 13 | font-style:normal; 14 | font-weight:normal; 15 | } 16 | ol,ul { 17 | list-style:none; 18 | } 19 | caption,th { 20 | text-align:left; 21 | } 22 | h1,h2,h3,h4,h5,h6 { 23 | font-size:100%; 24 | font-weight:normal; 25 | } 26 | q:before,q:after { 27 | content:''; 28 | } 29 | abbr,acronym { border:0; 30 | } 31 | html, body { 32 | background-color: #fff; 33 | font-family: Arial, Helvetica, sans-serif; 34 | font-size: 12px; 35 | line-height: 18px; 36 | color: #52697E; 37 | } 38 | body { 39 | text-align: center; 40 | overflow: auto; 41 | } 42 | .wrapper { 43 | width: 700px; 44 | margin: 0 auto; 45 | text-align: left; 46 | } 47 | h1 { 48 | font-size: 21px; 49 | height: 47px; 50 | line-height: 47px; 51 | text-transform: uppercase; 52 | } 53 | .navigationTabs { 54 | height: 23px; 55 | line-height: 23px; 56 | border-bottom: 1px solid #ccc; 57 | } 58 | .navigationTabs li { 59 | float: left; 60 | height: 23px; 61 | line-height: 23px; 62 | padding-right: 3px; 63 | } 64 | .navigationTabs li a{ 65 | float: left; 66 | dispaly: block; 67 | height: 23px; 68 | line-height: 23px; 69 | padding: 0 10px; 70 | overflow: hidden; 71 | color: #52697E; 72 | background-color: #eee; 73 | position: relative; 74 | text-decoration: none; 75 | } 76 | .navigationTabs li a:hover { 77 | background-color: #f0f0f0; 78 | } 79 | .navigationTabs li a.active { 80 | background-color: #fff; 81 | border: 1px solid #ccc; 82 | border-bottom: 0px solid; 83 | } 84 | .tabsContent { 85 | border: 1px solid #ccc; 86 | border-top: 0px solid; 87 | width: 698px; 88 | overflow: hidden; 89 | } 90 | .tab { 91 | padding: 16px; 92 | display: none; 93 | } 94 | .tab h2 { 95 | font-weight: bold; 96 | font-size: 16px; 97 | } 98 | .tab h3 { 99 | font-weight: bold; 100 | font-size: 14px; 101 | margin-top: 20px; 102 | } 103 | .tab p { 104 | margin-top: 16px; 105 | clear: both; 106 | } 107 | .tab ul { 108 | margin-top: 16px; 109 | list-style: disc; 110 | } 111 | .tab li { 112 | margin: 10px 0 0 35px; 113 | } 114 | .tab a { 115 | color: #8FB0CF; 116 | } 117 | .tab strong { 118 | font-weight: bold; 119 | } 120 | .tab pre { 121 | font-size: 11px; 122 | margin-top: 20px; 123 | width: 668px; 124 | overflow: auto; 125 | clear: both; 126 | } 127 | .tab table { 128 | width: 100%; 129 | } 130 | .tab table td { 131 | padding: 6px 10px 6px 0; 132 | vertical-align: top; 133 | } 134 | .tab dt { 135 | margin-top: 16px; 136 | } 137 | 138 | #colorSelector { 139 | position: relative; 140 | width: 36px; 141 | height: 36px; 142 | background: url(../images/select.png); 143 | } 144 | #colorSelector div { 145 | position: absolute; 146 | top: 3px; 147 | left: 3px; 148 | width: 30px; 149 | height: 30px; 150 | background: url(../images/select.png) center; 151 | } 152 | #colorSelector2 { 153 | position: absolute; 154 | top: 0; 155 | left: 0; 156 | width: 36px; 157 | height: 36px; 158 | background: url(../images/select2.png); 159 | } 160 | #colorSelector2 div { 161 | position: absolute; 162 | top: 4px; 163 | left: 4px; 164 | width: 28px; 165 | height: 28px; 166 | background: url(../images/select2.png) center; 167 | } 168 | #colorpickerHolder2 { 169 | top: 32px; 170 | left: 0; 171 | width: 356px; 172 | height: 0; 173 | overflow: hidden; 174 | position: absolute; 175 | } 176 | #colorpickerHolder2 .colorpicker { 177 | background-image: url(../images/custom_background.png); 178 | position: absolute; 179 | bottom: 0; 180 | left: 0; 181 | } 182 | #colorpickerHolder2 .colorpicker_hue div { 183 | background-image: url(../images/custom_indic.gif); 184 | } 185 | #colorpickerHolder2 .colorpicker_hex { 186 | background-image: url(../images/custom_hex.png); 187 | } 188 | #colorpickerHolder2 .colorpicker_rgb_r { 189 | background-image: url(../images/custom_rgb_r.png); 190 | } 191 | #colorpickerHolder2 .colorpicker_rgb_g { 192 | background-image: url(../images/custom_rgb_g.png); 193 | } 194 | #colorpickerHolder2 .colorpicker_rgb_b { 195 | background-image: url(../images/custom_rgb_b.png); 196 | } 197 | #colorpickerHolder2 .colorpicker_hsb_s { 198 | background-image: url(../images/custom_hsb_s.png); 199 | display: none; 200 | } 201 | #colorpickerHolder2 .colorpicker_hsb_h { 202 | background-image: url(../images/custom_hsb_h.png); 203 | display: none; 204 | } 205 | #colorpickerHolder2 .colorpicker_hsb_b { 206 | background-image: url(../images/custom_hsb_b.png); 207 | display: none; 208 | } 209 | #colorpickerHolder2 .colorpicker_submit { 210 | background-image: url(../images/custom_submit.png); 211 | } 212 | #colorpickerHolder2 .colorpicker input { 213 | color: #778398; 214 | } 215 | #customWidget { 216 | position: relative; 217 | height: 36px; 218 | } 219 | -------------------------------------------------------------------------------- /static/colorpicker/images/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/Thumbs.db -------------------------------------------------------------------------------- /static/colorpicker/images/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/blank.gif -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_background.png -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_hex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_hex.png -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_hsb_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_hsb_b.png -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_hsb_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_hsb_h.png -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_hsb_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_hsb_s.png -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_indic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_indic.gif -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_overlay.png -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_rgb_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_rgb_b.png -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_rgb_g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_rgb_g.png -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_rgb_r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_rgb_r.png -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_select.gif -------------------------------------------------------------------------------- /static/colorpicker/images/colorpicker_submit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/colorpicker_submit.png -------------------------------------------------------------------------------- /static/colorpicker/images/custom_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/custom_background.png -------------------------------------------------------------------------------- /static/colorpicker/images/custom_hex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/custom_hex.png -------------------------------------------------------------------------------- /static/colorpicker/images/custom_hsb_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/custom_hsb_b.png -------------------------------------------------------------------------------- /static/colorpicker/images/custom_hsb_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/custom_hsb_h.png -------------------------------------------------------------------------------- /static/colorpicker/images/custom_hsb_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/custom_hsb_s.png -------------------------------------------------------------------------------- /static/colorpicker/images/custom_indic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/custom_indic.gif -------------------------------------------------------------------------------- /static/colorpicker/images/custom_rgb_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/custom_rgb_b.png -------------------------------------------------------------------------------- /static/colorpicker/images/custom_rgb_g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/custom_rgb_g.png -------------------------------------------------------------------------------- /static/colorpicker/images/custom_rgb_r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/custom_rgb_r.png -------------------------------------------------------------------------------- /static/colorpicker/images/custom_submit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/custom_submit.png -------------------------------------------------------------------------------- /static/colorpicker/images/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/select.png -------------------------------------------------------------------------------- /static/colorpicker/images/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/select2.png -------------------------------------------------------------------------------- /static/colorpicker/images/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/colorpicker/images/slider.png -------------------------------------------------------------------------------- /static/colorpicker/js/eye.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Zoomimage 4 | * Author: Stefan Petre www.eyecon.ro 5 | * 6 | */ 7 | (function($){ 8 | var EYE = window.EYE = function() { 9 | var _registered = { 10 | init: [] 11 | }; 12 | return { 13 | init: function() { 14 | $.each(_registered.init, function(nr, fn){ 15 | fn.call(); 16 | }); 17 | }, 18 | extend: function(prop) { 19 | for (var i in prop) { 20 | if (prop[i] != undefined) { 21 | this[i] = prop[i]; 22 | } 23 | } 24 | }, 25 | register: function(fn, type) { 26 | if (!_registered[type]) { 27 | _registered[type] = []; 28 | } 29 | _registered[type].push(fn); 30 | } 31 | }; 32 | }(); 33 | $(EYE.init); 34 | })(jQuery); 35 | -------------------------------------------------------------------------------- /static/colorpicker/js/layout.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | var initLayout = function() { 3 | var hash = window.location.hash.replace('#', ''); 4 | var currentTab = $('ul.navigationTabs a') 5 | .bind('click', showTab) 6 | .filter('a[rel=' + hash + ']'); 7 | if (currentTab.size() == 0) { 8 | currentTab = $('ul.navigationTabs a:first'); 9 | } 10 | showTab.apply(currentTab.get(0)); 11 | $('#colorpickerHolder').ColorPicker({flat: true}); 12 | $('#colorpickerHolder2').ColorPicker({ 13 | flat: true, 14 | color: '#00ff00', 15 | onSubmit: function(hsb, hex, rgb) { 16 | $('#colorSelector2 div').css('backgroundColor', '#' + hex); 17 | } 18 | }); 19 | $('#colorpickerHolder2>div').css('position', 'absolute'); 20 | var widt = false; 21 | $('#colorSelector2').bind('click', function() { 22 | $('#colorpickerHolder2').stop().animate({height: widt ? 0 : 173}, 500); 23 | widt = !widt; 24 | }); 25 | $('#colorpickerField1, #colorpickerField2, #colorpickerField3').ColorPicker({ 26 | onSubmit: function(hsb, hex, rgb, el) { 27 | $(el).val(hex); 28 | $(el).ColorPickerHide(); 29 | }, 30 | onBeforeShow: function () { 31 | $(this).ColorPickerSetColor(this.value); 32 | } 33 | }) 34 | .bind('keyup', function(){ 35 | $(this).ColorPickerSetColor(this.value); 36 | }); 37 | $('#colorSelector').ColorPicker({ 38 | color: '#0000ff', 39 | onShow: function (colpkr) { 40 | $(colpkr).fadeIn(500); 41 | return false; 42 | }, 43 | onHide: function (colpkr) { 44 | $(colpkr).fadeOut(500); 45 | return false; 46 | }, 47 | onChange: function (hsb, hex, rgb) { 48 | $('#colorSelector div').css('backgroundColor', '#' + hex); 49 | } 50 | }); 51 | }; 52 | 53 | var showTab = function(e) { 54 | var tabIndex = $('ul.navigationTabs a') 55 | .removeClass('active') 56 | .index(this); 57 | $(this) 58 | .addClass('active') 59 | .blur(); 60 | $('div.tab') 61 | .hide() 62 | .eq(tabIndex) 63 | .show(); 64 | }; 65 | 66 | EYE.register(initLayout, 'init'); 67 | })(jQuery) -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/favicon.ico -------------------------------------------------------------------------------- /static/fontawesome-webfont.afm: -------------------------------------------------------------------------------- 1 | StartFontMetrics 2.0 2 | Comment Generated by FontForge 20120731 3 | Comment Creation Date: Wed Nov 13 01:22:55 2013 4 | FontName fontawesome 5 | FullName fontawesome 6 | FamilyName fontawesome 7 | Weight Book 8 | Notice (Created by root with FontForge 2.0 (http://fontforge.sf.net)) 9 | ItalicAngle 0 10 | IsFixedPitch true 11 | UnderlinePosition -170.667 12 | UnderlineThickness 85.3333 13 | Version 001.000 14 | EncodingScheme ISO10646-1 15 | FontBBox 0 -75 900 825 16 | StartCharMetrics 2 17 | C -1 ; WX 899 ; N resize-full ; B 0 -75 900 825 ; 18 | C -1 ; WX 899 ; N resize-small ; B 7 -68 893 818 ; 19 | EndCharMetrics 20 | EndFontMetrics 21 | -------------------------------------------------------------------------------- /static/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/fontawesome-webfont.eot -------------------------------------------------------------------------------- /static/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /static/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/fontawesome-webfont.woff -------------------------------------------------------------------------------- /static/fontawesome.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 3.0.2 3 | * the iconic font designed for use with Twitter Bootstrap 4 | * ------------------------------------------------------- 5 | * The full suite of pictographic icons, examples, and documentation 6 | * can be found at: http://fortawesome.github.com/Font-Awesome/ 7 | * 8 | * License 9 | * ------------------------------------------------------- 10 | * - The Font Awesome font is licensed under the SIL Open Font License - http://scripts.sil.org/OFL 11 | * - Font Awesome CSS, LESS, and SASS files are licensed under the MIT License - 12 | * http://opensource.org/licenses/mit-license.html 13 | * - The Font Awesome pictograms are licensed under the CC BY 3.0 License - http://creativecommons.org/licenses/by/3.0/ 14 | * - Attribution is no longer required in Font Awesome 3.0, but much appreciated: 15 | * "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome" 16 | 17 | * Contact 18 | * ------------------------------------------------------- 19 | * Email: dave@davegandy.com 20 | * Twitter: http://twitter.com/fortaweso_me 21 | * Work: Lead Product Designer @ http://kyruus.com 22 | */ 23 | @font-face { 24 | font-family: 'FontAwesomeSagecell'; 25 | src: url('fontawesome-webfont.eot?v=3.0.1'); 26 | src: url('fontawesome-webfont.eot?#iefix&v=3.0.1') format('embedded-opentype'), 27 | url('fontawesome-webfont.woff?v=3.0.1') format('woff'), 28 | url('fontawesome-webfont.ttf?v=3.0.1') format('truetype'); 29 | font-weight: normal; 30 | font-style: normal; 31 | } 32 | /* Font Awesome styles 33 | ------------------------------------------------------- */ 34 | .sagecell [class^="sagecell_icon-"], 35 | .sagecell [class*=" sagecell_icon-"] { 36 | font-family: FontAwesomeSagecell; 37 | font-weight: normal; 38 | font-style: normal; 39 | text-decoration: inherit; 40 | -webkit-font-smoothing: antialiased; 41 | 42 | font-size: 125%; 43 | background: transparent; 44 | /* sprites.less reset */ 45 | display: inline; 46 | width: auto; 47 | height: auto; 48 | line-height: normal; 49 | vertical-align: baseline; 50 | margin-top: 0; 51 | } 52 | .sagecell [class^="sagecell_icon-"]:before, 53 | .sagecell [class*=" sagecell_icon-"]:before { 54 | text-decoration: inherit; 55 | display: inline-block; 56 | speak: none; 57 | } 58 | .sagecell .sagecell_icon-resize-full:before { content: "\f021"; } 59 | .sagecell .sagecell_icon-resize-small:before { content: "\f022"; } 60 | -------------------------------------------------------------------------------- /static/jquery-1.5.min.js: -------------------------------------------------------------------------------- 1 | jquery.min.js -------------------------------------------------------------------------------- /static/logo_sagemath_cell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/logo_sagemath_cell.png -------------------------------------------------------------------------------- /static/logo_sagemath_cell.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 11 | cell 12 | 13 | 14 | -------------------------------------------------------------------------------- /static/main.css: -------------------------------------------------------------------------------- 1 | @import "../build/vendor/components/codemirror/lib/codemirror.css"; 2 | @import "../build/vendor/components/codemirror/addon/display/fullscreen.css"; 3 | @import "../build/vendor/components/codemirror/addon/fold/foldgutter.css"; 4 | @import "../build/vendor/components/codemirror/addon/hint/show-hint.css"; 5 | 6 | @import "jquery-ui.min.css"; 7 | @import "colorpicker/css/colorpicker.css"; 8 | @import "fontawesome.css"; 9 | 10 | @import "sagecell.css"; 11 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /sockjs 3 | -------------------------------------------------------------------------------- /static/root.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px; 3 | font-family: sans-serif; 4 | background-color: white; 5 | } 6 | 7 | #sagecell_main { 8 | /* This is so that an interact that changes size 9 | doesn't cause the browser to scroll everything up and down */ 10 | margin-bottom: 80%; 11 | } 12 | 13 | a { 14 | text-decoration: none; 15 | color: mediumblue; 16 | } 17 | 18 | a:hover { 19 | text-decoration: underline; 20 | } 21 | 22 | .sagecell button.sagecell_evalButton { 23 | font-size: 120%; 24 | } 25 | 26 | #sagecell_about { 27 | position: absolute; 28 | top: 5px; 29 | right: 5px; 30 | font-size: 80%; 31 | } 32 | 33 | ol.sagecell_toc, 34 | ol.sagecell_toc ol { 35 | list-style: none; 36 | } 37 | 38 | .sagecell_disclaimer { 39 | font-size: 80%; 40 | } 41 | 42 | .sagecell .CodeMirror-scroll { 43 | min-height: 14em; 44 | } 45 | -------------------------------------------------------------------------------- /static/sagemathcell-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/sagemathcell-logo.png -------------------------------------------------------------------------------- /static/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sagecell/2924393fec89f5f69c49b2df5738f3e7c0b84844/static/spinner.gif -------------------------------------------------------------------------------- /static/test/linked.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sage Cell Server 7 | 8 | 11 | 12 | 13 | Type your own Sage computation below and click “Evaluate”. 14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | SageMathCell Help Resources 9 | 10 | 11 | {% include info.html %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /templates/info.html: -------------------------------------------------------------------------------- 1 |

About

2 | 3 |

SageMathCell project is an easy-to-use web interface to a free open-source mathematics software system SageMath. You can help SageMath by becoming a .

4 | 5 |

It allows embedding Sage computations into any webpage: check out our short instructions, a comprehensive description of capabilities, or Notebook Player to convert Jupyter notebooks into dynamic HTML pages!

6 | 7 |

{% include provider.html %} You can also set up your own server.

8 | 9 |

General Questions on Using Sage

10 | 11 |

There are a lot of resources available to help you use Sage. In particular, you may ask questions on sage-support discussion group or ask.sagemath.org website.

12 | 13 |

Problems and Suggestions

14 | 15 |

Unfortunately, we can no longer allow user code in cells to freely access Internet. See this discussion for details.

16 | 17 |

If you experience any problems or have suggestions on improving this service (e.g., you want a package installed), please email Andrey Novoseltsev.

18 | 19 |

SageMathCell is expected to work with any modern browser and without any downtime.

20 | 21 |

CoCalc

22 | 23 |

Need more power and flexibility but still prefer to avoid your own installation of Sage? CoCalc will allow you to work with multiple persistent worksheets in Sage, IPython, LaTeX, and much, much more!

24 | -------------------------------------------------------------------------------- /templates/provider.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/root.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sage Cell Server 8 | 9 | 10 | 11 | 12 | 13 |

SageMathCell

14 | Type some Sage code below and press Evaluate. 15 |
16 | About SageMathCell 17 | 35 | {% include info.html %} 36 | 37 | 38 | -------------------------------------------------------------------------------- /templates/tos_default.html: -------------------------------------------------------------------------------- 1 |

The Sage Cell Server is a non-commercial service that may change or be shut down 2 | at any time. You agree to not post malware, viruses, spam, etc. You will not use 3 | the Sage Cell Server to send spam or attack other computers or people in any way. 4 | You will not attempt to bring harm to the system or hack into it. You also agree 5 | that all content that you compute with this site is by default visible to anybody 6 | else on the Internet. The resources available to you through this service, and 7 | these Terms of Usage, may change at any time without warning. This service is not 8 | guaranteed to have any uptime or backups.

9 |

THE SERVICE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS OR SERVICE PROVIDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 13 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

15 | -------------------------------------------------------------------------------- /tests/forking_kernel_manager_tests.py: -------------------------------------------------------------------------------- 1 | import forking_kernel_manager 2 | from misc import assert_is, assert_equal, assert_in, assert_not_in, assert_raises, assert_regexp_matches, assert_is_instance, assert_is_not_none, assert_greater, assert_len, assert_uuid, capture_output 3 | from multiprocessing import Process, Pipe 4 | from IPython.config.loader import Config 5 | 6 | def test_init(): 7 | fkm = forking_kernel_manager.ForkingKernelManager("testing.log", '127.0.0.1', update_function=test_init) 8 | assert_len(fkm.kernels, 0) 9 | assert_equal(fkm.filename, "testing.log") 10 | assert_in("function test_init at ", repr(fkm.update_function)) 11 | 12 | class TestForkingKernelManager(object): 13 | def setup(self): 14 | self.a = forking_kernel_manager.ForkingKernelManager("/dev/null", '127.0.0.1', update_function=None) 15 | def teardown(self): 16 | for i in self.a.kernels.keys(): 17 | self.a.kernels[i][0].terminate() 18 | def test_start_kernel_success(self): 19 | y = self.a.start_kernel() 20 | 21 | assert_is_instance(y, dict) 22 | assert_len(y, 2) 23 | assert_in("kernel_id", y) 24 | assert_uuid(y["kernel_id"]) 25 | assert_in("connection", y) 26 | assert_len(y["connection"], 6) 27 | for s in ("stdin_port", "hb_port", "shell_port", "iopub_port"): 28 | assert_in(s, y["connection"]) 29 | assert_len(str(y["connection"][s]), 5) 30 | assert_in("ip", y["connection"]) 31 | assert_equal(y["connection"]["ip"], "127.0.0.1") 32 | assert_in("key", y["connection"]) 33 | assert_uuid(y["connection"]["key"]) 34 | 35 | assert_in(y["kernel_id"], self.a.kernels.keys()) 36 | assert_is_instance(self.a.kernels[y["kernel_id"]][0], Process) 37 | assert_is(self.a.kernels[y["kernel_id"]][0].is_alive(), True) 38 | 39 | def test_resource_limit_setting(self): # incomplete 40 | y = self.a.start_kernel(resource_limits = {"RLIMIT_CPU": 3}) 41 | proc = self.a.kernels[y["kernel_id"]][0] 42 | # how to test if rlimit_cpu/any other rlimit is set given the multiprocessing.Process object proc?? 43 | 44 | def test_kill_kernel_success(self): # depends on start_kernel 45 | y = self.a.start_kernel() 46 | kernel_id = y["kernel_id"] 47 | proc = self.a.kernels[kernel_id][0] 48 | 49 | assert_is(proc.is_alive(), True) 50 | retval = self.a.kill_kernel(kernel_id) 51 | assert_is(retval, True) 52 | assert_not_in(kernel_id, self.a.kernels.keys()) 53 | assert_is(proc.is_alive(), False) 54 | 55 | def test_kill_kernel_invalid_kernel_id(self): 56 | kernel_id = 44 57 | retval = self.a.kill_kernel(kernel_id) 58 | assert_is(retval, False) 59 | 60 | def test_interrupt_kernel_success(self): # depends on start_kernel 61 | y = self.a.start_kernel() 62 | kernel_id = y["kernel_id"] 63 | proc = self.a.kernels[kernel_id][0] 64 | 65 | assert_is(proc.is_alive(), True) 66 | retval = self.a.interrupt_kernel(kernel_id) 67 | assert_is(retval, True) 68 | assert_is(proc.is_alive(), True) 69 | 70 | def test_interrupt_kernel_invalid_kernel_id(self): 71 | kernel_id = 44 72 | retval = self.a.interrupt_kernel(kernel_id) 73 | assert_is(retval, False) 74 | 75 | def test_restart_kernel_success(self): # depends on start_kernel 76 | y = self.a.start_kernel() 77 | kernel_id = y["kernel_id"] 78 | proc = self.a.kernels[kernel_id][0] 79 | preports = self.a.kernels[kernel_id][1] 80 | 81 | assert_is(proc.is_alive(), True) 82 | retval = self.a.restart_kernel(kernel_id) 83 | assert_is(proc.is_alive(), False) # old kernel process is killed 84 | 85 | proc = self.a.kernels[kernel_id][0] 86 | assert_is(proc.is_alive(), True) # and a new kernel process with the same kernel_id exists 87 | postports = self.a.kernels[kernel_id][1] 88 | 89 | for s in ("stdin_port", "hb_port", "shell_port", "iopub_port"): 90 | assert_equal(preports[s], postports[s]) # and that it has the same ports as before 91 | -------------------------------------------------------------------------------- /tests/multimechanize/config.cfg: -------------------------------------------------------------------------------- 1 | [global] 2 | run_time = 300 3 | rampup = 10 4 | results_ts_interval = 3 5 | 6 | [user_group-1] 7 | threads = 350 8 | script = simple_session.py 9 | 10 | [user_group-2] 11 | threads = 150 12 | script = interact_session.py 13 | -------------------------------------------------------------------------------- /tests/multimechanize/test_scripts/client.py: -------------------------------------------------------------------------------- 1 | import urllib2 2 | import websocket 3 | import json 4 | import uuid 5 | 6 | root = "http://localhost:8888" 7 | 8 | class SageCellSession(object): 9 | def __init__(self): 10 | f = urllib2.urlopen("%s/kernel" % (root,), "") 11 | data = json.loads(f.read()) 12 | f.close() 13 | self.kernel_id = data["kernel_id"] 14 | self.ws_url = data["ws_url"] 15 | self.iopub = websocket.create_connection("%skernel/%s/iopub" % (self.ws_url, self.kernel_id)) 16 | self.shell = websocket.create_connection("%skernel/%s/shell" % (self.ws_url, self.kernel_id)) 17 | self.session_id = str(uuid.uuid4()) 18 | 19 | def __del__(self): 20 | self.close() 21 | 22 | def __enter__(self): 23 | return self 24 | 25 | def __exit__(self, etype, value, traceback): 26 | self.close() 27 | 28 | def execute(self, code): 29 | content = {"code": code, 30 | "silent": False, 31 | "user_variables": [], 32 | "user_expressions": {"_sagecell_files": "sys._sage_.new_files()"}, 33 | "allow_stdin": False} 34 | self.send_msg("execute_request", content) 35 | 36 | def update_interact(self, interact_id, values): 37 | self.execute("sys._sage_.update_interact(%r, %r)" % (interact_id, values)) 38 | 39 | def send_msg(self, msg_type, content): 40 | msg = {"header": {"msg_id": str(uuid.uuid4()), 41 | "username": "username", 42 | "session": self.session_id, 43 | "msg_type": msg_type 44 | }, 45 | "metadata": {}, 46 | "content": content, 47 | "parent_header":{} 48 | } 49 | self.shell.send(json.dumps(msg)) 50 | 51 | def close(self): 52 | self.iopub.close() 53 | self.shell.close() 54 | 55 | def iopub_recv(self): 56 | return json.loads(self.iopub.recv()) 57 | 58 | def shell_recv(self): 59 | return json.loads(self.shell.recv()) 60 | 61 | def load_root(): 62 | resources = ["/", "/static/root.css", "/static/jquery.min.js", 63 | "/static/embedded_sagecell.js", 64 | "/static/jquery-ui/css/sagecell/jquery-ui-1.8.21.custom.css", 65 | "/static/colorpicker/css/colorpicker.css", 66 | "/static/all.min.css", "/static/mathjax/MathJax.js", 67 | "/static/sagelogo.png", "/static/spinner.gif", 68 | "/sagecell.html", "/static/all.min.js", 69 | "/static/mathjax/config/TeX-AMS-MML_HTMLorMML.js", 70 | "/static/mathjax/images/MenuArrow-15.png", 71 | "/static/jquery-ui/css/sagecell/images/ui-bg_highlight-hard_60_99bbff_1x100.png", 72 | "/static/mathjax/extensions/jsMath2jax.js", 73 | "/static/jquery-ui/css/sagecell/images/ui-bg_highlight-hard_90_99bbff_1x100.png"] 74 | for r in resources: 75 | f = urllib2.urlopen(root + r) 76 | assert f.code == 200, "Bad response: HTTP %d" % (f.code,) 77 | -------------------------------------------------------------------------------- /tests/multimechanize/test_scripts/interact_session.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import client 4 | import time 5 | import random 6 | 7 | computation = """@interact 8 | def f(x=(1, 100, 1)): 9 | print(x^2)""" 10 | 11 | class Transaction(object): 12 | """ 13 | A transaction that simulates loading the page 14 | and manipulating an interact 15 | """ 16 | 17 | def __init__(self): 18 | self.custom_timers = {} 19 | 20 | def run(self): 21 | t = time.time() 22 | client.load_root() 23 | self.custom_timers["root load"] = time.time() - t 24 | time.sleep(5) 25 | t = time.time() 26 | with client.SageCellSession() as s: 27 | self.custom_timers["initial connection"] = time.time() - t 28 | t = time.time() 29 | s.execute(computation) 30 | output = "" 31 | while True: 32 | msg = s.iopub_recv() 33 | if msg["header"]["msg_type"] == "status" and msg["content"]["execution_state"] == "idle": 34 | break 35 | elif msg["header"]["msg_type"] == "display_data" and "application/sage-interact" in msg["content"]["data"]: 36 | interact_id = msg["content"]["data"]["application/sage-interact"]["new_interact_id"] 37 | elif msg["header"]["msg_type"] == "stream" and msg["content"]["name"] == "stdout" and msg["metadata"]["interact_id"] == interact_id: 38 | output += msg["content"]["data"] 39 | assert output == "1\n", "Incorrect output: %r" % (output,) 40 | times = [] 41 | self.custom_timers["initial computation"] = time.time() - t 42 | for i in range(10): 43 | time.sleep(1) 44 | num = random.randint(1, 100) 45 | t = time.time() 46 | s.update_interact(interact_id, {"x": num}) 47 | output = "" 48 | while True: 49 | msg = s.iopub_recv() 50 | if msg["header"]["msg_type"] == "status" and msg["content"]["execution_state"] == "idle": 51 | break 52 | elif msg["header"]["msg_type"] == "stream" and msg["content"]["name"] == "stdout" and msg["metadata"]["interact_id"] == interact_id: 53 | output += msg["content"]["data"] 54 | assert int(output.strip()) == num * num, "Incorrect output: %r" % (output,) 55 | times.append(time.time() - t) 56 | self.custom_timers["interact update (average of 10)"] = sum(times) / len(times) 57 | 58 | if __name__ == "__main__": 59 | t = Transaction() 60 | t.run() 61 | print(t.custom_timers) 62 | -------------------------------------------------------------------------------- /tests/multimechanize/test_scripts/simple_session.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import client 4 | import time 5 | import random 6 | 7 | class Transaction(object): 8 | """ 9 | A transaction that simulates loading the page 10 | and performing a simple addition 11 | """ 12 | 13 | def __init__(self): 14 | self.custom_timers = {} 15 | 16 | def run(self): 17 | t = time.time() 18 | client.load_root() 19 | self.custom_timers["root load"] = time.time() - t 20 | time.sleep(5) 21 | t = time.time() 22 | with client.SageCellSession() as s: 23 | self.custom_timers["initial connection"] = time.time() - t 24 | t = time.time() 25 | num1 = random.randint(1, 10 ** 20) 26 | num2 = random.randint(1, 10 ** 20) 27 | s.execute("print %d + %d" % (num1, num2)) 28 | output = "" 29 | while True: 30 | msg = s.iopub_recv() 31 | if msg["header"]["msg_type"] == "status" and msg["content"]["execution_state"] == "idle": 32 | break 33 | elif msg["header"]["msg_type"] == "stream" and msg["content"]["name"] == "stdout": 34 | output += msg["content"]["data"] 35 | assert int(output.strip()) == num1 + num2, "Incorrect output: %r" % (output,) 36 | self.custom_timers["computation"] = time.time() - t 37 | 38 | if __name__ == "__main__": 39 | t = Transaction() 40 | t.run() 41 | print t.custom_timers 42 | -------------------------------------------------------------------------------- /tests/untrusted_kernel_manager_tests.py: -------------------------------------------------------------------------------- 1 | import untrusted_kernel_manager 2 | 3 | from misc import assert_is, assert_equal, assert_in, assert_not_in, assert_raises, assert_regexp_matches, assert_is_instance, assert_is_not_none, assert_greater, assert_len, assert_uuid, capture_output, Config 4 | 5 | def test_init(): 6 | umkm = untrusted_kernel_manager.UntrustedMultiKernelManager("testing.log", '127.0.0.1', update_function=test_init) 7 | assert_len(umkm._kernels, 0) 8 | assert_equal(umkm.filename, "testing.log") 9 | assert_is(hasattr(umkm, "fkm"), True) 10 | 11 | class TestUntrustedMultiKernelManager(object): 12 | def setup(self): 13 | self.a = untrusted_kernel_manager.UntrustedMultiKernelManager("/dev/null", '127.0.0.1') 14 | def teardown(self): 15 | for i in list(self.a._kernels): 16 | self.a.kill_kernel(i) 17 | 18 | def test_start_kernel_success(self): 19 | y = self.a.start_kernel() 20 | assert_is_instance(y, dict) 21 | assert_len(y, 2) 22 | assert_in("kernel_id", y) 23 | assert_uuid(y["kernel_id"]) 24 | assert_in(y["kernel_id"], self.a._kernels) 25 | assert_in("connection", y) 26 | assert_len(y["connection"], 6) 27 | for s in ("stdin_port", "hb_port", "shell_port", "iopub_port"): 28 | assert_in(s, y["connection"]) 29 | assert_len(str(y["connection"][s]), 5) 30 | assert_in("ip", y["connection"]) 31 | assert_equal(y["connection"]["ip"], "127.0.0.1") 32 | assert_in("key", y["connection"]) 33 | assert_uuid(y["connection"]["key"]) 34 | 35 | def test_kill_kernel_success(self): # depends on start_kernel 36 | y = self.a.start_kernel() 37 | kernel_id = y["kernel_id"] 38 | assert_in(kernel_id, self.a._kernels) 39 | 40 | retval = self.a.kill_kernel(kernel_id) 41 | assert_is(retval, True) 42 | assert_not_in(kernel_id, self.a._kernels) 43 | 44 | def test_kill_kernel_invalid_kernel_id(self): 45 | retval = self.a.kill_kernel(44) 46 | assert_is(retval, False) 47 | 48 | def test_purge_kernels_success(self): # depends on start_kernel 49 | for i in xrange(5): 50 | self.a.start_kernel() 51 | 52 | retval = self.a.purge_kernels() 53 | assert_equal(retval, []) 54 | 55 | def test_purge_kernels_with_failures(self): # depends on start_kernel 56 | for i in xrange(5): 57 | self.a.start_kernel() 58 | self.a._kernels.add(55) 59 | self.a._kernels.add(66) 60 | 61 | retval = self.a.purge_kernels() 62 | assert_in(55, retval) 63 | assert_in(66, retval) 64 | assert_len(retval, 2) 65 | -------------------------------------------------------------------------------- /timing/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The timing tests use a heavily-modified version of MultiMechanize, which is a framework for running stress tests of web applications. To test: 3 | 4 | #. Download the `custom version of MultiMechanize `_ 5 | #. Copy (or symbolic link) the ``timing`` directory into the ``projects`` directory of multi-mechanize 6 | #. Get the web server running 7 | #. Run ``python multi-mechanize.py timing`` from the multi-mechanize root directory 8 | 9 | """ 10 | 11 | -------------------------------------------------------------------------------- /timing/config.cfg: -------------------------------------------------------------------------------- 1 | [global] 2 | run_time: 100 3 | rampup: 10 4 | console_logging: off 5 | results_ts_interval: 5 6 | 7 | project_config_script: curl --silent http://boxen.math.washington.edu:5467/config 8 | 9 | [user_group-1] 10 | threads: 50 11 | script: simple_computation.py 12 | script_options: poll_interval=0.1, base_url="http://boxen.math.washington.edu:5467" 13 | 14 | 15 | #[user_group-2] 16 | #threads: 100 17 | #script: simple_computation.py 18 | -------------------------------------------------------------------------------- /timing/test_scripts/MultipartPostHandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | #### 4 | # 02/2006 Will Holcomb 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # 7/2/10 refactored by Jason Grout--made two functions instead of a class 17 | # 7/26/07 Slightly modified by Brian Schneider 18 | # in order to support unicode files ( multipart_encode function ) 19 | # from http://peerit.blogspot.com/2007/07/multipartposthandler-doesnt-work-for.html 20 | 21 | """ 22 | Usage: 23 | Enables the use of multipart/form-data for posting forms 24 | 25 | Inspirations: 26 | Upload files in python: 27 | http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306 28 | urllib2_file: 29 | Fabien Seisen: 30 | 31 | Example: 32 | import MultipartPostHandler, urllib2, cookielib 33 | 34 | cookies = cookielib.CookieJar() 35 | opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), 36 | MultipartPostHandler.MultipartPostHandler) 37 | params = { "username" : "bob", "password" : "riviera", 38 | "file" : open("filename", "rb") } 39 | opener.open("http://wwww.bobsite.com/upload/", params) 40 | 41 | Further Example: 42 | The main function of this file is a sample which downloads a page and 43 | then uploads it to the W3C validator. 44 | """ 45 | 46 | import urllib 47 | import urllib2 48 | import mimetools, mimetypes 49 | import os, stat 50 | from cStringIO import StringIO 51 | 52 | # Controls how sequences are uncoded. If true, elements may be given multiple values by 53 | # assigning a sequence. 54 | doseq = 1 55 | 56 | 57 | def encode_request(request): 58 | data = request.get_data() 59 | if data is not None and type(data) != str: 60 | v_files = [] 61 | v_vars = [] 62 | if isinstance(data, dict): 63 | data=data.items() 64 | try: 65 | for(key, value) in data: 66 | if type(value) == file: 67 | v_files.append((key, value)) 68 | else: 69 | v_vars.append((key, value)) 70 | except TypeError: 71 | systype, value, traceback = sys.exc_info() 72 | raise TypeError, "not a valid non-string sequence or mapping object", traceback 73 | 74 | if len(v_files) == 0: 75 | data = urllib.urlencode(v_vars, doseq) 76 | else: 77 | boundary, data = multipart_encode(v_vars, v_files) 78 | 79 | contenttype = 'multipart/form-data; boundary=%s' % boundary 80 | if(request.has_header('Content-Type') 81 | and request.get_header('Content-Type').find('multipart/form-data') != 0): 82 | print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data') 83 | request.add_unredirected_header('Content-Type', contenttype) 84 | 85 | request.add_data(data) 86 | 87 | return request 88 | 89 | def multipart_encode(vars, files, boundary = None, buf = None): 90 | if boundary is None: 91 | boundary = mimetools.choose_boundary() 92 | if buf is None: 93 | buf = StringIO() 94 | for(key, value) in vars: 95 | buf.write('--%s\r\n' % boundary) 96 | buf.write('Content-Disposition: form-data; name="%s"' % key) 97 | buf.write('\r\n\r\n%s\r\n'%value) 98 | for(key, fd) in files: 99 | file_size = os.fstat(fd.fileno())[stat.ST_SIZE] 100 | filename = fd.name.split('/')[-1] 101 | contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' 102 | buf.write('--%s\r\n' % boundary) 103 | buf.write('Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename)) 104 | buf.write('Content-Type: %s\r\n' % contenttype) 105 | # buffer += 'Content-Length: %s\r\n' % file_size 106 | fd.seek(0) 107 | buf.write('\r\n' + fd.read() + '\r\n') 108 | buf.write('--' + boundary + '--\r\n\r\n') 109 | buf = buf.getvalue() 110 | return boundary, buf 111 | 112 | -------------------------------------------------------------------------------- /timing/test_scripts/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /timing/test_scripts/sagecell.py: -------------------------------------------------------------------------------- 1 | import random 2 | from MultipartPostHandler import encode_request 3 | from urllib2 import urlopen, Request 4 | from urllib import urlencode 5 | from json import loads 6 | 7 | 8 | EVAL_PATH='/eval' 9 | POLL_PATH='/output_poll' 10 | FILE_PATH='/files' 11 | 12 | 13 | class Session: 14 | def __init__(self, server): 15 | server=server.rstrip('/') 16 | self.server=server 17 | self.session=random.random() 18 | 19 | def prepare_execution_request(self, code, files=None, sage_mode=True): 20 | """ 21 | Prepare an execution request prior to sending it. 22 | 23 | We break up the preparation and sending phases so that it is easy 24 | to time just the request. 25 | """ 26 | msg=[('session_id', self.session), 27 | ('commands', code), 28 | ('msg_id', random.random()), 29 | ('sage_mode', True if sage_mode else False)] 30 | if files is not None: 31 | for filename in files: 32 | msg.append(('file', open(filename,"rb"))) 33 | request=Request(self.server+EVAL_PATH, msg) 34 | return encode_request(request) 35 | 36 | def send_execution_request(self, request): 37 | """ 38 | Send an execution request along with a number of files. 39 | 40 | TODO: break into a "prepare" and "send" function for timing? 41 | """ 42 | result=urlopen(request).read() 43 | if result: 44 | try: 45 | jsonresult=loads(result) 46 | self.session = jsonresult["session_id"] 47 | return jsonresult 48 | except ValueError: 49 | return result 50 | else: 51 | return result 52 | 53 | def output_poll(self, sequence=0): 54 | query=urlencode([('computation_id', self.session), 55 | ('sequence',sequence)]) 56 | url=self.server+POLL_PATH+'?'+query 57 | return loads(urlopen(url).read()) 58 | 59 | def get_file(self, filename): 60 | return urlopen(self.server+"%s/%s/%s"%(FILE_PATH,self.session,filename)).read() 61 | 62 | -------------------------------------------------------------------------------- /timing/test_scripts/simple_computation.py: -------------------------------------------------------------------------------- 1 | 2 | from urllib2 import urlopen 3 | from urllib import urlencode 4 | import json 5 | from random import random 6 | from time import sleep, time 7 | import sys 8 | from multiprocessing import Pool 9 | import contextlib 10 | import traceback 11 | 12 | from timing_util import timing, json, json_request 13 | from time import time 14 | 15 | from sagecell import Session 16 | 17 | class Transaction(object): 18 | def __init__(self, **kwargs): 19 | self.custom_timers={} 20 | self.MAXRAND=kwargs.get('maxrand', 2**30) 21 | self.BASE_URL=kwargs.get('base_url', 'http://localhost:8080/') 22 | self.POLL_INTERVAL=kwargs.get('poll_interval', 0.25) 23 | self.TIMEOUT=kwargs.get('timeout', 30) 24 | 25 | def run(self): 26 | """ 27 | Ask for the sum of two random numbers and check the result 28 | """ 29 | computation_times=[] 30 | response_times=[] 31 | a=int(random()*self.MAXRAND) 32 | b=int(random()*self.MAXRAND) 33 | code=json.dumps('print(%d+%d)' % (a, b)) 34 | s=Session(self.BASE_URL) 35 | request=s.prepare_execution_request(code) 36 | sequence=0 37 | 38 | with timing(computation_times): 39 | with timing(response_times): 40 | s.send_execution_request(request) 41 | start_time = time() 42 | done=False 43 | while not done: 44 | if time()-start_time>self.TIMEOUT: 45 | raise Exception("TIMEOUT") 46 | sleep(self.POLL_INTERVAL) 47 | with timing(response_times): 48 | r=s.output_poll(sequence) 49 | if len(r)==0 or 'content' not in r: 50 | continue 51 | for m in r['content']: 52 | sequence+=1 53 | if (m['msg_type']=="stream" 54 | and m['content']['name']=="stdout"): 55 | ans=int(m['content']['data']) 56 | if ans!=a+b: 57 | print("COMPUTATION NOT CORRECT") 58 | raise ValueError("Computation not correct: %s+%s!=%s, off by %s "%(a,b,ans, ans-a-b)) 59 | else: 60 | done=True 61 | break 62 | 63 | self.custom_timers['Computation']=computation_times 64 | self.custom_timers['Response']=response_times 65 | 66 | __all__=['Transaction'] 67 | 68 | if __name__ == '__main__': 69 | import argparse 70 | 71 | parser = argparse.ArgumentParser(description='Run simple additionc computation.') 72 | parser.add_argument('--base_url', default='http://localhost:8080', 73 | help='the base url for the sage server') 74 | parser.add_argument('-q','--quiet', dest='quiet', action='store_true') 75 | parser.add_argument('--timeout', dest='timeout', default=30, type=float) 76 | args = parser.parse_args() 77 | 78 | trans = Transaction(base_url=args.base_url, timeout=args.timeout) 79 | trans.run() 80 | if not args.quiet: 81 | print(trans.custom_timers) 82 | -------------------------------------------------------------------------------- /timing/test_scripts/simple_upload_modify_download.py: -------------------------------------------------------------------------------- 1 | 2 | from urllib2 import urlopen 3 | from urllib import urlencode 4 | import json 5 | from random import random 6 | from time import sleep, time 7 | import sys 8 | import numpy 9 | from multiprocessing import Pool 10 | import contextlib 11 | import traceback 12 | 13 | from timing_util import timing, json, json_request 14 | 15 | from sagecell import Session 16 | 17 | code=""" 18 | print('beginning...') 19 | with open('test.txt','r+') as f: 20 | s = f.read() 21 | f.seek(0) 22 | f.write(s.replace('test','finished')) 23 | print('ending...') 24 | """ 25 | 26 | FILE_CONTENTS = 'This is a test file' 27 | FILE_RESULT_CONTENTS = FILE_CONTENTS.replace('test','finished') 28 | 29 | class Transaction(object): 30 | def __init__(self, **kwargs): 31 | self.custom_timers={} 32 | self.BASE_URL=kwargs.get('base_url', 'http://localhost:8080/') 33 | self.POLL_INTERVAL=kwargs.get('poll_interval', 0.1) 34 | with open('test.txt', 'w') as f: 35 | f.write(FILE_CONTENTS) 36 | 37 | def run(self): 38 | """ 39 | Upload a file, change it, and then download it again 40 | """ 41 | computation_times=[] 42 | response_times=[] 43 | 44 | s=Session(self.BASE_URL) 45 | request=s.prepare_execution_request(code,files=['test.txt']) 46 | sequence=0 47 | with timing(computation_times): 48 | with timing(response_times): 49 | s.send_execution_request(request) 50 | 51 | done=False 52 | while not done: 53 | sleep(self.POLL_INTERVAL) 54 | with timing(response_times): 55 | r=s.output_poll(sequence) 56 | if len(r)==0 or 'content' not in r: 57 | continue 58 | for m in r['content']: 59 | sequence+=1 60 | if (m['msg_type']=="extension" 61 | and m['content']['msg_type']=="files"): 62 | returned_file=m['content']['content']['files'][0] 63 | if returned_file!='test.txt': 64 | print("RETURNED FILENAME NOT CORRECT") 65 | raise ValueError("Returned filename not correct: %s"%returned_file) 66 | with timing(response_times): 67 | f=s.get_file(returned_file) 68 | if f!=FILE_RESULT_CONTENTS: 69 | print("RETURNED FILE CONTENTS NOT CORRECT") 70 | raise ValueError("Returned file contents not correct: %s"%f) 71 | # if we've made it this far, we're done 72 | done=True 73 | break 74 | 75 | self.custom_timers['Computation']=computation_times 76 | self.custom_timers['Response']=response_times 77 | 78 | __all__=['Transaction'] 79 | 80 | if __name__ == '__main__': 81 | trans = Transaction() 82 | trans.run() 83 | print(trans.custom_timers) 84 | -------------------------------------------------------------------------------- /timing/test_scripts/timing_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provide a timing utility context manager 3 | """ 4 | import contextlib 5 | @contextlib.contextmanager 6 | def timing(results=None): 7 | """ 8 | Time the execution of the block of code. If a results list is 9 | passed in, the time is appended to the list. Also returns a list 10 | of one element containing the time the execution took. 11 | 12 | To use, do something like:: 13 | 14 | from time import sleep 15 | results_list=[] 16 | with timing(results_list) as t: 17 | sleep(1) 18 | print results_list, t 19 | 20 | Exceptions in the code should be re-raised and the timing should 21 | correctly be set regardless of the exceptions. 22 | """ 23 | from time import time 24 | try: 25 | # code in the context is executed when we yield 26 | start=[time()] 27 | yield start 28 | except: 29 | # any exceptions in the code should get propogated 30 | raise 31 | finally: 32 | start.append(time()-start[0]) 33 | if results is not None: 34 | results.append(start) 35 | 36 | import urllib 37 | import urllib2 38 | 39 | try: import simplejson as json 40 | except ImportError: import json 41 | 42 | def json_request(url, data=None): 43 | """ 44 | Send a JSON message to the URL and return the result as a 45 | dictionary. 46 | 47 | :param data: a JSON-stringifiable object, passed in the POST 48 | variable ``message`` 49 | :returns: a JSON-parsed dict/list/whatever from the server reply 50 | """ 51 | if data is not None: 52 | data = urllib.urlencode(data) 53 | response = urllib2.urlopen(url, data) 54 | return json.loads(response.read()) 55 | 56 | -------------------------------------------------------------------------------- /web_server.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import fcntl 4 | import os 5 | import signal 6 | import socket 7 | import struct 8 | 9 | import asyncio 10 | asyncio.set_event_loop(asyncio.new_event_loop()) 11 | 12 | import paramiko 13 | import psutil 14 | import tornado.ioloop 15 | import tornado.web 16 | 17 | import handlers 18 | from log import logger 19 | from kernel_dealer import KernelDealer 20 | import misc 21 | import permalink 22 | 23 | 24 | config = misc.Config() 25 | 26 | 27 | def start_providers(port, providers, dir): 28 | r""" 29 | Start kernel providers. 30 | 31 | INPUT: 32 | 33 | - ``port`` -- port for providers to connect to 34 | 35 | - ``providers`` -- list of dictionaries 36 | 37 | - ``dir`` -- directory name for user files saved by kernels 38 | """ 39 | for config in providers: 40 | client = paramiko.SSHClient() 41 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 42 | client.connect(config["host"], username=config["username"]) 43 | command = "{} '{}/kernel_provider.py' {} '{}'".format( 44 | config["python"], config["location"], port, dir) 45 | logger.debug("starting kernel provider: %s", command) 46 | client.exec_command(command) 47 | client.close() 48 | 49 | 50 | class SageCellServer(tornado.web.Application): 51 | def __init__(self, baseurl, dir): 52 | # This matches a kernel id (uuid4 format) from a url 53 | _kernel_id_regex = r"(?P\w+-\w+-\w+-\w+-\w+)" 54 | baseurl = baseurl.rstrip('/') 55 | handlers_list = [ 56 | (r"/", handlers.RootHandler), 57 | (r"/embedded_sagecell.js", 58 | tornado.web.RedirectHandler, 59 | {"url":baseurl+"/static/embedded_sagecell.js"}), 60 | (r"/help.html", handlers.HelpHandler), 61 | (r"/kernel", handlers.KernelHandler), 62 | (r"/kernel/%s" % _kernel_id_regex, handlers.KernelHandler), 63 | (r"/kernel/%s/channels" % _kernel_id_regex, 64 | handlers.WebChannelsHandler), 65 | (r"/kernel/%s/files/(?P.*)" % _kernel_id_regex, 66 | handlers.FileHandler, {"path": dir}), 67 | (r"/permalink", permalink.PermalinkHandler), 68 | (r"/service", handlers.ServiceHandler), 69 | (r"/tos.html", handlers.TOSHandler), 70 | ] + handlers.KernelRouter.urls 71 | handlers_list = [[baseurl+i[0]]+list(i[1:]) for i in handlers_list] 72 | settings = dict( 73 | compress_response = True, 74 | template_path = os.path.join( 75 | os.path.dirname(os.path.abspath(__file__)), "templates"), 76 | static_path = os.path.join( 77 | os.path.dirname(os.path.abspath(__file__)), "static"), 78 | static_url_prefix = baseurl + "/static/", 79 | static_handler_class = handlers.StaticHandler 80 | ) 81 | self.kernel_dealer = KernelDealer(config.get("provider_settings")) 82 | start_providers(self.kernel_dealer.port, config.get("providers"), dir) 83 | self.completer = handlers.Completer(self.kernel_dealer) 84 | db = __import__('db_' + config.get('db')) 85 | self.db = db.DB(config.get('db_config')['uri']) 86 | self.ioloop = tornado.ioloop.IOLoop.current() 87 | super(SageCellServer, self).__init__(handlers_list, **settings) 88 | logger.info('SageCell server started') 89 | try: 90 | from systemd.daemon import notify 91 | logger.debug('notifying systemd that we are ready') 92 | notify('READY=1\nMAINPID={}'.format(os.getpid()), True) 93 | except ImportError: 94 | pass 95 | 96 | 97 | def get_ip_address(ifname): 98 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 99 | return socket.inet_ntoa(fcntl.ioctl( 100 | s.fileno(), 101 | 0x8915, # SIOCGIFADDR 102 | struct.pack('256s', ifname[:15]) 103 | )[20:24]) 104 | 105 | 106 | if __name__ == "__main__": 107 | import argparse 108 | parser = argparse.ArgumentParser(description='Launch a SageCell web server', 109 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 110 | parser.add_argument('-p', '--port', type=int, default=8888, 111 | help='port to launch the server') 112 | parser.add_argument('-b', '--baseurl', default="", help="base url") 113 | parser.add_argument('--interface', default=None, help="interface to listen on (default all)") 114 | parser.add_argument('--dir', default=config.get("dir"), help="directory for user files") 115 | args = parser.parse_args() 116 | 117 | logger.info("starting tornado web server") 118 | from lockfile.pidlockfile import PIDLockFile 119 | pidfile_path = config.get('pid_file') 120 | pidlock = PIDLockFile(pidfile_path) 121 | if pidlock.is_locked(): 122 | old_pid = pidlock.read_pid() 123 | logger.info("Lock file exists for PID %d." % old_pid) 124 | if os.getpid() == old_pid: 125 | logger.info("Stale lock since we have the same PID.") 126 | else: 127 | try: 128 | old = psutil.Process(old_pid) 129 | if os.path.basename(__file__) in old.cmdline(): 130 | try: 131 | logger.info("Trying to terminate old instance...") 132 | old.terminate() 133 | try: 134 | old.wait(10) 135 | except psutil.TimeoutExpired: 136 | logger.info("Trying to kill old instance.") 137 | old.kill() 138 | except psutil.AccessDenied: 139 | logger.error("The process seems to be SageCell, but " 140 | "can not be stopped. Its command line: %s" 141 | % old.cmdline()) 142 | else: 143 | logger.info("Process does not seem to be SageCell.") 144 | except psutil.NoSuchProcess: 145 | logger.info("No such process exist anymore.") 146 | logger.info("Breaking old lock.") 147 | pidlock.break_lock() 148 | 149 | pidlock.acquire(timeout=10) 150 | app = SageCellServer(args.baseurl, args.dir) 151 | listen = {'port': args.port, 'xheaders': True} 152 | if args.interface is not None: 153 | listen['address'] = get_ip_address(args.interface) 154 | logger.info("Listening configuration: %s", listen) 155 | 156 | def handler(signum, frame): 157 | logger.info("Received %s, shutting down...", signum) 158 | app.kernel_dealer.stop() 159 | app.ioloop.stop() 160 | 161 | signal.signal(signal.SIGHUP, handler) 162 | signal.signal(signal.SIGINT, handler) 163 | signal.signal(signal.SIGTERM, handler) 164 | 165 | app.listen(**listen) 166 | app.ioloop.start() 167 | pidlock.release() 168 | logger.info('SageCell server stopped') 169 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | entry: "./js/main.js", 6 | output: { 7 | filename: "./embedded_sagecell.js", 8 | path: path.resolve(__dirname, "build"), 9 | }, 10 | // Enable sourcemaps for debugging webpack's output. 11 | devtool: "source-map", 12 | resolve: { 13 | extensions: ["", ".webpack.js", ".web.js", ".js"], 14 | modules: ["build/vendor", "node_modules"], 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.m?js$/, 20 | exclude: /(node_modules|bower_components|JSmol.js)/, 21 | use: { 22 | loader: "babel-loader", 23 | options: { 24 | presets: ["@babel/preset-env"], 25 | plugins: ["@babel/plugin-transform-modules-amd"], 26 | }, 27 | }, 28 | }, 29 | // JSmol.js is not written in strict mode, so the babel-loader 30 | // will error if it is imported. Instead we directly load 31 | // it in an unprocessed form. 32 | { test: /JSmol.js$/, loader: "source-map-loader" }, 33 | { 34 | test: /\.(html|css)$/i, 35 | use: "raw-loader", 36 | }, 37 | ], 38 | }, 39 | plugins: [ 40 | new webpack.ProvidePlugin({ 41 | jQuery: "jquery", 42 | $: "jquery", 43 | // Normally the following lines are used to make sure that jQuery 44 | // cannot "leak" into the outside environment. However, since 45 | // we *want* to initialize the global jQuery object, we omit them. 46 | // "window.jQuery": "jquery", 47 | // "window.$": "jquery", 48 | }), 49 | new webpack.optimize.LimitChunkCountPlugin({ 50 | maxChunks: 1, 51 | }), 52 | ], 53 | }; 54 | --------------------------------------------------------------------------------