% \section{The \texttt{remote-sagetex}script}% \label{sec:remote-sagetex-code}%%%HerewedescribethePythoncodefor|remote-sagetex.py|.Sinceits%jobistoreplicatethefunctionalityofusingSageand|sagetex.py|,%thereissomeoverlapwiththePythonmodule.%% \iffalse%<*remotesagetex>% \fi%%The|#!/usr/bin/env python| line is provided for us by the |.ins|%file's preamble, so we don'tputithere.% \begin{macrocode}from__future__importprint_functionimportjsonimportsysimporttimeimportreimporturllibimporthashlibimportosimportos.pathimportshutilimportgetoptfromcontextlibimportclosing########################################################################## 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.sageOptions: -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 fileIf 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 intothe remote-sagetex script. Command-line arguments take precedence overthe contents of that file. See the SageTeX documentation for formattingdetails.If any of the server, username, and password are omitted, you will beasked to provide them.See the SageTeX documentation for more details on usage and limitationsof remote-sagetex.""".format(sys.argv[0])server,username,password=(None,)*3try:opts,args=getopt.getopt(sys.argv[1:],'hs:u:p:f:',['help','server=','user=','password=','file='])exceptgetopt.GetoptErroraserr:print(str(err),usage,sep='\n\n')sys.exit(2)foro,ainopts:ifoin('-h','--help'):print(usage)sys.exit()elifoin('-s','--server'):server=aelifoin('-u','--user'):username=aelifoin('-p','--password'):password=aelifoin('-f','--file'):login_info_file=aiflen(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}%Whenwesendthingstotheserver,wegeteverythingbackasastring,%includingtracebacks.Wecansearchthroughoutputusingregexpsto%lookfortypicaltracebackstrings,butthere's a more robust way: put%inaspecialstringthatchangeseverytimeandisprintedwhen%there's an error, and look for that. Then it is massively unlikely%thatauser's code could produce output that we'llmistakeforan%actualtraceback.Systemtimewillworkwellenoughforthese%purposes.Weproducethisstringnow,andweitwhenparsingthe%|.sage|file(weinsertitintocodeblocks)andwhenparsingthe%outputthattheremoteservergivesus.% \begin{macrocode}traceback_str='Exception in SageTeX session {0}:'.format(time.time())% \end{macrocode}% \begin{macro}{parsedotsage}%Tofigureoutwhatcommandstosendtheremoteserver,weactually%readinthe|.sage|fileasstringsandparseit.Thisseemsabit%strange,butsinceweknowexactlywhattheformatofthatfileis,we%canparseitwithacoupleflagsandahandfulofregexps.% \begin{macrocode}defparsedotsage(fn):withopen(fn,'r')asf:% \end{macrocode}%Herearetheregexpsweusetosnarftheinterestingbitsoutofthe%|.sage|file.Belowwe'll use the |re| module's|match|functionsowe%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=Falsein_block=Falsecmds=[]% \end{macrocode}%Okay,let's go through the file. We'regoingtomakealistof%dictionaries.Eachdictionarycorrespondstosomethingwehavetodo%withtheremoteserver,exceptforthepause/unpauseones,whichwe%onlyusetoprintoutinformationfortheuser.Allthedictionaries%havea|type|key,whichobviouslytellsyoutypetheyare.The%pause/unpausedictionariesthenjusthavea|msg|whichwetossoutto%theuser.The``real''dictionariesallhavethefollowingkeys:% \begin{itemize}% \item|type|:oneof|inline|,|plot|,and|block|.% \item|goboom|:usedtohelptheuserpinpointerrors,justlikethe%|goboom|function(page \pageref{macro:goboom})does.% \item|code|:thecodetobeexecuted.% \end{itemize}%Additionally,the|inline|and|plot|dictshavea|num|keyforthe%labelwewritetothe|.sout|file.%%Here's the whole parser loop. The interesting bits are for parsing%blocksbecausethereweneedtoaccumulateseverallinesofcode.% \begin{macrocode}forlineinf.readlines():ifline.startswith('"""'):in_comment=notin_commentelifnotin_comment:m=pausemsg.match(line)ifm:cmds.append({'type':'pause','msg':m.group('msg')})m=inline.match(line)ifm:cmds.append({'type':'inline','num':m.group('num'),'code':m.group('code')})m=plot.match(line)ifm:cmds.append({'type':'plot','num':m.group('num'),'code':m.group('code')})% \end{macrocode}%Theorderofthenextthree``if''sisimportant,sinceweneedthe%``goboom''lineandthe``blockbegin''lineto \emph{not}getincluded%intotheblock's code. Note that the lines in the |.sage| file already%havesomeindentation,whichwe'll use when sending the block to the%server---wewrapthetextinatry/except.% \begin{macrocode}m=goboom.match(line)ifm:cmds[-1]['goboom']=m.group('num')ifin_block:in_block=Falseifin_blockandnotignore.match(line):cmds[-1]['code']+=lineifblockbegin.match(line):cmds.append({'type':'block','code':''})in_block=Truereturncmds% \end{macrocode}% \end{macro}%Parsingthe|.sage|fileissimpleenoughsothatwecanwriteone%functionandjustdoit.Interactingwiththeremoteserverisabit%morecomplicated,andrequiresustocarrysomestate,solet's make a%class.%% \begin{macro}{RemoteSage}% \changes{v2.2.1}{2009/06/20}{Fixstupidbugin \texttt{do\_inline()}%sothatweactuallywriteoutputto.soutfile}%%Thisisprettysimple;it's more or less a translation of the examples%in \texttt{sage/server/simple/twist.py}.% \begin{macrocode}debug=FalseclassRemoteSage: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}%Inthestringbelow,wewanttodo``partialformatting'':weformat%inthetracebackstringnow,andwanttobeabletoformatinthecode%later.Thedoublebracesgetignoredby|format()|now,andarepicked%upby|format()|whenweusethislater.% \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):withclosing(urllib.urlopen(self._srv+'/simple/'+action+'?'+u))ash:data=self._response.match(h.read())result=json.loads(data.group('header'))result['output']=data.group('output').rstrip()returnresultdef_get_file(self,fn,cell,ofn=None):withclosing(urllib.urlopen(self._srv+'/simple/'+'file'+'?'+self._encode({'cell':cell,'file':fn})))ash:myfn=ofnifofnelsefndata=h.read()ifnotself._404.search(data):withopen(myfn,'w')asf:f.write(data)else:print('Remote server reported {0} could not be found:'.format(fn))print(data)% \end{macrocode}%The|code|belowgetsstuffedbetweenatry/except,somakesureit's%indented!% \begin{macrocode}def_do_cell(self,code):realcode=self._codewrap.format(code)result=self._get_url('compute',self._encode({'code':realcode}))ifresult['status']=='computing':cell=result['cell_id']whileresult['status']=='computing':sys.stdout.write('working...')sys.stdout.flush()time.sleep(10)result=self._get_url('status',self._encode({'cell':cell}))ifdebug:print('cell: <<<',realcode,'>>>','result: <<<',result['output'],'>>>',sep='\n')returnresultdefdo_inline(self,code):returnself._do_cell(' print(latex({0}))'.format(code))defdo_block(self,code):result=self._do_cell(code)forfninresult['files']:self._get_file(fn,result['cell_id'])returnresultdefdo_plot(self,num,code,plotdir):result=self._do_cell(' __st_plot__({0}, {1})'.format(num,code))forfninresult['files']:self._get_file(fn,result['cell_id'],os.path.join(plotdir,fn))returnresult% \end{macrocode}%WhenusingthesimpleserverAPI,it's important to log out so the%serverdoesn't accumulate idle sessions that take up lots of memory.%Wedefinea|close()|methodandusethisclasswiththe|closing|%contextmanagerthatalwayscalls|close()|onthewayout.% \begin{macrocode}defclose(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}%Nextwehavealittlepileofmiscellaneousfunctionsandvariables%thatwewanttohaveathandwhiledoingourwork.Notethatweagain%usethetracebackstringintheerror-findingregularexpression.% \begin{macrocode}defdo_plot_setup(plotdir):printc('initializing plots directory...')ifos.path.isdir(plotdir):shutil.rmtree(plotdir)os.mkdir(plotdir)returnTruedid_plot_setup=Falseplotdir='sage-plots-for-'+jobname+'.tex'deflabelline(n,s):returnr'\newlabel{@sageinline'+str(n)+'}{{'+s+'}{}{}{}{}}\n'defprintc(s):print(s,end='')sys.stdout.flush()error=re.compile("(^"+traceback_str+")|(^Syntax Error:)",re.MULTILINE)defcheck_for_error(string,line):iferror.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}%Nowlet's actually start doing stuff.% \begin{macrocode}print('Processing Sage code for {0}.tex using remote Sage server.'.format(jobname))iflogin_info_file:withopen(login_info_file,'r')asf:print('Reading login information from {0}.'.format(login_info_file))get_val=lambdax:x.split('=')[1].strip().strip('\'"')forlineinf:print(line)ifnotline.startswith('#'):ifline.startswith('server')andnotserver:server=get_val(line)ifline.startswith('username')andnotusername:username=get_val(line)ifline.startswith('password')andnotpassword:password=get_val(line)ifnotserver:server=raw_input('Enter server: ')ifnotserver.startswith('http'):server='https://'+serverifnotusername:username=raw_input('Enter username: ')ifnotpassword:fromgetpassimportgetpasspassword=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))withclosing(RemoteSage(server,username,password))assage:print('done.')forcmdincmds:ifcmd['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.')ifcmd['type']=='block':printc('Code block begin...')result=sage.do_block(cmd['code'])check_for_error(result['output'],cmd['goboom'])print('end.')ifcmd['type']=='plot':printc('Plot {0}...'.format(cmd['num']))ifnotdid_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.')ifcmd['type']=='pause':print(cmd['msg'])ifint(time.time())%2280==0:printc('Unscheduled offworld activation; closing iris...')time.sleep(1)print('end.')withopen(jobname+'.sage','r')assagef:h=hashlib.md5()forlineinsagef:if(notline.startswith(' _st_.goboom')andnotline.startswith("print 'SageT")):h.update(line)% \end{macrocode}%Puttingthe|{1}|inthestring,justtoreplaceitwith|%|,seemsa%bitweird,butifIputasinglepercentsignthere,Docstripwon't%putthatlineintotheresulting|.py|file---andifIputtwopercent%signs,itreplacesthemwith|\MetaPrefix|whichis|##| when this%fileisgenerated.Thisisaquickandeasyworkaround.% \begin{macrocode}sout+="""%{0}% md5sum of corresponding .sage file{1} (minus "goboom" and pause/unpause lines)""".format(h.hexdigest(),'%')printc('Writing .sout file...')withopen(jobname+'.sout','w')assoutf:soutf.write(sout)print('done.')print('Sage processing complete. Run LaTeX on {0}.tex again.'.format(jobname))% \end{macrocode}% \endinput%</remotesagetex>%LocalVariables:%mode:doctex%TeX-master:"sagetex"%End: