SageTeX / remote-sagetex.dtx

% \section{The \texttt{remote-sagetex} script}
% \label{sec:remote-sagetex-code}
%
% Here we describe the Python code for |remote-sagetex.py|. Since its
% job is to replicate the functionality of using Sage and |sagetex.py|,
% there is some overlap with the Python module.
%
% \iffalse
%<*remotesagetex>
% \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 json
import sys
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       # 'http://foo.com:8000', 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

Options:

    -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
them.

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

try:
    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')
    sys.exit(2)

for o, a in opts:
    if o in ('-h', '--help'):
        print(usage)
        sys.exit()
    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')
    sys.exit(2)

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 to
% look for typical traceback strings, but there's a more robust way: put
% in a special string that changes every time and is printed when
% there's an error, 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 (page \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',
                                 'msg': m.group('msg')})
                m = inline.match(line)
                if m:
                    cmds.append({'type': 'inline',
                                 'num': m.group('num'),
                                 'code': m.group('code')})
                m = plot.match(line)
                if m:
                    cmds.append({'type': 'plot',
                                 'num': m.group('num'),
                                 'code': m.group('code')})
%    \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'] = m.group('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': ''})
                    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/twist.py}.
%    \begin{macrocode}
debug = False
class RemoteSage:
    def __init__(self, server, user, password):
        self._srv = server.rstrip('/')
        sep = '___S_A_G_E___'
        self._response = re.compile('(?P<header>.*)' + sep +
                                   '\n*(?P<output>.*)', re.DOTALL)
        self._404 = re.compile('404 Not Found')
        self._session = self._get_url('login',
                                    urllib.urlencode({'username': user,
                                    'password':
                                    password}))['session']
%    \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:
{{0}}
except:
    print('{0}')
    traceback.print_exc()""".format(traceback_str)
        self.do_block("""
    import traceback
    def __st_plot__(counter, _p_, format='notprovided', **kwargs):
        if format == 'notprovided':
            formats = ['eps', 'pdf']
        else:
            formats = [format]
        for fmt in formats:
            plotfilename = 'plot-%s.%s' % (counter, fmt)
            _p_.save(filename=plotfilename, **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(h.read())
            result = json.loads(data.group('header'))
            result['output'] = data.group('output').rstrip()
        return result

    def _get_file(self, fn, cell, ofn=None):
        with closing(urllib.urlopen(self._srv + '/simple/' + 'file' + '?' +
                     self._encode({'cell': cell, 'file': fn}))) as h:
            myfn = ofn if ofn else fn
            data = h.read()
            if not self._404.search(data):
                with open(myfn, 'w') as f:
                    f.write(data)
            else:
                print('Remote server reported {0} could not be found:'.format(
                      fn))
                print(data)
%    \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':
                sys.stdout.write('working...')
                sys.stdout.flush()
                time.sleep(10)
                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))
        sys.stdout.flush()
        self._get_url('logout', self._encode({}))
        print('done')
%    \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):
    printc('initializing plots directory...')
    if os.path.isdir(plotdir):
        shutil.rmtree(plotdir)
    os.mkdir(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='')
    sys.stdout.flush()

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

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

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')
print('done.')

sout = '% This file was *autogenerated* from the file {0}.sage.\n'.format(
    os.path.splitext(jobname)[0])

printc('Logging into {0} and starting session...'.format(server))
with closing(RemoteSage(server, username, password)) as sage:
    print('done.')
    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'])
            print('done.')
        if cmd['type'] == 'block':
            printc('Code block begin...')
            result = sage.do_block(cmd['code'])
            check_for_error(result['output'], cmd['goboom'])
            print('end.')
        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'])
            print('done.')
        if cmd['type'] == 'pause':
            print(cmd['msg'])
        if int(time.time()) % 2280 == 0:
            printc('Unscheduled offworld activation; closing iris...')
            time.sleep(1)
            print('end.')

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")):
            h.update(line)
    sout += """%{0}% md5sum of corresponding .sage file
% (minus "goboom" and pause/unpause lines)
""".format(h.hexdigest())

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

% \endinput
%</remotesagetex>
% Local Variables:
% mode: doctex
% TeX-master: "sagetexpackage"
% End:
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.