I've been trying to create a web service using Tornado Framework. The system is supposed to handle URLs such as the following:
IP:Port/?user=1822&catid=48&skus=AB1,FS35S,98KSU1
First I've created this code to read urls:
#!/usr/bin/env python
from datetime import date
import tornado.httpserver
import tornado.escape
import tornado.ioloop
import tornado.web
class WService(tornado.web.RequestHandler):
def get(self, url):
self.write("value of url: %s" %(url))
application = tornado.web.Application([
(r"/([^/]+)", WService)])
if __name__ == "__main__":
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(9000)
tornado.ioloop.IOLoop.instance().start()
and entering the url:
IP:Port/hello_world
resulted in:
value of url: hello_world
Any character used in the URL works, except for the "?". When changing the code such as:
application = tornado.web.Application([
(r"/?([^/]+)", WService)])
and sent the url with the "?" mark (IP:Port/?hello_world) the result is:
404: Not Found
Researching about Tornado to solve this problem I found the get_argument method and tried to apply it, such as:
class WService2(tornado.web.RequestHandler):
def get(self):
user = self.get_argument('user', None)
respose = { 'user': user }
self.write(response)
and
application = tornado.web.Application([
(r"/", WService2),
])
But sending the URL IP:Port/user=5 returned:
404: Not Found
I also tried:
application = tornado.web.Application([
(r"/(\w+)", WService2),
])
And also:
application = tornado.web.Application([
(r"/([^/]+)", WService2),
])
and nothing worked.
Am I doing something wrong to read the URLs with the "?" marks and is there a reason why Tornado is not reading URLs with parameters such as the user one? Is there something missing?
I've updated Tornado to the lastest version but it didn't work as well.
If you need more information or something is not clear please let me know.
Thanks in advance,
The reason that you aren't seeing the entire string is that Tornado has already parsed it and placed it into a request object. If you want to see the entire path including the query string, then take a look at request.uri.
In your example, if you were to go to http://localhost:9000/hello_world?x=10&y=33 then request.uri would be set to /hello_world?x=10&y=33.
However, as you state, you are better off using get_argument (or get_arguments) to examine the arguments. Here's your example, augmented to show how you can read them.
Try going to http://localhost:9000/distance?x1=0&y1=0&x2=100&y2=100 and see what you get back.
Similarly, try, http://localhost:9000/?user=1822&catid=48&skus=AB1,FS35S,98KSU1. This should now work as you expected.
#!/usr/bin/env python
import math
import tornado.httpserver
import tornado.escape
import tornado.ioloop
import tornado.web
class DistanceService(tornado.web.RequestHandler):
def get(self):
x1 = float(self.get_argument('x1'))
y1 = float(self.get_argument('y1'))
x2 = float(self.get_argument('x2'))
y2 = float(self.get_argument('y2'))
dx = x1 - x2
dy = y1 - y2
magnitude = math.sqrt(dx * dx + dy * dy)
self.write("the distance between the points is %3.2f" % magnitude)
class WService(tornado.web.RequestHandler):
def get(self):
self.write("value of request.uri: %s" % self.request.uri)
self.write("<br>")
self.write("value of request.path: %s" % self.request.path)
self.write("<br>")
self.write("value of request.query: %s" % self.request.query)
application = tornado.web.Application([
(r"/distance", DistanceService),
(r"/", WService),
])
if __name__ == "__main__":
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(9000)
tornado.ioloop.IOLoop.instance().start()
Related
Let's say I have a very simple web app in python Tornado framework with a single endpoint. All I'm interested in is returning a value calculated before starting the server. Slightly modified example from https://www.tornadoweb.org/en/stable/index.html will do just fine.
handler.py
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write('I want to return var `expensive_value`')
main.py
import tornado.ioloop
import tornado.web
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
# calculate some var here before starting the server
expensive_value = 'value from long_calculation()'
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
When running python main.py and sending a request to the endpoint it returns only a string of course. But I'd like to return the actual value of expensive_value. Currently I'm aware of two solutions to the problem.
1. Using global variable in handler
handler.py
import tornado.web
global_variable = None
def setter(val):
global global_variable
global_variable = val
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write(global_variable)
main.py
import tornado.ioloop
import tornado.web
from handler import MainHandler, setter
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
expensive_value = 'value from long_calculation()'
setter(expensive_value)
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
Having a global var and setting its value from some other module sounds like an antipattern to me.
2. Using initialize method in handler
handler.py
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def initialize(self, expensive_value):
self.expensive_value = expensive_value
def get(self):
self.write(self.expensive_value)
main.py
import tornado.ioloop
import tornado.web
from handler import MainHandler
def make_app(parameter):
return tornado.web.Application([
(r"/", MainHandler, {'expensive_value': parameter}),
])
if __name__ == "__main__":
expensive_value = 'value from long_calculation()'
app = make_app(expensive_value)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
This solution is better. But initialize method is called for every request. I realize the overhead for that would be rather small but I think it might be misleading for potential reader of the code since expensive_value never changes.
Summary
Both of these solution work. But I don't like any of them and it seems like I'm missing some Tornado functionality. What would be a pythonic way to solve this?
For example I believe Flask has app.config dictionary that is accessible in handlers and it seems to be a nice solution to this as expensive_value is indeed a configuration to the app. But I'm not aware of anything similar in Tornado.
Handlers have access to self.application.settings which is a dictionary containing additional arguments passed to the Application constructor.
So you can pass expensive_value directly to the Application class like this:
def make_app(parameter):
return tornado.web.Application(
[
(r"/", MainHandler),
],
expensive_value=parameter
)
And access this value in any handler like this:
def initialize(self):
self.expensive_value = self.application.settings.get('expensive_value')
I have following code:
application = tornado.web.Application([
(r"/get(.*)", GetHandler),
])
class GetHandler(tornado.web.RequestHandler):
def get(self, key):
response = {'key': key}
self.write(response)
when I go to localhost:port/get?key=python I receive empty key value {'key': ''}. What is wrong here?
(.*) in regex matches everything. So this — (r"/get(.*)", GetHandler) — will match anything followed by /get, example:
/get
/getsomething
/get/something
/get.asldfkj%5E&(*&fkasljf
Let's say a request comes in at localhost:port/get/something, then the value of key argument in GetHandler.get(self, key) will be /something (yes, including the slash because .* matches everything).
But if a request comes in at localhost:port/get?key=python, the value of key argument in GETHandler.get(self, key) will be an empty string. It happens because the part containing ?key=python is called a Query String. It's not part of the url path. Tornado (or almost every other web framework) doesn't pass this to the view as an argument.
There are two ways you can change your code:
If you want to access your view like this - localhost:port/get?key=python, you'll need to make changes to your url config and to your view:
application = tornado.web.Application([
(r"/get", GetHandler),
])
class GetHandler(tornado.web.RequestHandler):
def get(self):
key = self.get_argument('key', None)
response = {'key': key}
self.write(response)
If you don't want to change your app url config and your view, you'll need to make the request like this - localhost:port/get/python.
But still you'll need to make a small change to your url config. Add a slash - / - between get and (.*), because otherwise the value of key would be /python instead of python.
application = tornado.web.Application([
(r"/get/(.*)", GetHandler), # note the slash
])
I hope that you will figure what you did wrong by yourself - that's a task for you.
Your working code:
import tornado.ioloop
import tornado.web
class GetHandler(tornado.web.RequestHandler):
def get(self):
response = self.get_arguments("key")
if len(response) == 0:
# Handle me
self.set_status(400)
return self.finish("Invalid key")
self.write({"key":self.get_argument("key")})
def make_app():
return tornado.web.Application([
(r"/", GetHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
So I'm learning the Tornado web framework right now, and following a few examples from a book I managed to get authentication with OAuth2/Google almost working. It redirects to Google to prompt a user to sign in, but after signing in it throws the following error at me:
Traceback (most recent call last):
File "C:\python27\lib\site-packages\tornado\web.py", line 1288, in _stack_context_handle_exception
raise_exc_info((type, value, traceback))
File "C:\python27\lib\site-packages\tornado\web.py", line 1475, in wrapper
result = method(self, *args, **kwargs)
File "C:\Users\enricojr\Downloads\Github\LearningTornado\grumble_login.py", line 8, in get
self.get_authenticated_user(callback=self.async_callback(self._on_auth))
AttributeError: 'LoginHandler' object has no attribute 'async_callback'
Upon checking the Tornado source code on Github, it does appear indeed that the async_callback() method is gone. Any idea on how to handle this? Every example of authentication using Tornado's auth module I've seen thus far calls for it to be used, and since I'm new to the whole 'asynchronous web programming' thing I'm not quite sure at this point how else I can do it.
My code is below, Python 2.7 and Tornado 4.0.2:
############################
# grumble_login.py
############################
from tornado.auth import GoogleMixin
from tornado.web import RequestHandler, asynchronous
class LoginHandler(RequestHandler, GoogleMixin):
#asynchronous
def get(self):
if self.get_argument("openid.mode", None):
self.get_authenticated_user(callback=self.async_callback(self._on_auth))
return
else:
self.authenticate_redirect()
def _on_auth(self, user):
if not user:
self.clear_all_cookies()
raise tornado.web.HTTPError(500, "Google auth failed.")
else:
# the user id serves as the basis for
# all the data we store.
self.set_secure_cookie('user_id', user['id'])
self.redirect("/")
class LogoutHandler(RequestHandler):
def get(self):
self.clear_all_cookies()
self.render("logout.html")
############################
# main.py
############################
import os
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.web import Application
from tornado.options import define, options
from grumble_handlers import *
from grumble_user_handlers import *
from grumble_login import *
# /user/profile/<user> - leads to profile page
# /user/posts - leads to posts made by user
# /post/<id> - leads to a specific post
# /post/add - add post
# /post/edit - edit post
# /post/delete - delete post
# D$oP5lz3D$oP5lz3
TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "templates")
STATIC_PATH = os.path.join(os.path.dirname(__file__), "static")
if __name__ == "__main__":
define("port", default=8000, help="run on the given port", type=int)
settings_dict = {
'cookie_secret': "GENERATE NEW KEY HERE",
'template_path': TEMPLATE_PATH,
'static_path': STATIC_PATH,
'debug': True,
'login_url': "/login",
}
general_handlers = [
(r"/", IndexHandler),
(r"/login", LoginHandler),
(r"/logout", LogoutHandler)
]
post_handler_list =[
(r"/post/(\d+)", PostFetchHandler), # leads to a single post page
(r"/post/add", PostAddHandler),
(r"/post/edit", PostEditHandler),
(r"/post/delete", PostDeleteHandler),
]
user_handler_list = [
(r"/user/(\d+)/posts", UserPostsHandler), # lists out all the posts by a given user
(r"/user/(\d+)/profile", UserProfileHandler),
]
master_handler_list = general_handlers + post_handler_list + user_handler_list
app = Application(
handlers=master_handler_list,
**settings_dict
)
http_server = HTTPServer(app)
http_server.listen(options.port)
# GO TEAM GO
IOLoop.instance().start()
############################
# grumble_handlers.py
############################
...
class IndexHandler(NotImplementedHandler):
#authenticated
def get(self):
pass
The book is, unfortunately, long outdated. async_callback was removed in Tornado 4 since it is no longer required. You can simply do:
self.get_authenticated_user(callback=self._on_auth)
Once you have that working, I recommend following the coroutine documents and learn how to use "yield":
user = yield self.get_authenticated_user()
I'm trying to make bottle.py work with repoze.who, and so far have managed to put together the following very simplistic program to get it working, using a combination of various examples I've found. Obviously this isn't something I'd run in production, I'm just trying to make the least complicated code I can so that I can learn how to use this - but unfortunately, tutorials for using bottle.py with repoze.who are very few and far between.
This example below works, and allows someone to login with username/password of admin/admin. What am I supposed to do with repoze.who to make the logout() function work? I gather there's a forget function that might be for this purpose, but I can't work out how I'm meant to call it.
Thanks.
from bottle import route, run, app, get, abort, request
from StringIO import StringIO
import repoze
from repoze.who.middleware import PluggableAuthenticationMiddleware
from repoze.who.interfaces import IIdentifier
from repoze.who.interfaces import IChallenger
from repoze.who.plugins.basicauth import BasicAuthPlugin
from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin
from repoze.who.plugins.cookie import InsecureCookiePlugin
from repoze.who.plugins.form import FormPlugin
from repoze.who.plugins.htpasswd import HTPasswdPlugin
from repoze.who.classifiers import default_request_classifier
from repoze.who.classifiers import default_challenge_decider
import logging, sys
import pprint
#route('/')
def root():
if request.environ.get('repoze.who.identity') is None:
abort(401, "Not authenticated")
return "Authenticated"
#route('/hello')
def index():
identity = request.environ.get('repoze.who.identity')
if identity == None:
abort(401, "Not authenticated")
user = identity.get('repoze.who.userid')
return '<b>Hello %s!</b>' % user
#route('/logout')
def logout():
# I have no idea what to put here
pass
io = StringIO()
salt = 'aa'
for name, password in [ ('admin', 'admin'), ('paul', 'paul') ]:
io.write('%s:%s\n' % (name, password))
io.seek(0)
def cleartext_check(password, hashed):
return password == hashed
htpasswd = HTPasswdPlugin(io, cleartext_check)
basicauth = BasicAuthPlugin('repoze.who')
auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt')
form = FormPlugin('__do_login', rememberer_name='auth_tkt')
form.classifications = { IIdentifier:['browser'],
IChallenger:['browser'] }
identifiers = [('form', form),('auth_tkt',auth_tkt),('basicauth',basicauth)]
authenticators = [('htpasswd', htpasswd)]
challengers = [('form',form), ('basicauth',basicauth)]
mdproviders = []
log_stream = None
import os
if os.environ.get('WHO_LOG'):
log_stream = sys.stdout
middleware = PluggableAuthenticationMiddleware(
app(),
identifiers,
authenticators,
challengers,
mdproviders,
default_request_classifier,
default_challenge_decider,
log_stream = log_stream,
log_level = logging.DEBUG
)
if __name__ == '__main__':
run(app=middleware, host='0.0.0.0', port=8080, reloader=True)
else:
application = middleware
run(host='0.0.0.0', port=8080)
If you can I would use RedirectingFormPlugin rather than FormPlugin. RedirectingFormPlugin allows you to register a logout URL. With it you do not have to implement the /logout handler as such as the RedirectingFormPlugin intercepts the request and handles the calls to forget etc. for you. I have used this with Bobo and appengine and it works well.
if you still want to do it the unpreferred way in old repoze.who v1, following worked for me:
from bottle import response # , redirect
# ...
#route('/logout')
def logout():
identity = request.environ.get('repoze.who.identity')
if identity:
for (i_name, i) in identifiers:
hdrs = i.forget(request.environ, identity)
[ response.add_header(*h) for h in hdrs ]
## following would be nice, but does not work,
## since redirect is not using defined response headers
# rfr = request.get_header('referer', '/')
# redirect(rfr)
## so we do just this:
return "you have been hopefully logged out"
Bottle.py ships with an import to handle throwing HTTPErrors and route to a function.
Firstly, the documentation claims I can (and so do several examples):
from bottle import error
#error(500)
def custom500(error):
return 'my custom message'
however, when importing this statement error is unresolved but on running the application ignores this and just directs me to the generic error page.
I found a way to get around this by:
from bottle import Bottle
main = Bottle()
#Bottle.error(main, 500)
def custom500(error):
return 'my custom message'
But this code prevents me from embedding my errors all in a separate module to control the nastiness that would ensue if I kept them in my main.py module because the first argument has to be a bottle instance.
So my questions:
Has anyone else experienced this?
why doesn't error seem to resolve in only my case (I installed from pip install bottle)?
Is there a seamless way to import my error routing from a separate python module into the main application?
If you want to embed your errors in another module, you could do something like this:
error.py
def custom500(error):
return 'my custom message'
handler = {
500: custom500,
}
app.py
from bottle import *
import error
app = Bottle()
app.error_handler = error.handler
#app.route('/')
def divzero():
return 1/0
run(app)
This works for me:
from bottle import error, run, route, abort
#error(500)
def custom500(error):
return 'my custom message'
#route("/")
def index():
abort("Boo!")
run()
In some cases I find it's better to subclass Bottle. Here's an example of doing that and adding a custom error handler.
#!/usr/bin/env python3
from bottle import Bottle, response, Route
class MyBottle(Bottle):
def __init__(self, *args, **kwargs):
Bottle.__init__(self, *args, **kwargs)
self.error_handler[404] = self.four04
self.add_route(Route(self, "/helloworld", "GET", self.helloworld))
def helloworld(self):
response.content_type = "text/plain"
yield "Hello, world."
def four04(self, httperror):
response.content_type = "text/plain"
yield "You're 404."
if __name__ == '__main__':
mybottle = MyBottle()
mybottle.run(host='localhost', port=8080, quiet=True, debug=True)