SageTeX / remote-sagetex.dtx

Full commit
% \section{The \texttt{remote-sagetex} script}
% \label{sec:remote-sagetex-code}
% Here we describe the Python code for ||. Since its
% job is to replicate the functionality of using Sage and ||,
% there is some overlap with the Python module.
% \iffalse
% \fi
% The |#!/usr/bin/env python| line is provided for us by the |.ins|
% file's preamble, so we don't put it here.
%    \begin{macrocode}
from __future__ import print_function
import sys
    import json
except ImportError:
    print('You need Python 2.6 or later to run this! Exiting.')
import time
import re
import urllib
import hashlib
import os
import os.path
import shutil
import getopt
from contextlib import closing

# You can fill in your information here, or you can set any of these to #
# None and the script will ask you to provide them if you don't specify #
# them as command-line arguments.                                       #
server = None       # '', https works too
username = None     # 'my_name'
password = None     # 's33krit'

usage = """Process a SageTeX-generated .sage file using a remote Sage server.

Usage: {0} [options] inputfile.sage


    -h, --help:         print this message
    -s, --server:       the Sage server to contact
    -u, --username:     username on the server
    -p, --password:     your password

If the server does not begin with the four characters `http', then
`https://' will be prepended to the server name.

You can hard-code the server, username, and password values in the
remote-sagetex script. If any are omitted, you will be asked to provide

See the SageTeX documentation for more details on usage and limitations
of remote-sagetex.""".format(sys.argv[0])

    opts, args = getopt.getopt(sys.argv[1:], 'hs:u:p:',
                               ['help', 'server=', 'user=', 'password='])
except getopt.GetoptError as err:
    print(str(err), usage, sep='\n\n')
    #    usage()

for o, a in opts:
    if o in ('-h', '--help'):
    elif o in ('-s', '--server'):
        server = a
    elif o in ('-u', '--user'):
        username = a
    elif o in ('-p', '--password'):
        password = a

if len(args) != 1:
    print('Error: must specify exactly one file. Please specify options first.',
          usage, sep='\n\n')

jobname = os.path.splitext(args[0])[0]
%    \end{macrocode}
% When we send things to the server, we get everything back as a string,
% including tracebacks. We can search through output using regexps, but
% there's a more robust way: put in a special string that changes every
% time, and look for that. Then it is massively unlikely that a user's
% code could produce output that we'll mistake for an actual traceback.
% System time will work well enough for these purposes. We produce this
% string now, and we it when parsing the |.sage| file (we insert it into
% code blocks) and when parsing the output that the remote server gives
% us.
%    \begin{macrocode}
traceback_str = 'Exception in SageTeX session {0}:'.format(time.time())
%    \end{macrocode}
% \begin{macro}{parsedotsage}
% To figure out what commands to send the remote server, we actually
% read in the |.sage| file as strings and parse it. This seems a bit
% strange, but since we know exactly what the format of that file is, we
% can parse it with a couple flags and a handful of regexps.
%    \begin{macrocode}
def parsedotsage(fn):
    with open(fn, 'r') as f:
%    \end{macrocode}
% Here are the regexps we use to snarf the interesting bits out of the
% |.sage| file. Below we'll use the |re| module's |match| function so we
% needn't anchor any of these at the beginning of the line.
%    \begin{macrocode}
        inline = re.compile(r" _st_.inline\((?P<num>\d+), (?P<code>.*)\)")
        plot = re.compile(r" _st_.plot\((?P<num>\d+), (?P<code>.*)\)")
        goboom = re.compile(r" _st_.goboom\((?P<num>\d+)\)")
        pausemsg = re.compile(r"print.'(?P<msg>SageTeX (un)?paused.*)'")
        blockbegin = re.compile(r"_st_.blockbegin\(\)")
        ignore = re.compile(r"(try:)|(except):")
        in_comment = False
        in_block = False
        cmds = []
%    \end{macrocode}
% Okay, let's go through the file. We're going to make a list of
% dictionaries. Each dictionary corresponds to something we have to do
% with the remote server, except for the pause/unpause ones, which we
% only use to print out information for the user. All the dictionaries
% have a |type| key, which obviously tells you type they are. The
% pause/unpause dictionaries then just have a |msg| which we toss out to
% the user. The ``real'' dictionaries all have the following keys:
% \begin{itemize}
% \item |type|: one of |inline|, |plot|, and |block|.
% \item |goboom|: used to help the user pinpoint errors, just like the
% |goboom| function (\pageref{macro:goboom}) does.
% \item |code|: the code to be executed.
% \end{itemize}
% Additionally, the |inline| and |plot| dicts have a |num| key for the
% label we write to the |.sout| file.
% Here's the whole parser loop. The interesting bits are for parsing
% blocks because there we need to accumulate several lines of code.
%    \begin{macrocode}
        for line in f.readlines():
            if line.startswith('"""'):
                in_comment = not in_comment
            elif not in_comment:
                m = pausemsg.match(line)
                if m:
                    cmds.append({'type': 'pause',
                m = inline.match(line)
                if m:
                    cmds.append({'type': 'inline',
                m = plot.match(line)
                if m:
                    cmds.append({'type': 'plot',
%    \end{macrocode}
% The order of the next three ``if''s is important, since we need the
% ``goboom'' line and the ``blockbegin'' line to \emph{not} get included
% into the block's code. Note that the lines in the |.sage| file already
% have some indentation, which we'll use when sending the block to the
% server---we wrap the text in a try/except.
%    \begin{macrocode}
                m = goboom.match(line)
                if m:
                    cmds[-1]['goboom'] ='num')
                    if in_block:
                        in_block = False
                if in_block and not ignore.match(line):
                    cmds[-1]['code'] += line
                if blockbegin.match(line):
                    cmds.append({'type': 'block',
                                 'code': '',
                                 'indent': 0})
                    in_block = True
    return cmds
%    \end{macrocode}
% \end{macro}
% Parsing the |.sage| file is simple enough so that we can write one
% function and just do it. Interacting with the remote server is a bit
% more complicated, and requires us to carry some state, so let's make a
% class.
% \begin{macro}{RemoteSage}
% This is pretty simple; it's more or less a translation of the examples
% in \texttt{sage/server/simple/}.
%    \begin{macrocode}
debug = False
class RemoteSage:
    def __init__(self, server, user, password):
        self._srv = server
        sep = '___S_A_G_E___'
        self._response = re.compile('(?P<header>.*)' + sep +
                                   '\n*(?P<output>.*)', re.DOTALL)
        self._session = self._get_url('login',
                                    urllib.urlencode({'username': user,
%    \end{macrocode}
% In the string below, we want to do ``partial formatting'': we format
% in the traceback string now, and want to be able to format in the code
% later. The double braces get ignored by |format()| now, and are picked
% up by |format()| when we use this later.
%    \begin{macrocode}
        self._codewrap = """try:
    import traceback
    def __st_plot__(counter, _p_, format='notprovided', **kwargs):
        if format == 'notprovided':
            formats = ['eps', 'pdf']
            formats = [format]
        for fmt in formats:
            plotfilename = 'plot-%s.%s' % (counter, fmt)
  , **kwargs)""")

    def _encode(self, d):
        return 'session={0}&'.format(self._session) + urllib.urlencode(d)

    def _get_url(self, action, u):
        with closing(urllib.urlopen(self._srv + '/simple/' + action +
                                    '?' + u)) as h:
            data = self._response.match(
            result = json.loads('header'))
            result['output'] ='output').rstrip()
        return result

    def _get_file(self, fn, cell, ofn=None):
        with closing(urllib.urlopen(server + '/simple/' + 'file' + '?' +
                     self._encode({'cell': cell, 'file': fn}))) as h:
            myfn = ofn if ofn else fn
            with open(myfn, 'w') as f:
%    \end{macrocode}
% The |code| below gets stuffed between a try/except, so make sure it's
% indented!
%    \begin{macrocode}
    def _do_cell(self, code):
        realcode = self._codewrap.format(code)
        result = self._get_url('compute', self._encode({'code': realcode}))
        if result['status'] == 'computing':
            cell = result['cell_id']
            while result['status'] == 'computing':
                result = self._get_url('status', self._encode({'cell': cell}))
        if debug:
            print('cell: <<<', realcode, '>>>', 'result: <<<',
                  result['output'], '>>>', sep='\n')
        return result

    def do_inline(self, code):
        return self._do_cell(' latex({0})'.format(code))

    def do_block(self, code):
        result = self._do_cell(code)
        for fn in result['files']:
            self._get_file(fn, result['cell_id'])
        return result

    def do_plot(self, num, code, plotdir):
        result = self._do_cell(' __st_plot__({0}, {1})'.format(num, code))
        for fn in result['files']:
            self._get_file(fn, result['cell_id'], os.path.join(plotdir, fn))
        return result
%    \end{macrocode}
% When using the simple server API, it's important to log out so the
% server doesn't accumulate idle sessions that take up lots of memory.
% We define a |close()| method and use this class with the |closing|
% context manager that always calls |close()| on the way out.
%    \begin{macrocode}
    def close(self):
        sys.stdout.write('Logging out of {0}...'.format(server))
        self._get_url('logout', self._encode({}))
%    \end{macrocode}
% \end{macro}
% Next we have a little pile of miscellaneous functions and variables
% that we want to have at hand while doing our work. Note that we again
% use the traceback string in the error-finding regular expression.
%    \begin{macrocode}
def do_plot_setup(plotdir):
    print('Initializing plots directory')
    if os.path.isdir(plotdir):
    return True

did_plot_setup = False
plotdir = 'sage-plots-for-' + jobname + '.tex'

def labelline(n, s):
    return r'\newlabel{@sageinline' + str(n) + '}{{' + s  + '}{}{}{}{}}\n'

def printc(s):
    print(s, end='')

error = re.compile("(^" + traceback_str + ")|(^Syntax Error:)", re.MULTILINE)

def check_for_error(string, line):
**** Error in Sage code on line {0} of {1}.tex!
**** Running Sage on {1}.sage failed! Fix {1}.tex and try again.""".format(
              line, jobname, string))
%    \end{macrocode}
% Now let's actually start doing stuff.
%    \begin{macrocode}
print('Processing Sage code for {0}.tex using remote Sage server.'.format(

if not server:
    server = raw_input('Enter server: ')

if not username:
    username = raw_input('Enter username: ')

if not password:
    from getpass import getpass
    password = getpass('Please enter password for user {0} on {1}: '.format(
        username, server))

if not server.startswith('http'):
    server = 'https://' + server

printc('Parsing {0}.sage...'.format(jobname))
cmds = parsedotsage(jobname + '.sage')

sout = '% This file was *autogenerated* from the file {0}.sage.\n'.format(

printc('Logging into {0} and starting session...'.format(server))
with closing(RemoteSage(server, username, password)) as sage:
    for cmd in cmds:
        if cmd['type'] == 'inline':
            printc('Inline formula {0}...'.format(cmd['num']))
            result = sage.do_inline(cmd['code'])
            check_for_error(result['output'], cmd['goboom'])
            sout += labelline(cmd['num'], result['output'])
        if cmd['type'] == 'block':
            printc('Code block begin...')
            result = sage.do_block(cmd['code'])
            check_for_error(result['output'], cmd['goboom'])
        if cmd['type'] == 'plot':
            printc('Plot {0}...'.format(cmd['num']))
            if not did_plot_setup:
                did_plot_setup = do_plot_setup(plotdir)
            result = sage.do_plot(cmd['num'], cmd['code'], plotdir)
            check_for_error(result['output'], cmd['goboom'])
        if cmd['type'] == 'pause':
        if int(time.time()) % 2280 == 0:
            printc('Unscheduled offworld activation; closing iris...')

with open(jobname + '.sage', 'r') as sagef:
    h = hashlib.md5()
    for line in sagef:
        if (not line.startswith(' _st_.goboom') and 
            not line.startswith("print 'SageT")):
    sout += """%{0}% md5sum of corresponding .sage file
% (minus "goboom" and pause/unpause lines)

printc('Writing .sout file...')
with open(jobname + '.sout', 'w') as soutf:
print('Sage processing complete. Run LaTeX on {0}.tex again.'.format(jobname))
%    \end{macrocode}

% \endinput
% Local Variables:
% mode: doctex
% TeX-master: "sagetexpackage"
% End: