summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx
diff options
context:
space:
mode:
Diffstat (limited to 'Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx')
-rw-r--r--Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx438
1 files changed, 438 insertions, 0 deletions
diff --git a/Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx b/Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx
new file mode 100644
index 00000000000..2360e220277
--- /dev/null
+++ b/Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx
@@ -0,0 +1,438 @@
+% \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 provide a filename here and the script will read your login #
+# information from that file. The format must be: #
+# #
+# server = 'http://foo.com:8000' #
+# username = 'my_name' #
+# password = 's33krit' #
+# #
+# You can omit one or more of those lines, use " quotes, and put hash #
+# marks at the beginning of a line for comments. Command-line args #
+# take precedence over information from the file. #
+#########################################################################
+login_info_file = None # e.g. '/home/foo/Private/sagetex-login.txt'
+
+
+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
+ -f, --file: get login information from a file
+
+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 filename from which to read login information into
+the remote-sagetex script. Command-line arguments take precedence over
+the contents of that file. See the SageTeX documentation for formatting
+details.
+
+If any of the server, username, and password 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])
+
+server, username, password = (None,) * 3
+
+try:
+ opts, args = getopt.getopt(sys.argv[1:], 'hs:u:p:f:',
+ ['help', 'server=', 'user=', 'password=', 'file='])
+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
+ elif o in ('-f', '--file'):
+ login_info_file = 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}
+% \changes{v2.2.1}{2009/06/20}{Fix stupid bug in \texttt{do\_inline()}
+% so that we actually write output to .sout file}
+%
+% 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(' print(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 login_info_file:
+ with open(login_info_file, 'r') as f:
+ print('Reading login information from {0}.'.format(login_info_file))
+ get_val = lambda x: x.split('=')[1].strip().strip('\'"')
+ for line in f:
+ print(line)
+ if not line.startswith('#'):
+ if line.startswith('server') and not server:
+ server = get_val(line)
+ if line.startswith('username') and not username:
+ username = get_val(line)
+ if line.startswith('password') and not password:
+ password = get_val(line)
+
+if not server:
+ server = raw_input('Enter server: ')
+
+if not server.startswith('http'):
+ server = 'https://' + 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))
+
+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)
+% \end{macrocode}
+% Putting the |{1}| in the string, just to replace it with |%|, seems a
+% bit weird, but if I put a single percent sign there, Docstrip won't
+% put that line into the resulting |.py| file---and if I put two percent
+% signs, it replaces them with |\MetaPrefix| which is |##| when this
+% file is generated. This is a quick and easy workaround.
+% \begin{macrocode}
+ sout += """%{0}% md5sum of corresponding .sage file
+{1} (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: