Skip to content

Commit edb91ad

Browse files
authored
Merge pull request #166 from sympy/refactor-handler-ndb
Refactor shell into handlers and ndb modules
2 parents 2605734 + 7922c9e commit edb91ad

File tree

4 files changed

+369
-359
lines changed

4 files changed

+369
-359
lines changed

app/entrypoint.py

+17-22
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
1+
import six
2+
# https://github.com/googleapis/python-ndb/issues/249#issuecomment-560957294
3+
six.moves.reload_module(six)
4+
15
from google.appengine.ext import webapp
2-
from app.shell import (
3-
FrontPageHandler,
4-
EvaluateHandler,
5-
ForceDesktopCookieHandler,
6-
DeleteHistory,
7-
CompletionHandler,
8-
SphinxBannerHandler,
9-
RedirectHandler,
10-
StatusHandler,
11-
_DEBUG
12-
)
6+
from app import handlers
7+
138

149
application = webapp.WSGIApplication([
15-
('/', FrontPageHandler),
16-
('/evaluate', EvaluateHandler),
17-
('/forcedesktop', ForceDesktopCookieHandler),
18-
('/delete', DeleteHistory),
19-
('/complete', CompletionHandler),
20-
('/sphinxbanner', SphinxBannerHandler),
21-
('/shellmobile', RedirectHandler),
22-
('/shelldsi', RedirectHandler),
23-
('/helpdsi', RedirectHandler),
24-
('/status', StatusHandler),
25-
], debug=_DEBUG)
10+
('/', handlers.FrontPageHandler),
11+
('/evaluate', handlers.EvaluateHandler),
12+
('/forcedesktop', handlers.ForceDesktopCookieHandler),
13+
('/delete', handlers.DeleteHistory),
14+
('/complete', handlers.CompletionHandler),
15+
('/sphinxbanner', handlers.SphinxBannerHandler),
16+
('/shellmobile', handlers.RedirectHandler),
17+
('/shelldsi', handlers.RedirectHandler),
18+
('/helpdsi', handlers.RedirectHandler),
19+
('/status', handlers.StatusHandler),
20+
], debug=handlers._DEBUG)

app/handlers.py

+345
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
import json
2+
import os
3+
import datetime
4+
import sys
5+
import traceback
6+
7+
from StringIO import StringIO
8+
9+
from google.appengine.ext.webapp import template
10+
from google.appengine.ext import webapp
11+
from google.appengine.api import users
12+
13+
from google.appengine.runtime import DeadlineExceededError
14+
15+
from google.cloud import ndb
16+
17+
from .models import Session, Searches
18+
from .ndb import ndb_client
19+
from .shell import Live
20+
21+
import settings
22+
23+
from app.constants import (
24+
INITIAL_UNPICKLABLES,
25+
PREEXEC,
26+
PREEXEC_INTERNAL,
27+
PREEXEC_MESSAGE,
28+
VERBOSE_MESSAGE,
29+
VERBOSE_MESSAGE_SPHINX,
30+
PRINTERS
31+
)
32+
33+
34+
LIVE_VERSION, LIVE_DEPLOYED = os.environ['CURRENT_VERSION_ID'].split('.')
35+
LIVE_DEPLOYED = datetime.datetime.fromtimestamp(long(LIVE_DEPLOYED) / pow(2, 28))
36+
LIVE_DEPLOYED = LIVE_DEPLOYED.strftime("%d/%m/%y %X")
37+
_DEBUG = True
38+
39+
40+
def banner(quiet=False):
41+
from sympy import __version__ as sympy_version
42+
python_version = "%d.%d.%d" % sys.version_info[:3]
43+
44+
message = "Python console for SymPy %s (Python %s)\n" % (sympy_version, python_version)
45+
46+
if not quiet:
47+
source = ""
48+
49+
for line in PREEXEC_MESSAGE.split('\n')[:-1]:
50+
if not line:
51+
source += '\n'
52+
else:
53+
source += '>>> ' + line + '\n'
54+
55+
docs_version = sympy_version
56+
if 'git' in sympy_version or '.rc' in sympy_version:
57+
docs_version = 'dev'
58+
59+
message += '\n' + VERBOSE_MESSAGE % {
60+
'source': source,
61+
'version': docs_version
62+
}
63+
64+
return message
65+
66+
67+
def banner_sphinx(quiet=False):
68+
from sympy import __version__ as sympy_version
69+
python_version = "%d.%d.%d" % sys.version_info[:3]
70+
71+
message = "Python console for SymPy %s (Python %s)\n" % (sympy_version, python_version)
72+
73+
if not quiet:
74+
source = ""
75+
76+
for line in PREEXEC_MESSAGE.split('\n')[:-1]:
77+
if not line:
78+
source += '\n'
79+
else:
80+
source += '>>> ' + line + '\n'
81+
82+
message += '\n' + VERBOSE_MESSAGE_SPHINX % {'source': source}
83+
84+
return message
85+
86+
87+
class ForceDesktopCookieHandler(webapp.RequestHandler):
88+
def get(self):
89+
#Cookie stuff
90+
import Cookie
91+
import datetime
92+
93+
expiration = datetime.datetime.now() + datetime.timedelta(days=1000)
94+
cookie = Cookie.SimpleCookie()
95+
cookie["desktop"] = "yes"
96+
#cookie["desktop"]["domain"] = "live.sympy.org"
97+
cookie["desktop"]["path"] = "/"
98+
cookie["desktop"]["expires"] = \
99+
expiration.strftime("%a, %d-%b-%Y %H:%M:%S PST")
100+
print cookie.output()
101+
template_file = os.path.join(os.path.dirname(__file__), '../templates', 'redirect.html')
102+
vars = { 'server_software': os.environ['SERVER_SOFTWARE'],
103+
'python_version': sys.version,
104+
'user': users.get_current_user(),
105+
}
106+
rendered = webapp.template.render(template_file, vars, debug=_DEBUG)
107+
self.response.out.write(rendered)
108+
109+
110+
class FrontPageHandler(webapp.RequestHandler):
111+
"""Creates a new session and renders the ``shell.html`` template. """
112+
113+
def get(self):
114+
#Get the 10 most recent queries
115+
with ndb_client.context():
116+
searches_query = Searches.query_(Searches.private == False).order(-Searches.timestamp)
117+
search_results = [result.query for result in searches_query.fetch(10)]
118+
user = users.get_current_user()
119+
if user:
120+
_saved_searches = Searches.query_(Searches.user_id == user.user_id()).order(-Searches.timestamp).fetch()
121+
saved_searches = [search.query for search in _saved_searches]
122+
else:
123+
saved_searches = []
124+
saved_searches_count = len(saved_searches)
125+
template_file = os.path.join(os.path.dirname(__file__), '../templates', 'shell.html')
126+
127+
vars = {
128+
'server_software': os.environ['SERVER_SOFTWARE'],
129+
'application_version': LIVE_VERSION,
130+
'current_year': datetime.datetime.utcnow().year,
131+
'date_deployed': LIVE_DEPLOYED,
132+
'python_version': sys.version,
133+
'user': users.get_current_user(),
134+
'login_url': users.create_login_url('/'),
135+
'logout_url': users.create_logout_url('/'),
136+
'banner': banner(),
137+
'printer': self.request.get('printer').lower() or '',
138+
'submit': self.request.get('submit').lower() or '',
139+
'tabWidth': self.request.get('tabWidth').lower() or 'undefined',
140+
'searches': search_results,
141+
'has_searches': bool(search_results),
142+
'saved_searches': saved_searches,
143+
'has_saved_searches': saved_searches_count
144+
}
145+
146+
rendered = webapp.template.render(template_file, vars, debug=_DEBUG)
147+
self.response.out.write(rendered)
148+
149+
150+
class CompletionHandler(webapp.RequestHandler):
151+
"""Takes an incomplete statement and returns possible completions."""
152+
153+
def _cross_site_headers(self):
154+
self.response.headers['Access-Control-Allow-Origin'] = '*'
155+
self.response.headers['Access-Control-Allow-Headers'] = 'Content-Type, X-Requested-With'
156+
157+
def options(self):
158+
self._cross_site_headers()
159+
160+
def post(self):
161+
self._cross_site_headers()
162+
try:
163+
message = json.loads(self.request.body)
164+
except ValueError:
165+
self.error(400)
166+
return
167+
168+
session_key = message.get('session')
169+
statement = message.get('statement').encode('utf-8')
170+
live = Live()
171+
172+
if session_key is not None:
173+
try:
174+
with ndb_client.context():
175+
session = ndb.Key(urlsafe=session_key).get()
176+
except ndb.exceptions.Error:
177+
self.error(400)
178+
return
179+
else:
180+
with ndb_client.context():
181+
session = Session()
182+
session.unpicklables = [line for line in INITIAL_UNPICKLABLES]
183+
session_key = session.put().urlsafe()
184+
185+
live.evaluate(PREEXEC, session)
186+
live.evaluate(PREEXEC_INTERNAL, session)
187+
188+
completions = list(sorted(set(live.complete(statement, session))))
189+
if not statement.split('.')[-1].startswith('_'):
190+
completions = [x for x in completions if
191+
not x.split('.')[-1].startswith('_')]
192+
193+
# From http://stackoverflow.com/a/1916632
194+
# Get longest common prefix to fill instantly
195+
common = os.path.commonprefix(completions)
196+
197+
result = {
198+
'session': str(session_key),
199+
'completions': completions,
200+
'prefix': common
201+
}
202+
203+
self.response.headers['Content-Type'] = 'application/json'
204+
self.response.out.write(json.dumps(result))
205+
206+
207+
class EvaluateHandler(webapp.RequestHandler):
208+
"""Evaluates a Python statement in a given session and returns the result. """
209+
210+
def _cross_site_headers(self):
211+
self.response.headers['Access-Control-Allow-Origin'] = '*'
212+
self.response.headers['Access-Control-Allow-Headers'] = 'Content-Type, X-Requested-With'
213+
214+
def options(self):
215+
self._cross_site_headers()
216+
217+
def post(self):
218+
self._cross_site_headers()
219+
220+
try:
221+
message = json.loads(self.request.body)
222+
except ValueError:
223+
self.error(400)
224+
return
225+
226+
# Code modified to store each query in a database
227+
print_statement = '\n'.join(message.get('print_statement'))
228+
statement = message.get('statement')
229+
privacy = message.get('privacy')
230+
231+
with ndb_client.context():
232+
if statement != '':
233+
user = users.get_current_user()
234+
235+
searches = Searches()
236+
searches.user_id = user.user_id() if user else None
237+
searches.query = print_statement
238+
239+
if privacy == 'off': searches.private = False
240+
if privacy == 'on': searches.private = True
241+
242+
searches.put()
243+
244+
session_key = message.get('session')
245+
printer_key = message.get('printer')
246+
live = Live()
247+
248+
if session_key is not None:
249+
try:
250+
with ndb_client.context():
251+
session = ndb.Key(urlsafe=session_key).get()
252+
except ndb.exceptions.Error:
253+
self.error(400)
254+
return
255+
else:
256+
with ndb_client.context():
257+
session = Session()
258+
session.unpicklables = [line for line in INITIAL_UNPICKLABLES]
259+
session_key = session.put().urlsafe()
260+
261+
live.evaluate(PREEXEC, session)
262+
live.evaluate(PREEXEC_INTERNAL, session)
263+
264+
try:
265+
printer = PRINTERS[printer_key]
266+
except KeyError:
267+
printer = None
268+
269+
stream = StringIO()
270+
try:
271+
live.evaluate(statement, session, printer, stream)
272+
result = {
273+
'session': str(session_key),
274+
'output': stream.getvalue(),
275+
}
276+
except DeadlineExceededError:
277+
result = {
278+
'session': str(session_key),
279+
'output': 'Error: Operation timed out.'
280+
}
281+
except Exception, e:
282+
if settings.DEBUG:
283+
errmsg = '\n'.join([
284+
'Exception in SymPy Live of type ',
285+
str(type(e)),
286+
'for reference the stack trace is',
287+
traceback.format_exc()
288+
])
289+
else:
290+
errmsg = '\n'.join([
291+
'Exception in SymPy Live of type ',
292+
str(type(e)),
293+
'for reference the last 5 stack trace entries are',
294+
traceback.format_exc(5)
295+
])
296+
result = {
297+
'session': str(session_key),
298+
'output': errmsg
299+
}
300+
301+
self.response.headers['Content-Type'] = 'application/json'
302+
self.response.out.write(json.dumps(result))
303+
304+
305+
class SphinxBannerHandler(webapp.RequestHandler):
306+
"""Provides the banner for the Sphinx extension.
307+
"""
308+
309+
def _cross_site_headers(self):
310+
self.response.headers['Access-Control-Allow-Origin'] = '*'
311+
self.response.headers['Access-Control-Allow-Headers'] = 'Content-Type, X-Requested-With'
312+
313+
def get(self):
314+
self._cross_site_headers()
315+
self.response.headers['Content-Type'] = 'text/plain'
316+
self.response.out.write(banner_sphinx())
317+
318+
319+
class DeleteHistory(webapp.RequestHandler):
320+
"""Deletes all of the user's history"""
321+
322+
def get(self):
323+
with ndb_client.context():
324+
user = users.get_current_user()
325+
results = Searches.query_(Searches.user_id == user.user_id()).order(-Searches.timestamp)
326+
327+
for result in results:
328+
result.key.delete()
329+
330+
self.response.out.write("Your queries have been deleted.")
331+
332+
333+
class RedirectHandler(webapp.RedirectHandler):
334+
"""Redirects deprecated pages to the frontpage."""
335+
336+
def get(self):
337+
self.redirect('/', permanent=True)
338+
339+
340+
class StatusHandler(webapp.RequestHandler):
341+
"""Status endpoint to check if the app is running or not."""
342+
343+
def get(self):
344+
self.response.headers['Content-Type'] = 'application/json'
345+
self.response.out.write(json.dumps({"status": "ok"}))

app/ndb.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import os
2+
3+
from google.cloud import ndb
4+
5+
ndb_client = ndb.Client(project=os.environ['PROJECT_ID'])

0 commit comments

Comments
 (0)