Info: | See github for the latest source. |
---|---|
Author: | Alexandre Fiori <[email protected]> |
cyclone is a clone of facebook's Tornado, on top of Twisted.
Although cyclone and tornado are very similar, cyclone leverages all enterprise class features of Twisted, and more. It is extremely stable, and ready for production.
cyclone is a Twisted protocol. Thus, it may be used in conjunction with any other protocol implemented in Twisted. The same server can deliver HTTP content on one port, SSH on another, and it can keep a pool of persistent, non-blocking connections to several databases. All in a single process.
Web apps built with cyclone are fully translatable. The localization system is based on Gettext. It's possible to translate strings in the server code, as well as text and HTML templates.
Secure. It can deliver HTTP and HTTPS (SSL) on the same server, with individual request routing mechanism. Also, cyclone supports the standard HTTP Authentication, which can be used to implement HTTP Basic, Digest, or any other hand crafted authentication system, like Amazon's S3.
API friendly. cyclone is very useful for writing web services, RESTful or any other type of web API. Features like HTTP Keep-Alive and XSRF can be enabled or disabled per request, which means the server can have different behaviour when communicating with browsers, or other custom HTTP clients.
Ships with a full featured, non-blocking HTTP client, using TwistedWeb.
E-mail, the easy way. With cyclone, the web server can connect to multiple SMTP servers, on demand. The e-mail API is simple, support client connections with SSL and TLS, and provide you with an easy way to customize messages, and attachments.
Supports multiple protocols: built-in support for XML-RPC, JSON-RPC, WebSocket and SSE. And, many other protocols can be used in cyclone-based web servers, like the Event Socket protocol of Freeswitch, a highly scalable soft switch, telephony platform.
Storage engines: cyclone ships with built-in support for inline SQLite, and Redis. MongoDB and many other NoSQL are supported with 3rd party libraries. All other RDBMs supported by Python are available as well, like MySQL and PostgreSQL, via twisted.enterprise.adbapi. Connection pools can persist, and be efficiently used by all requests. It can also auto-reconnect automatically, making it totally fault-tolerant on database errors and disconnections.
For the simple, and the complex: cyclone-based web apps can be written as
Bottle, or Tornado. A 10-line script can handle
thousands of connections per second, with very low CPU and memory footprint.
For more complex applications, cyclone offers an app template out of the box,
with a configuration file, database support, translation, and deployment
scheme, fully integrated with Debian GNU/Linux. Via twistd
, cyclone-based
apps can be easily deployed in any operating system, with customized log and
pid files, reactor, permissions, and many other settings.
Documented, here and there, mostly by sample code. Features are either documented here, or in the demos. Check out the demos. For some other stuff, we use the Tornado docs. Like HTML templates, Escaping and string manipulation, Locale, and OpenID and Oauth.
Check out the benchmarks page.
cyclone can be installed from pip
or easy_install
:
$ pip install cyclone
It has no external dependencies other than Twisted 10 or newer, running on Python 2.6 or 2.7, or PyPy 1.8 or 1.9.
It runs on Python 2.5 too, but requires the simplejson
module. Also, it
runs on PyPy 1.8 as long as there's no SSL.
On most Linux distributions, such as Debian, twisted is splitted in several
packages. In this case, you'll need at least python-twisted-core
,
python-twisted-web
, and optionally python-twisted-mail
- if you want to
be able to send e-mails straight from cyclone apps.
Source code is available on https://github.com/fiorix/cyclone, and ships with very useful and comprehensive demo applications.
Check out the demos.
cyclone apps can be run by twistd
, and it's the easiest way to start
playing with the framework.
This is hello.py
:
import cyclone.web class MainHandler(cyclone.web.RequestHandler): def get(self): self.write("hello, world") Application = lambda: cyclone.web.Application([(r"/", MainHandler)])
A dev server can be started like this:
$ twistd -n cyclone -r hello.Application Log opened. reactor class: twisted.internet.selectreactor.SelectReactor. cyclone.web.Application starting on 8888
Due to the power of twistd
, cyclone apps can be easily deployed in
production, with all the basic features of standard daemons:
$ twistd --uid=www-data --gid=www-data --reactor=epoll \ --logfile=/var/log/hello.log --pidfile=/var/run/hello.log \ cyclone --port=80 --listen=0.0.0.0 --app=hello.Application
Process permissions are properly set, log files rotate automatically, syslog is also an option, pid files are generated so other subsystems can use it on start/stop scripts.
Setting up SSL on the same server is just a matter or creating a certificate
and adding --ssl-app=hello.Application
to the command line. It could easily
point to yet another Application
class, with a completely different URL
routing. Check out the SSL demo.
Run twistd --help
for more details. Here's a complete list of options
supported by the cyclone twisted plugin:
$ twistd cyclone --help Usage: twistd [options] cyclone [options] Options: -p, --port= tcp port to listen on [default: 8888] -l, --listen= interface to listen on [default: 127.0.0.1] -r, --app= cyclone application to run -c, --appopts= arguments to your application --ssl-port= port to listen on for ssl [default: 8443] --ssl-listen= interface to listen on for ssl [default: 127.0.0.1] --ssl-cert= ssl certificate [default: server.crt] --ssl-key= ssl server key [default: server.key] --ssl-app= ssl application (same as --app) --ssl-appopts= arguments to the ssl application --version --help Display this help and exit.
cyclone ships with a full featured project template. It helps on avoiding the repetitive process of creating a basic project structure, like parsing configuration files, setting up database connections, and translation of code and HTML templates.
$ python -m cyclone.app --help use: cyclone.app [options] Options: -h --help Show this help. -p --project=NAME Create new cyclone project. -g --git Use in conjunction with -p to make it a git repository. -m --modname=NAME Use another name for the module [default: project_name] -v --version=VERSION Set project version [default: 0.1] -s --set-pkg-version Set version on package name [default: False] -t --target=PATH Set path where project is created [default: ./]
Creating new projects can be as simple as running this:
$ python -m cyclone.app -p foobar
Check README.rst in the new project directory for detailed information on how to use it.
The template ships with Debian init scripts for running twistd
as single,
or multiple instances (one per CPU core) to help make deployments as simple as
possible.
As a clone, the API implemented in cyclone is almost the same of Tornado. Therefore you may use Tornado Documentation for things like auth, and the template engine.
The snippets below will show some tips and tricks regarding the few differences between the two.
First things first: you don't need to care about deferreds at all to start playing with cyclone. http://cyclone.io/ssedemo is an example.
However, Deferreds might help take your app to a whole new level.
cyclone uses deferreds extensively, to provide persistent database connections, and, generally speaking, to allow web apps to easily communicate with other subsystems on demand, while handling HTTP requests.
Here's an example, crawler.py
:
# run: twistd -n cyclone -r crawler.Application import cyclone.web from twisted.internet import defer from twisted.web.client import getPage class MainHandler(cyclone.web.RequestHandler): @defer.inlineCallbacks def get(self): response = yield getPage("http://freegeoip.net/xml/") self.set_header("Content-Type", "text/plain") self.write(response) Application = lambda: cyclone.web.Application([("/", MainHandler)], debug=True)
The example above is an app that makes a new request to freegeoip.net
on
each request it takes, and respond to this request with whatever it gets from
freegeoip. All without blocking the server.
The exact same concept is used to communicate with databases. Basically, using
inlineCallbacks
eliminates the nightmare of dealing with chained deferreds
and their responses in different callbacks. This way is simple and
straightforward.
Here is another example, delayed.py
:
# run: twistd -n cyclone -r delayed.Application import cyclone.web from twisted.internet import defer from twisted.internet import reactor def sleep(n): d = defer.Deferred() reactor.callLater(n, lambda: d.callback(None)) return d class MainHandler(cyclone.web.RequestHandler): @defer.inlineCallbacks def get(self): yield sleep(5) self.write("hello, world") Application = lambda: cyclone.web.Application([("/", MainHandler)], debug=True)
There are other useful examples in the demos directory. Take a
look at demos/email
, demos/redis
, and demos/httpauth
.
By default, cyclone will terminate the request after it is processed by the
RequestHandler
. Consider this code:
class MainHandler(cyclone.web.RequestHandler): def get(self): self.write("hello, world")
The above request is always terminated after get
returns. Even if get
returns a deferred, the request is automatically terminated after the deferred
is fired.
The cyclone.web.asynchronous
decorator can be used to keep the request
open until self.finish()
is explicitly called. The request will be in a
stale state, allowing for sending late, and incremental (chunked) responses.
Here's an example, clock.py
:
# run: twistd -n cyclone -r clock.Application import cyclone.web import time from twisted.internet import task from twisted.internet import reactor class MessagesMixin(object): clients = [] @classmethod def setup(self): task.LoopingCall(MessagesMixin.broadcast).start(1) @classmethod def broadcast(self): for req in MessagesMixin.clients: req.write("%s\r\n" % time.ctime()) req.flush() class MainHandler(cyclone.web.RequestHandler, MessagesMixin): @cyclone.web.asynchronous def get(self): self.set_header("Content-Type", "text/plain") self.clients.append(self) d = self.notifyFinish() d.addCallback(lambda *ign: self.clients.remove(self)) class Application(cyclone.web.Application): def __init__(self): reactor.callWhenRunning(MessagesMixin.setup) cyclone.web.Application.__init__(self, [("/", MainHandler)])
This server will never terminate client connections. Instead, it'll send one message per second, to all clients, forever.
Whenever the client disconnects, it's automatically removed from the list of
connected clients - notifyFinish()
returns a deferred, which is fired
when the connection is terminated.
A quick refresh: @inlineCallbacks
turns decorated functions into
deferreds so they can cooperatively call functions that returns deferreds
and handle their results inline, making the code much simpler.
@asynchronous
is used to keep the connection open until explicitly
terminated. And @authenticated
is used to require the client to be logged
in, usually via a control Cookie.
All can be mixed up, but some care has to be taken.
When multiple decorators are applied to a request method, @authenticated
must always be the first (top of other decorators). The reason for this, is
because if authentication fails the request shouln't be processed.
For the other two, inlineCallbacks
and @authenticated
, sequence doesn't
really matter.
class MainHandler(cyclone.web.RequestHandler): @cyclone.web.authenticated @cyclone.web.asynchronous @defer.inlineCallbacks def get(self): ... @cyclone.web.authenticated @defer.inlineCallbacks @cyclone.web.asynchronous def post(self): ...
cyclone.locale
uses gettext
to provide translation to strings in the
server code, and any other text or HTML templates.
It must be initialized with a domain, which is where all translation messages
are stored. Refer to the gettext
manual for details on usage, domains, etc.
The default domain is cyclone, and must be changed to the name of your app or module. All translation files are named after the domain.
There is a complete example of internationalized web application in demos/locale.
Also, the project template that ships with cyclone is already prepared for full translation. Give it a try:
$ python -m cyclone.app -p foobar
Then check the contents of foobar/
.
Keep-Alive
Because of the HTTP 1.1 support, sockets aren't always closed when you call
self.finish()
in aRequestHandler
. cyclone lets you enforce that by setting theno_keep_alive
class attribute attribute:class IndexHandler(cyclone.web.RequestHandler): no_keep_alive = True def get(self): ...
With this, cyclone will always close the socket after
self.finish()
is called on all methods (get, post, etc) of thisRequestHandler
class.XSRF
By default, XSRF is either enabled or disabled for the entire server, because it's set in Application's settings:
cyclone.web.Application(handlers, xsrf_cookies=True)
That might be a problem if the application is also serving an API, like a RESTful API supposed to work with HTTP clients other than the browser.
For those endpoints, it's possible to disable XSRF:
class WebLoginHandler(cyclone.web.RequestHandler): def post(self): u = self.get_argument("username") ... class APILoginHandler(cyclone.web.RequestHandler): no_xsrf = True def post(self): u = self.get_argument("username") ...
Socket closed notification
One of the great features of
TwistedWeb
is therequest.notifyFinish()
, which is also available in cyclone.This method returns a deferred, which is fired when the request socket is closed, by either
self.finish()
, someone closing their browser while receiving data, or closing the connection of a Comet request:class IndexHandler(cyclone.web.RequestHandler): def get(self): ... d = self.notifyFinish() d.addCallback(remove_from_comet_handlers_list) def on_finish(self): # alternative to notifyFinish pass
This was implemented before Tornado added support to
on_finish
. Currently, both methods are supported in cyclone.HTTP X-Headers
When running a cyclone-based application behind Nginx, it's very important to make it automatically use X-Real-Ip and X-Scheme HTTP headers. In order to make cyclone recognize those headers, the option
xheaders=True
must be set in the Application settings:cyclone.web.Application(handlers, xheaders=True)
Cookie-Secret generation
The following code can be used to generate random cookie secrets:
>>> import uuid, base64 >>> base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes) 'FoQv5hgLTYCb9aKiBagpJJYtLJInWUcXilg3/vPkUnI='
SSL
cyclone can serve SSL or sit behind a termination proxy (e.g. Nginx). Make sure that you bind the right port with listenSSL, passing the certs:
import cyclone.web import sys from twisted.internet import reactor from twisted.internet import ssl from twisted.python import log class MainHandler(cyclone.web.RequestHandler): def get(self): self.write("Hello, world") def main(): log.startLogging(sys.stdout) application = cyclone.web.Application([(r"/", MainHandler)]) interface = "127.0.0.1" reactor.listenTCP(8888, application, interface=interface) reactor.listenSSL(8443, application, ssl.DefaultOpenSSLContextFactory("server.key", "server.crt"), interface=interface) reactor.run() if __name__ == "__main__": main()
This example plus a script to generate certificates sits under demos/ssl.
Where are the request headers?
They are part of the request, dude:
class MyHandler(cyclone.web.RequestHandler): def get(self): # self.request.headers is a dict user_agent = self.request.headers.get("User-Agent")
How do I access raw POST data?
Both raw POST data and GET/DELETE un-parsed query strings are available:
class MyHandler(cyclone.web.RequestHandler): def get(self): raw = self.request.query def post(self): raw = self.request.body
Where is the request information like remote IP address, etc?
Everything is available as request attributes, like protocol, HTTP method, URI and version:
class MyHandler(cyclone.web.RequestHandler): def get(self): proto = self.request.protocol remote_ip = self.request.remote_ip method = self.request.method uri = self.request.uri version = self.request.version
How do I set my own headers for the reply?
Guess what, use
self.set_header(name, value)
:class MyHandler(cyclone.web.RequestHandler): def get(self): self.set_header("Content-Type", "application/json") self.finish(cyclone.escape.json_encode({"success":True}))
What HTTP methods are supported in RequestHandler?
Well, almost all of them. HEAD, GET, POST, DELETE, PATCH, PUT and OPTIONS are supported. TRACE is disabled by default, because it may get you in trouble. CONNECT has nothing to do with web servers, it's for proxies.
For more information on HTTP 1.1 methods, please refer to the RFC 2612 Fielding, et al..
For information regarding TRACE vulnerabilities, please check the following links: What is HTTP TRACE? and Apache Week, security issues.
Supporting different HTTP methods in the same RequestHandler is easy:
class MyHandler(cyclone.web.RequestHandler): def get(self): pass def head(self): pass def post(self): pass def delete(self): pass
How to handle file uploads?
They are available inside the request object as
self.request.files
. Make sure your HTML form encoding ismultipart/form-data
:class MyHandler(cyclone.web.RequestHandler): def post(self): photos = self.request.files.get("photos") # Because it's possible to upload several files under the # same form name, we fetch the first uploaded photo. first_photo = photos[0] # first_photo.filename: original filename # first_photo.content_type: parsed content type (not mime-type) # first_photo.body: file contents
There's an example in demos/upload.
Thanks to (in no particular order):
- Nuswit Telephony API
- Granting permission for this code to be published and sponsoring
- Gleicon Moraes
- Testing and using it in the RestMQ web service
- Vanderson Mota
- Patching setup.py and PyPi maintenance
- Andrew Badr
- Fixing auth bugs and adding current Tornado's features
- Jon Oberheide
- Syncing code with Tornado and security features/fixes
- Silas Sewell
- Syncing code and minor mail fix
- Twitter Bootstrap
- For making our demo applications look good
- Dan Griffin
- WebSocket Keep-Alive for OpDemand
- Toby Padilla
- WebSocket server
- Jeethu Rao
- Minor bugfixes and patches
- Flavio Grossi
- Minor code fixes and websockets chat statistics example