+++ /dev/null
-Running the Tornado AppEngine example
-=====================================
-This example is designed to run in Google AppEngine, so there are a couple
-of steps to get it running. You can download the Google AppEngine Python
-development environment at http://code.google.com/appengine/downloads.html.
-
-1. Link or copy the tornado code directory into this directory:
-
- ln -s ../../tornado tornado
-
- AppEngine doesn't use the Python modules installed on this machine.
- You need to have the 'tornado' module copied or linked for AppEngine
- to find it.
-
-3. Install and run dev_appserver
-
- If you don't already have the App Engine SDK, download it from
- http://code.google.com/appengine/downloads.html
-
- To start the tornado demo, run the dev server on this directory:
-
- dev_appserver.py .
-
-4. Visit http://localhost:8080/ in your browser
-
- If you sign in as an administrator, you will be able to create and
- edit blog posts. If you sign in as anybody else, you will only see
- the existing blog posts.
-
-
-If you want to deploy the blog in production:
-
-1. Register a new appengine application and put its id in app.yaml
-
- First register a new application at http://appengine.google.com/.
- Then edit app.yaml in this directory and change the "application"
- setting from "tornado-appenginge" to your new application id.
-
-2. Deploy to App Engine
-
- If you registered an application id, you can now upload your new
- Tornado blog by running this command:
-
- appcfg update .
-
- After that, visit application_id.appspot.com, where application_id
- is the application you registered.
-
+++ /dev/null
-application: tornado-appengine
-version: 2
-runtime: python27
-api_version: 1
-threadsafe: yes
-
-handlers:
-- url: /static/
- static_dir: static
-
-- url: /.*
- script: blog.application
+++ /dev/null
-#
-# Copyright 2009 Facebook
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import functools
-import os.path
-import re
-import tornado.escape
-import tornado.web
-import tornado.wsgi
-import unicodedata
-
-from google.appengine.api import users
-from google.appengine.ext import db
-
-
-class Entry(db.Model):
- """A single blog entry."""
- author = db.UserProperty()
- title = db.StringProperty(required=True)
- slug = db.StringProperty(required=True)
- body_source = db.TextProperty(required=True)
- html = db.TextProperty(required=True)
- published = db.DateTimeProperty(auto_now_add=True)
- updated = db.DateTimeProperty(auto_now=True)
-
-
-def administrator(method):
- """Decorate with this method to restrict to site admins."""
- @functools.wraps(method)
- def wrapper(self, *args, **kwargs):
- if not self.current_user:
- if self.request.method == "GET":
- self.redirect(self.get_login_url())
- return
- raise tornado.web.HTTPError(403)
- elif not self.current_user.administrator:
- if self.request.method == "GET":
- self.redirect("/")
- return
- raise tornado.web.HTTPError(403)
- else:
- return method(self, *args, **kwargs)
- return wrapper
-
-
-class BaseHandler(tornado.web.RequestHandler):
- """Implements Google Accounts authentication methods."""
- def get_current_user(self):
- user = users.get_current_user()
- if user:
- user.administrator = users.is_current_user_admin()
- return user
-
- def get_login_url(self):
- return users.create_login_url(self.request.uri)
-
- def get_template_namespace(self):
- # Let the templates access the users module to generate login URLs
- ns = super(BaseHandler, self).get_template_namespace()
- ns['users'] = users
- return ns
-
-
-class HomeHandler(BaseHandler):
- def get(self):
- entries = db.Query(Entry).order('-published').fetch(limit=5)
- if not entries:
- if not self.current_user or self.current_user.administrator:
- self.redirect("/compose")
- return
- self.render("home.html", entries=entries)
-
-
-class EntryHandler(BaseHandler):
- def get(self, slug):
- entry = db.Query(Entry).filter("slug =", slug).get()
- if not entry:
- raise tornado.web.HTTPError(404)
- self.render("entry.html", entry=entry)
-
-
-class ArchiveHandler(BaseHandler):
- def get(self):
- entries = db.Query(Entry).order('-published')
- self.render("archive.html", entries=entries)
-
-
-class FeedHandler(BaseHandler):
- def get(self):
- entries = db.Query(Entry).order('-published').fetch(limit=10)
- self.set_header("Content-Type", "application/atom+xml")
- self.render("feed.xml", entries=entries)
-
-
-class ComposeHandler(BaseHandler):
- @administrator
- def get(self):
- key = self.get_argument("key", None)
- entry = Entry.get(key) if key else None
- self.render("compose.html", entry=entry)
-
- @administrator
- def post(self):
- key = self.get_argument("key", None)
- if key:
- entry = Entry.get(key)
- entry.title = self.get_argument("title")
- entry.body_source = self.get_argument("body_source")
- entry.html = tornado.escape.linkify(
- self.get_argument("body_source"))
- else:
- title = self.get_argument("title")
- slug = unicodedata.normalize("NFKD", title).encode(
- "ascii", "ignore")
- slug = re.sub(r"[^\w]+", " ", slug)
- slug = "-".join(slug.lower().strip().split())
- if not slug:
- slug = "entry"
- while True:
- existing = db.Query(Entry).filter("slug =", slug).get()
- if not existing or str(existing.key()) == key:
- break
- slug += "-2"
- entry = Entry(
- author=self.current_user,
- title=title,
- slug=slug,
- body_source=self.get_argument("body_source"),
- html=tornado.escape.linkify(self.get_argument("body_source")),
- )
- entry.put()
- self.redirect("/entry/" + entry.slug)
-
-
-class EntryModule(tornado.web.UIModule):
- def render(self, entry):
- return self.render_string("modules/entry.html", entry=entry)
-
-
-settings = {
- "blog_title": u"Tornado Blog",
- "template_path": os.path.join(os.path.dirname(__file__), "templates"),
- "ui_modules": {"Entry": EntryModule},
- "xsrf_cookies": True,
-}
-application = tornado.web.Application([
- (r"/", HomeHandler),
- (r"/archive", ArchiveHandler),
- (r"/feed", FeedHandler),
- (r"/entry/([^/]+)", EntryHandler),
- (r"/compose", ComposeHandler),
-], **settings)
-
-application = tornado.wsgi.WSGIAdapter(application)
+++ /dev/null
-/*
- * Copyright 2009 Facebook
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License. You may obtain
- * a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-
-body {
- background: white;
- color: black;
- margin: 15px;
- margin-top: 0;
-}
-
-body,
-input,
-textarea {
- font-family: Georgia, serif;
- font-size: 12pt;
-}
-
-table {
- border-collapse: collapse;
- border: 0;
-}
-
-td {
- border: 0;
- padding: 0;
-}
-
-h1,
-h2,
-h3,
-h4 {
- font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
- margin: 0;
-}
-
-h1 {
- font-size: 20pt;
-}
-
-pre,
-code {
- font-family: monospace;
- color: #060;
-}
-
-pre {
- margin-left: 1em;
- padding-left: 1em;
- border-left: 1px solid silver;
- line-height: 14pt;
-}
-
-a,
-a code {
- color: #00c;
-}
-
-#body {
- max-width: 800px;
- margin: auto;
-}
-
-#header {
- background-color: #3b5998;
- padding: 5px;
- padding-left: 10px;
- padding-right: 10px;
- margin-bottom: 1em;
-}
-
-#header,
-#header a {
- color: white;
-}
-
-#header h1 a {
- text-decoration: none;
-}
-
-#footer,
-#content {
- margin-left: 10px;
- margin-right: 10px;
-}
-
-#footer {
- margin-top: 3em;
-}
-
-.entry h1 a {
- color: black;
- text-decoration: none;
-}
-
-.entry {
- margin-bottom: 2em;
-}
-
-.entry .date {
- margin-top: 3px;
-}
-
-.entry p {
- margin: 0;
- margin-bottom: 1em;
-}
-
-.entry .body {
- margin-top: 1em;
- line-height: 16pt;
-}
-
-.compose td {
- vertical-align: middle;
- padding-bottom: 5px;
-}
-
-.compose td.field {
- padding-right: 10px;
-}
-
-.compose .title,
-.compose .submit {
- font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
- font-weight: bold;
-}
-
-.compose .title {
- font-size: 20pt;
-}
-
-.compose .title,
-.compose .body_source {
- width: 100%;
-}
-
-.compose .body_source {
- height: 500px;
- line-height: 16pt;
-}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block head %}
- <style type="text/css">
- ul.archive {
- list-style-type: none;
- margin: 0;
- padding: 0;
- }
-
- ul.archive li {
- margin-bottom: 1em;
- }
-
- ul.archive .title {
- font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
- font-size: 14pt;
- }
- </style>
-{% end %}
-
-{% block body %}
- <ul class="archive">
- {% for entry in entries %}
- <li>
- <div class="title"><a href="/entry/{{ entry.slug }}">{{ entry.title }}</a></div>
- <div class="date">{{ locale.format_date(entry.published, full_format=True, shorter=True) }}</div>
- </li>
- {% end %}
- </ul>
-{% end %}
+++ /dev/null
-<!DOCTYPE html>
-<html>
- <head>
- <meta charset="UTF-8">
- <title>{{ handler.settings["blog_title"] }}</title>
- <link rel="stylesheet" href="/static/blog.css" type="text/css">
- <link rel="alternate" href="/feed" type="application/atom+xml" title="{{ handler.settings["blog_title"] }}">
- {% block head %}{% end %}
- </head>
- <body>
- <div id="body">
- <div id="header">
- <div style="float:right">
- {% if not current_user %}
- {% raw _('<a href="%(url)s">Sign in</a> to compose/edit') % {"url": escape(users.create_login_url(request.uri))} %}
- {% else %}
- {% if current_user.administrator %}
- <a href="/compose">{{ _("New post") }}</a> -
- {% end %}
- <a href="{{ users.create_logout_url(request.uri) }}">{{ _("Sign out") }}</a>
- {% end %}
- </div>
- <h1><a href="/">{{ handler.settings["blog_title"] }}</a></h1>
- </div>
- <div id="content">{% block body %}{% end %}</div>
- </div>
- {% block bottom %}{% end %}
- </body>
-</html>
+++ /dev/null
-{% extends "base.html" %}
-
-{% block body %}
- <form action="{{ request.path }}" method="post" class="compose">
- <div style="margin-bottom:5px"><input name="title" type="text" class="title" value="{{ entry.title if entry else "" }}"/></div>
- <div style="margin-bottom:5px"><textarea name="body_source" rows="30" cols="40" class="body_source">{{ entry.body_source if entry else "" }}</textarea></div>
- <div>
- <input type="submit" value="{{ _("Save changes") if entry else _("Publish post") }}" class="submit"/>
- <a href="{{ "/entry/" + entry.slug if entry else "/" }}">{{ _("Cancel") }}</a>
- </div>
- {% if entry %}
- <input type="hidden" name="key" value="{{ str(entry.key()) }}"/>
- {% end %}
- {% module xsrf_form_html() %}
- </form>
-{% end %}
-
-{% block bottom %}
- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script>
- <script type="text/javascript">
- //<![CDATA[
-
- $(function() {
- $("input[name=title]").select();
- $("form.compose").submit(function() {
- var required = ["title", "body_source"];
- var form = $(this).get(0);
- for (var i = 0; i < required.length; i++) {
- if (!form[required[i]].value) {
- $(form[required[i]]).select();
- return false;
- }
- }
- return true;
- });
- });
-
- //]]>
- </script>
-{% end %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block body %}
- {% module Entry(entry) %}
-{% end %}
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<feed xmlns="http://www.w3.org/2005/Atom">
- {% set date_format = "%Y-%m-%dT%H:%M:%SZ" %}
- <title>{{ handler.settings["blog_title"] }}</title>
- {% if len(entries) > 0 %}
- <updated>{{ max(e.updated for e in entries).strftime(date_format) }}</updated>
- {% else %}
- <updated>{{ datetime.datetime.utcnow().strftime(date_format) }}</updated>
- {% end %}
- <id>http://{{ request.host }}/</id>
- <link rel="alternate" href="http://{{ request.host }}/" title="{{ handler.settings["blog_title"] }}" type="text/html"/>
- <link rel="self" href="{{ request.full_url() }}" title="{{ handler.settings["blog_title"] }}" type="application/atom+xml"/>
- <author><name>{{ handler.settings["blog_title"] }}</name></author>
- {% for entry in entries %}
- <entry>
- <id>http://{{ request.host }}/entry/{{ entry.slug }}</id>
- <title type="text">{{ entry.title }}</title>
- <link href="http://{{ request.host }}/entry/{{ entry.slug }}" rel="alternate" type="text/html"/>
- <updated>{{ entry.updated.strftime(date_format) }}</updated>
- <published>{{ entry.published.strftime(date_format) }}</published>
- <content type="xhtml" xml:base="http://{{ request.host }}/">
- <div xmlns="http://www.w3.org/1999/xhtml">{% raw entry.html %}</div>
- </content>
- </entry>
- {% end %}
-</feed>
+++ /dev/null
-{% extends "base.html" %}
-
-{% block body %}
- {% for entry in entries %}
- {% module Entry(entry) %}
- {% end %}
- <div><a href="/archive">{{ _("Archive") }}</a></div>
-{% end %}
+++ /dev/null
-<div class="entry">
- <h1><a href="/entry/{{ entry.slug }}">{{ entry.title }}</a></h1>
- <div class="date">{{ locale.format_date(entry.published, full_format=True, shorter=True) }}</div>
- <div class="body">{% raw entry.html %}</div>
- {% if current_user and current_user.administrator %}
- <div class="admin"><a href="/compose?key={{ str(entry.key()) }}">{{ _("Edit this post") }}</a></div>
- {% end %}
-</div>
+++ /dev/null
-Unit test support for app engine. Currently very limited as most of
-our tests depend on direct network access, but these tests ensure that the
-modules that are supposed to work on app engine don't depend on any
-forbidden modules.
-
-The code lives in maint/appengine/common, but should be run from the py25
-or py27 subdirectories (which contain an app.yaml and a bunch of symlinks).
-runtests.py is the entry point; cgi_runtests.py is used internally.
+++ /dev/null
-#!/usr/bin/env python
-from __future__ import absolute_import, division, print_function
-
-import sys
-import unittest
-
-# Most of our tests depend on IOLoop, which is not usable on app engine.
-# Run the tests that work, and check that everything else is at least
-# importable (via tornado.test.import_test)
-TEST_MODULES = [
- 'tornado.httputil.doctests',
- 'tornado.iostream.doctests',
- 'tornado.util.doctests',
- #'tornado.test.auth_test',
- #'tornado.test.concurrent_test',
- #'tornado.test.curl_httpclient_test',
- 'tornado.test.escape_test',
- #'tornado.test.gen_test',
- #'tornado.test.httpclient_test',
- #'tornado.test.httpserver_test',
- 'tornado.test.httputil_test',
- 'tornado.test.import_test',
- #'tornado.test.ioloop_test',
- #'tornado.test.iostream_test',
- 'tornado.test.locale_test',
- #'tornado.test.netutil_test',
- #'tornado.test.log_test',
- 'tornado.test.options_test',
- #'tornado.test.process_test',
- #'tornado.test.simple_httpclient_test',
- #'tornado.test.stack_context_test',
- 'tornado.test.template_test',
- #'tornado.test.testing_test',
- #'tornado.test.twisted_test',
- 'tornado.test.util_test',
- #'tornado.test.web_test',
- #'tornado.test.websocket_test',
- #'tornado.test.wsgi_test',
-]
-
-
-def all():
- return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES)
-
-
-def main():
- print("Content-Type: text/plain\r\n\r\n", end="")
-
- try:
- unittest.main(defaultTest='all', argv=sys.argv[:1])
- except SystemExit as e:
- if e.code == 0:
- print("PASS")
- else:
- raise
-
-
-if __name__ == '__main__':
- main()
+++ /dev/null
-#!/usr/bin/env python
-from __future__ import absolute_import, division, print_function
-
-import contextlib
-import errno
-import os
-import random
-import signal
-import socket
-import subprocess
-import sys
-import time
-import urllib2
-
-try:
- xrange
-except NameError:
- xrange = range
-
-if __name__ == "__main__":
- tornado_root = os.path.abspath(os.path.join(os.path.dirname(__file__),
- '../../..'))
- # dev_appserver doesn't seem to set SO_REUSEADDR
- port = random.randrange(10000, 11000)
- # does dev_appserver.py ever live anywhere but /usr/local/bin?
- proc = subprocess.Popen([sys.executable,
- "/usr/local/bin/dev_appserver.py",
- os.path.dirname(os.path.abspath(__file__)),
- "--port=%d" % port,
- "--skip_sdk_update_check",
- ],
- cwd=tornado_root)
-
- try:
- for i in xrange(50):
- with contextlib.closing(socket.socket()) as sock:
- err = sock.connect_ex(('localhost', port))
- if err == 0:
- break
- elif err != errno.ECONNREFUSED:
- raise Exception("Got unexpected socket error %d" % err)
- time.sleep(0.1)
- else:
- raise Exception("Server didn't start listening")
-
- resp = urllib2.urlopen("http://localhost:%d/" % port)
- print(resp.read())
- finally:
- # dev_appserver sometimes ignores SIGTERM (especially on 2.5),
- # so try a few times to kill it.
- for sig in [signal.SIGTERM, signal.SIGTERM, signal.SIGKILL]:
- os.kill(proc.pid, sig)
- res = os.waitpid(proc.pid, os.WNOHANG)
- if res != (0, 0):
- break
- time.sleep(0.1)
- else:
- os.waitpid(proc.pid, 0)
+++ /dev/null
-application: tornado-tests-appengine27
-version: 1
-runtime: python27
-threadsafe: false
-api_version: 1
-
-handlers:
-- url: /
- script: cgi_runtests.py
\ No newline at end of file
+++ /dev/null
-../common/cgi_runtests.py
\ No newline at end of file
+++ /dev/null
-../common/runtests.py
\ No newline at end of file
+++ /dev/null
-../../../../tornado
\ No newline at end of file
+++ /dev/null
-# Dummy setup file to make tox happy. In the appengine world things aren't
-# installed through setup.py
-import distutils.core
-distutils.core.setup()
+++ /dev/null
-# App Engine tests require the SDK to be installed separately.
-# Version 1.6.1 or newer is required (older versions don't work when
-# python is run from a virtualenv)
-#
-# These are currently excluded from the main tox.ini because their
-# logs are spammy and they're a little flaky.
-[tox]
-envlist = py27-appengine
-
-[testenv]
-changedir = {toxworkdir}
-
-[testenv:py27-appengine]
-basepython = python2.7
-commands = python {toxinidir}/py27/runtests.py {posargs:}