Source

SageTeX / remote-sagetex.dtx

Full commit
% \section{The \texttt{remote-sagetex} script}
% \label{sec:remote-sagetex-code}
%
% Here we describe the Python code for |remote-sagetex|
%
% \iffalse
%<*remotesagetex>
% \fi
%
% First, |makestatic.py| script. It's about the most basic, generic
% Python script taking command-line arguments that you'll find. 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}
# 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.
server = 'http://localhost:8000'
username = 'admin'
password = 'password'

import sys
try:
    import json
except ImportError:
    print 'You need Python 2.6 or later to run this! Exiting.'
    sys.exit(1)
import time
import re
import urllib
import hashlib
import os
import os.path
import shutil
import getopt
from contextlib import contextmanager

def usage():
    print("""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, err:
    print(str(err))
    usage()
    sys.exit(2)

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

jobname = os.path.splitext(args[0])[0]

def parsedotsage(fn):
    with open(fn, 'r') as f:
        ignore = re.compile(r"""(\#\#.This.file.*was.\*autogenerated)|
                                (import.sagetex)|
                                (_st_.=.sagetex)|
                                (_st_.blockend\(\))|
                                (_st_.useimagemagick = True)|
                                (_st_.useepstopdf = True)|
                                (try:)|
                                (except:)""", re.VERBOSE)
        goboom = re.compile(r" _st_.goboom\((?P<num>\d+)\)")
        pausemsg = re.compile(r"print '(?P<msg>SageTeX (un)?paused.*)'")
        inline = re.compile(r" _st_.inline\((?P<num>\d+), (?P<code>.*)\)")
        blockbegin = re.compile(r"_st_.blockbegin\(\)")
        ws = re.compile(r"(?P<indent>\s+)")
        plot = re.compile(r" _st_.plot\((?P<num>\d+), (?P<code>.*)\)")
        in_comment = False
        in_block = False
        cmds = []
        for line in f.readlines():
            if not ignore.match(line):
                if line[:-1] == '"""':
                    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')})
                    m = goboom.match(line)
                    if m:
                        cmds[-1]['goboom'] = m.group('num')
                        if in_block:
                            in_block = False
                    if in_block:
                        if cmds[-1]['indent'] == 0:
                            # this is the first line of the block, so
                            # establish indentation to remove. We know it
                            # must be at least 1.
                            cmds[-1]['indent'] = len(ws.match(line).group('indent'))
                        cmds[-1]['cell'] += line[cmds[-1]['indent']:]
                    if blockbegin.match(line):
                        cmds.append({'type': 'block',
                                     'cell': '',
                                     'indent': 0})
                        in_block = True
    return cmds

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,
                                    'password':
                                    password}))['session']
        self.did_plot_setup = False


    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(server + '/simple/' + 'file' + '?' +
                     self.encode({'cell': cell, 'file': fn}))) as h:
            myfn = ofn if ofn else fn
            with open(myfn, 'w') as f:
                f.write(h.read())

    def do_cell(self, code):
        result = self.get_url('compute', self.encode({'code': code}))
        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: <<<')
            print(code)
            print('>>>')
            print('result: <<<')
            print(result['output'])
            print('>>>')
        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):
        if not self.did_plot_setup:
            self.do_block("""
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)""")
            self.did_plot_setup = True
        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

    def close(self):
        sys.stdout.write('Logging out of {0}...'.format(server))
        sys.stdout.flush()
        self.get_url('logout', self.encode({}))
        print('done')

# is it just me, or does the function definition read like something by
# Dr. Seuss?
@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()

def do_plot_setup(plotdir):
    print('Initializing plots directory')
    if os.path.isdir(plotdir):
        shutil.rmtree(plotdir)
    os.mkdir(plotdir)
    return True

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

def progress(s, linebreak=True):
    if linebreak:
        print(s)
    else:
        sys.stdout.write(s)
        sys.stdout.flush()

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

error = re.compile(r"(^Traceback \(most recent call last\):)|" +
                   r"(^SyntaxError:)", re.MULTILINE)
def check_for_error(string, line):
    if error.search(string):
        print('\n**** Error in Sage code on line {0} of {1}.tex!'.format(
                line, jobname))
        print(string)
        print('\n**** Running Sage on {0}.sage failed! Fix {0}.tex and try again.'.format(jobname))
        sys.exit(1)

# okay, now we start actually doing stuff

progress('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 server[:4] != 'http':
    server = 'https://' + server

progress('Parsing {0}.sage...'.format(jobname), False)
cmds = parsedotsage(jobname + '.sage')
progress('done.')

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

progress('Logging into {0} and starting session...'.format(server), False)
with closing(RemoteSage(server, username, password)) as sage:
    progress('done.')
    for cmd in cmds:
        if cmd['type'] == 'inline':
            progress('Inline formula {0}...'.format(cmd['num']), False)
            result = sage.do_inline(cmd['code'])
            check_for_error(result['output'], cmd['goboom'])
            sout += labelline(cmd['num'], result['output'])
            progress('done.')
        if cmd['type'] == 'block':
            progress('Code block begin...'.format(cmd['goboom']), False)
            result = sage.do_block(cmd['cell'])
            check_for_error(result['output'], cmd['goboom'])
            progress('end.')
        if cmd['type'] == 'plot':
            progress('Plot {0}...'.format(cmd['num']), False)
            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'])
            progress('done.')
        if cmd['type'] == 'pause':
            progress(cmd['msg'])
        if int(time.time()) % 2280 == 0:
            progress('Unscheduled offworld activation; closing iris...', False)
            time.sleep(1)
            progress('done.')

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

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

% \endinput
%</remotesagetex>
% Local Variables: 
% mode: doctex
% TeX-master: "sagetexpackage"
% End: