PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/src/sage/server/simple/twist.py

https://github.com/certik/spd_notebook
Python | 324 lines | 287 code | 15 blank | 22 comment | 21 complexity | b5f56710509e3a8433f7622a6660c676 MD5 | raw file
  1. r"""nodoctest
  2. This module provides a very simple API for interacting with a Sage session
  3. over http. It runs in as part of the notebook server.
  4. NOTE:
  5. The exact data in the JSON header may vary over time (for example, further
  6. data may be added), but should remain backwards compatable if it is being
  7. parsed as JSON data.
  8. TESTS:
  9. Start the notebook.
  10. sage: from sage.server.misc import find_next_available_port
  11. sage: port = find_next_available_port(9000, verbose=False)
  12. sage: from sage.server.notebook.notebook_object import test_notebook
  13. sage: passwd = str(randint(1,1<<128))
  14. sage: nb = test_notebook(passwd, secure=False, address='localhost', port=port, verbose=True) #doctest: +ELLIPSIS
  15. ...
  16. Notebook started.
  17. Import urllib:
  18. sage: import urllib, re
  19. sage: def get_url(url): h = urllib.urlopen(url); data = h.read(); h.close(); return data
  20. Login to a new session:
  21. sage: sleep(1)
  22. sage: login_page = get_url('http://localhost:%s/simple/login?username=admin&password=%s' % (port, passwd))
  23. sage: print login_page # random session id
  24. {
  25. "session": "2afee978c09b3d666c88b9b845c69608"
  26. }
  27. ___S_A_G_E___
  28. sage: session = re.match(r'.*"session": "([^"]*)"', login_page, re.DOTALL).groups()[0]
  29. Run a command:
  30. sage: sleep(0.5)
  31. sage: print get_url('http://localhost:%s/simple/compute?session=%s&code=2*2' % (port, session))
  32. {
  33. "status": "done",
  34. "files": [],
  35. "cell_id": 1
  36. }
  37. ___S_A_G_E___
  38. 4
  39. Do a longer-running example:
  40. sage: n = next_prime(10^25)*next_prime(10^30)
  41. sage: print get_url('http://localhost:%s/simple/compute?session=%s&code=factor(%s)&timeout=0.1' % (port, session, n))
  42. {
  43. "status": "computing",
  44. "files": [],
  45. "cell_id": 2
  46. }
  47. ___S_A_G_E___
  48. Get the status of the computation:
  49. sage: print get_url('http://localhost:%s/simple/status?session=%s&cell=2' % (port, session))
  50. {
  51. "status": "computing",
  52. "files": [],
  53. "cell_id": 2
  54. }
  55. ___S_A_G_E___
  56. Interrupt the computation:
  57. sage: _ = get_url('http://localhost:%s/simple/interrupt?session=%s' % (port, session))
  58. Test out getting files:
  59. sage: code = "h = open('a.txt', 'w'); h.write('test'); h.close()"
  60. sage: print get_url('http://localhost:%s/simple/compute?session=%s&code=%s' % (port, session, urllib.quote(code)))
  61. {
  62. "status": "done",
  63. "files": ["a.txt"],
  64. "cell_id": 3
  65. }
  66. ___S_A_G_E___
  67. sage: print get_url('http://localhost:%s/simple/file?session=%s&cell=3&file=a.txt' % (port, session))
  68. test
  69. Log out:
  70. sage: _ = get_url('http://localhost:%s/simple/logout?session=%s' % (port, session))
  71. sage: nb.dispose()
  72. """
  73. #############################################################################
  74. # Copyright (C) 2007 Robert Bradshaw <robertwb@math.washington.edu>
  75. # Distributed under the terms of the GNU General Public License (GPL)
  76. # The full text of the GPL is available at:
  77. # http://www.gnu.org/licenses/
  78. #############################################################################
  79. import re, random, os.path, shutil, time
  80. from twisted.internet.task import LoopingCall
  81. from twisted.python import log
  82. from twisted.internet import defer, reactor
  83. from twisted.cred import credentials
  84. from twisted.web2 import server, http, resource, channel
  85. from twisted.web2 import static, http_headers, responsecode
  86. sessions = {}
  87. # There must be a better way to avoid circular imports...
  88. late_import_done = False
  89. def late_import():
  90. global SEP, notebook_twist, late_import_done
  91. if not late_import_done:
  92. from sage.server.notebook.twist import SEP
  93. import sage.server.notebook.twist as notebook_twist
  94. late_import_done = True
  95. def simple_jsonize(data):
  96. """
  97. This may be replaced by a JSON spkg."
  98. EXAMPLES:
  99. sage: from sage.server.simple.twist import simple_jsonize
  100. sage: print simple_jsonize({'a': [1,2,3], 'b': "yep"})
  101. { "a": [1, 2, 3], "b": "yep" }
  102. """
  103. if isinstance(data, dict):
  104. values = ['"%s": %s' % (key, simple_jsonize(value)) for key, value in data.iteritems()]
  105. return "{\n%s\n}" % ',\n'.join(values)
  106. elif isinstance(data, (list, tuple)):
  107. values = [simple_jsonize(value) for value in data]
  108. return "[%s]" % ",\n".join(values)
  109. elif isinstance(data, bool):
  110. return str(data).lower()
  111. elif data is None:
  112. return "null"
  113. else:
  114. value = str(data)
  115. if re.match(r'^\d+(\.\d*)?$', value):
  116. return value
  117. else:
  118. return '"%s"' % value
  119. class SessionObject:
  120. def __init__(self, id, username, worksheet, timeout=5):
  121. self.id = id
  122. self.username = username
  123. self.worksheet = worksheet
  124. self.default_timeout = timeout
  125. def get_status(self):
  126. """
  127. Return a dictionary to be returned (in JSON format) representing
  128. the status of self.
  129. TEST:
  130. sage: from sage.server.simple.twist import SessionObject
  131. sage: s = SessionObject(id=1, username=None, worksheet=None)
  132. sage: s.get_status()
  133. {'session': 1}
  134. """
  135. return {
  136. 'session': self.id
  137. }
  138. class LoginResource(resource.Resource):
  139. def render(self, ctx):
  140. late_import()
  141. try:
  142. username = ctx.args['username'][0]
  143. password = ctx.args['password'][0]
  144. U = notebook_twist.notebook.user(username)
  145. if not U.password_is(password):
  146. raise ValueError # want to return the same error message
  147. except KeyError, ValueError:
  148. return http.Response(stream = "Invalid login.")
  149. worksheet = notebook_twist.notebook.create_new_worksheet("_simple_session_", username, add_to_list=False)
  150. worksheet.sage() # create the sage session
  151. worksheet.initialize_sage()
  152. # is this a secure enough random number?
  153. session_id = "%x" % random.randint(1, 1 << 128)
  154. session = SessionObject(session_id, username, worksheet)
  155. sessions[session_id] = session
  156. sessions['test'] = session
  157. status = session.get_status()
  158. return http.Response(stream = "%s\n%s\n" % (simple_jsonize(status), SEP))
  159. class LogoutResource(resource.Resource):
  160. def render(self, ctx):
  161. try:
  162. session = sessions[ctx.args['session'][0]]
  163. except KeyError:
  164. return http.Response(stream = "Invalid session.")
  165. session.worksheet.quit()
  166. shutil.rmtree(session.worksheet.directory())
  167. status = session.get_status()
  168. return http.Response(stream = "%s\n%s\n" % (simple_jsonize(status), SEP))
  169. class InterruptResource(resource.Resource):
  170. def render(self, ctx):
  171. try:
  172. session = sessions[ctx.args['session'][0]]
  173. except KeyError:
  174. return http.Response(stream = "Invalid session.")
  175. session.worksheet.interrupt()
  176. status = session.get_status()
  177. return http.Response(stream = "%s\n%s\n" % (simple_jsonize(status), SEP))
  178. class RestartResource(resource.Resource):
  179. def render(self, ctx):
  180. try:
  181. session = sessions[ctx.args['session'][0]]
  182. except KeyError:
  183. return http.Response(stream = "Invalid session.")
  184. session.worksheet.restart_sage()
  185. status = session.get_status()
  186. return http.Response(stream = "%s\n%s\n" % (simple_jsonize(status), SEP))
  187. class CellResource(resource.Resource):
  188. def start_comp(self, cell, timeout):
  189. start_time = time.time()
  190. looper_list = []
  191. looper = LoopingCall(self.check_comp, cell, start_time, timeout, looper_list)
  192. looper_list.append(looper) # so check_comp has access
  193. looper.cell = cell # to pass it on
  194. d = looper.start(0.25, now=True)
  195. d.addCallback(self.render_cell_result)
  196. return d
  197. def check_comp(self, cell, start_time, timeout, looper_list):
  198. cell.worksheet().check_comp(wait=0.01) # don't want to block, delay handled by twisted
  199. if not cell.computing() or time.time() - start_time > timeout:
  200. looper_list[0].stop()
  201. def render_cell_result(self, looper):
  202. cell = looper.cell
  203. if cell.interrupted():
  204. cell_status = 'interrupted'
  205. elif cell.computing():
  206. cell_status = 'computing'
  207. else:
  208. cell_status = 'done'
  209. status = { 'cell_id': cell.id(), 'status': cell_status, 'files': cell.files() }
  210. result = cell.output_text(raw=True)
  211. return http.Response(stream = "\n".join([simple_jsonize(status), SEP, result]))
  212. class ComputeResource(CellResource):
  213. def render(self, ctx):
  214. try:
  215. session = sessions[ctx.args['session'][0]]
  216. except KeyError:
  217. return http.Response(stream = "Invalid session.")
  218. try:
  219. timeout = float(ctx.args['timeout'][0])
  220. except (KeyError, ValueError), msg:
  221. timeout = session.default_timeout
  222. cell = session.worksheet.append_new_cell()
  223. cell.set_input_text(ctx.args['code'][0])
  224. cell.evaluate(username = session.username)
  225. return self.start_comp(cell, timeout)
  226. class StatusResource(CellResource):
  227. def render(self, ctx):
  228. try:
  229. session = sessions[ctx.args['session'][0]]
  230. except KeyError:
  231. return http.Response(stream = "Invalid session.")
  232. try:
  233. cell_id = int(ctx.args['cell'][0])
  234. cell = session.worksheet.get_cell_with_id(cell_id)
  235. try:
  236. timeout = float(ctx.args['timeout'][0])
  237. except KeyError, ValueError:
  238. timeout = -1
  239. return self.start_comp(cell, timeout)
  240. except KeyError:
  241. status = session.get_status()
  242. return http.Response(stream = "%s\n%s\n" % (simple_jsonize(status), SEP))
  243. class FileResource(resource.Resource):
  244. """
  245. This differs from the rest as it does not print a header, just the raw file data.
  246. """
  247. def render(self, ctx):
  248. try:
  249. session = sessions[ctx.args['session'][0]]
  250. except KeyError:
  251. return http.Response(code=404, stream = "Invalid session.")
  252. try:
  253. cell_id = int(ctx.args['cell'][0])
  254. file_name = ctx.args['file'][0]
  255. except KeyError:
  256. return http.Response(code=404, stream = "Unspecified cell or file.")
  257. cell = session.worksheet.get_cell_with_id(cell_id)
  258. if file_name in cell.files():
  259. return static.File("%s/%s" % (cell.directory(), file_name))
  260. else:
  261. return http.Response(code=404, stream = "No such file %s in cell %s." % (file_name, cell_id))
  262. class SimpleServer(resource.Resource):
  263. child_login = LoginResource()
  264. child_logout = LogoutResource()
  265. child_interrupt = InterruptResource()
  266. child_restart = RestartResource()
  267. child_compute = ComputeResource()
  268. child_status = StatusResource()
  269. child_file = FileResource()
  270. def render(self, ctx):
  271. return http.Response(stream="Please login.")